Version 1.0.0 - Finalisation de Account et de GuildHeader

This commit is contained in:
2025-08-06 23:07:34 +02:00
parent 93379b0042
commit 4ab23f7c52
48 changed files with 1949 additions and 175 deletions

View File

@@ -1,7 +1,7 @@
{
"discord": {
"development": "https://discord.com/oauth2/authorize?client_id=1342913183744004158&response_type=code&redirect_uri=http%3A%2F%2F192.168.1.77%3A8080%2Fredirect&scope=guilds+identify+guilds.members.read",
"production": "https://discord.com/oauth2/authorize?client_id=1094727789682380922&response_type=code&redirect_uri=https%3A%2F%2Fsubsonics.raphix.fr%2Fredirect&scope=guilds+identify+guilds.members.read"
"development": "https://discord.com/oauth2/authorize?client_id=1342913183744004158&response_type=code&redirect_uri=http%3A%2F%2F192.168.1.77%3A8080%2Fredirect&scope=identify",
"production": "https://discord.com/oauth2/authorize?client_id=1094727789682380922&response_type=code&redirect_uri=https%3A%2F%2Fsubsonics.raphix.fr%2Fredirect&scope=identify"
},
"backend": {
"development": "http://192.168.1.77:3000",

View File

@@ -2,7 +2,6 @@
<router-view/>
</template>
<script setup>
import { onMounted } from 'vue'
import { useGlobalStore } from '@/stores/globalStore';
const globalStore = useGlobalStore();
console.log("Subsonics Chopin - App Vue Loaded");

View File

@@ -4,33 +4,44 @@
}
[data-theme='dark'] {
--main: #CD034F;
--main-hover: #A0023F;
--main-active: #7A002F;
--primary: #111210;
--primary-hover: #ececec;
--secondary: #2A2B28;
--tertiary: #404040;
--text: #FFFFFF;
--text-inverse: #111210;
--text-secondary: #C5c3c3;
--text-tertiary: #A5A5A5;
--text-error: #ff2b2b;
color: var(--text);
}
[data-theme='light'] {
--primary: #FFFFFF;
--primary-hover: #292b26;
--secondary: #EAEAEA;
--tertiary: #cacaca;
--text: #111210;
--text-inverse: #FFFFFF;
--text-secondary: #404040;
--text-tertiary: #C5c3c3;
--text-error: #CD034F;
}
:root {
--main: #CD034F;
--main-hover: #A0023F;
--main-active: #7A002F;
--primary: #FFFFFF;
--secondary: #EAEAEA;
--tertiary: #C5c3c3;
--text: #111210;
--text-secondary: #404040;
--text-error: #CD034F;
--text-success: #00ff00;
color: var(--text);
--admin-color: #209AFE;
--owner-color: #FFAA32;
--mod-color: #0BFF89;
}
html, body {
@@ -43,7 +54,9 @@ html, body {
height: 100%;
overflow-x: hidden;
background-color: var(--primary);
transition: all 0.5s ease-in-out;
transition: all 0.2s ease-in-out;
color: var(--text);
}
@@ -86,3 +99,53 @@ a {
.full {
width: 100%;
}
textarea {
resize: vertical;
background-color: var(--tertiary);
border: none;
border-radius: 5px;
padding: 5px;
color: var(--text);
font-family: 'Inter', sans-serif;
outline: none;
min-height: 4vh;
max-height: 15vh;
}
textarea:focus {
box-shadow: 0 0 5px var(--main);
}
@keyframes appear {
from {
transform: scale(0.9);
}
to {
transform: scale(1);
}
}
@keyframes unfold {
from {
max-height: 0px;
opacity: 0;
}
to {
max-height: 100vh;
opacity: 1;
}
}
input[type="checkbox"] {
background-color: #FFFFFF !important;
}
input[type="checkbox"]:checked {
background-color: var(--main-hover) !important;
}
input[type="checkbox"]:hover {
background-color: var(--main-active) !important;
}

View File

@@ -1,87 +0,0 @@
<template>
<Box padding="closed">
<div class="container">
<SubsonicsLogo/>
<div class="server-box">
<Box ref="collapsedBoxRef" level="second" padding="closed">
<div v-if="server" class="itm">
<ServerItem :server="server"/>
<div class="actions">
<ListenBox>{{ server.members.length + 1}}</ListenBox>
<IconAction @click="showMenu = !showMenu" :icon="showMenu ? 'fa-solid fa-angle-up' : 'fa-solid fa-angle-down'"/>
</div>
</div>
<Error v-else><ServerItem/></Error>
<div v-if="showMenu">
<p>Other items</p>
</div>
</Box>
</div>
</div>
</Box>
</template>
<script setup>
import SubsonicsLogo from '@/components/UI/SubsonicsLogo.vue';
import Box from '@/components/UI/Box.vue';
import ServerItem from '@/components/Widget/ServerItem.vue';
import IconAction from '../UI/IconAction.vue';
import ListenBox from '@/components/UI/ListenBox.vue';
import Error from '@/components/UI/Error.vue';
import { IORequest } from '@/utils/IORequest';
import { ref, onMounted } from 'vue';
import events from '@/utils/Events';
import { watch } from 'vue';
const server = ref(undefined)
const showMenu = ref(false);
events.on("UPDATE", () => {
updateServerInfo();
})
onMounted(() => {
updateServerInfo();
})
function updateServerInfo() {
IORequest("/GUILD/INFO", (data) => {
server.value = data;
})
}
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
gap: 10px;;
}
.itm {
display: flex;
align-items: center;
justify-content: space-between;
}
.actions {
display: flex;
align-items: center;
gap: 10px;
}
.server-box {
width: 100%;
position: relative;
display: inline-block;
}
.collapsed-box {
height: auto;
}
</style>

View File

@@ -2,11 +2,13 @@
<Box box-class="box" padding="closed">
<User :user="userStore.userInfo" />
<div class="user-action">
<IconAction icon="fa-solid fa-gear" @click="goToSettings()"/>
<IconAction color="red" icon="fa-solid fa-right-from-bracket" @click="signOut(router)"/>
<IconAction title="Paramètres" icon="fa-solid fa-gear" @click="userSettings.open()"/>
<IconAction title="Déconnexion" color="red" icon="fa-solid fa-right-from-bracket" @click="signOut(router)"/>
</div>
<UserSettings ref="userSettings"/>
</Box>
</template>
<script setup>
import Box from '../UI/Box.vue';
@@ -21,15 +23,14 @@ if(!socket.connected) {
}
import { useUserStore } from '@/stores/userStore';
import Modal from '../UI/Modal.vue';
import UserSettings from '../Widget/User/UserSettings.vue';
import { ref, onMounted } from 'vue';
const userStore = useUserStore();
const router = useRouter();
function goToSettings() {
console.log(router)
router.push("/settings");
}
const userSettings = ref(null);
</script>
<style scoped>

View File

@@ -0,0 +1,170 @@
<template>
<Box padding="closed">
<div class="container">
<SubsonicsLogo/>
<div ref="serverBox" class="server-box">
<Box :overbox="showMenu" level="second" no-shadow padding="closed">
<div v-if="server" class="itm">
<ServerItem :server="server"/>
<div class="actions">
<ListenBox>{{ server.members.length + 1}}</ListenBox>
<IconAction @click="showMenu = !showMenu" :icon="showMenu ? 'fa-solid fa-angle-up' : 'fa-solid fa-angle-down'"/>
</div>
</div>
<Error v-else><ServerItem/></Error>
</Box>
<div :style="`width: ${serverBox.offsetWidth}px !important;`" class="menu" v-if="showMenu">
<div class="menu-content">
<div>
<p><Icon icon="fa-solid fa-users"/> Utilisateurs en ligne</p>
<div v-if="server" class="users-container">
<div v-if="server.members.length > 0" class="users-list">
<GuildHeaderUsers :server="server"/>
</div>
<Info secondary v-else>Aucun utilisateur en ligne</Info>
</div>
</div>
<div class="server-actions">
<Button v-if="gestion" @click="settings.open()"><Icon icon="fa-solid fa-gear"/>Gestion</Button>
<Button :style="{width: gestion ? '' : '100%'}" @click="router.push(`/servers`)"><Icon icon="fa-solid fa-arrow-right"/>Changer de serveur</Button>
</div>
</div>
</div>
<div v-if="showMenu" style="height: 10px;"></div>
<GuildSettings ref="settings" :server="server" v-if="server && gestion"/>
</div>
</div>
</Box>
</template>
<script setup>
import SubsonicsLogo from '@/components/UI/SubsonicsLogo.vue';
import Box from '@/components/UI/Box.vue';
import ServerItem from '@/components/UI/Server.vue';
import IconAction from '../UI/IconAction.vue';
import ListenBox from '@/components/UI/ListenBox.vue';
import Error from '@/components/UI/Error.vue';
import Info from '@/components/UI/Info.vue';
import Button from '@/components/UI/Button.vue';
import { IORequest } from '@/utils/IORequest';
import events from '@/utils/Events';
import { useRouter } from 'vue-router';
import { ref, onMounted, onUnmounted } from 'vue';
import GuildHeaderUsers from '../Widget/Guild/GuildHeaderUsers.vue';
import GuildSettings from '../Widget/Guild/GuildSettings.vue';
import { useUserStore } from '@/stores/userStore';
const userStore = useUserStore();
const server = ref(undefined)
const showMenu = ref(false);
const serverBox = ref(null);
const router = useRouter();
var gestion = ref(false)
const settings = ref(null);
events.on("UPDATE", () => {
updateServerInfo();
})
onMounted(() => {
updateServerInfo();
window.addEventListener('resize', () => {
showMenu.value = false;
});
})
onUnmounted(() => {
window.removeEventListener('resize', () => {
showMenu.value = false;
});
userStore.userInfo.identity.isAdmin = null;
userStore.userInfo.identity.isMod = null;
userStore.userInfo.identity.isOwner = null;
});
function updateServerInfo() {
IORequest("/GUILD/INFO", (data) => {
server.value = data;
userStore.userInfo.identity.isAdmin = userStore.userInfo.labels.includes('ADMIN');
userStore.userInfo.identity.isMod = userStore.userInfo.labels.includes('MOD_' + server.value.id);
userStore.userInfo.identity.isOwner = userStore.userInfo.identity.id == server.value.owner;
gestion.value = userStore.userInfo.labels.includes('ADMIN') || userStore.userInfo.labels.includes('MOD_' + server.value.id) || userStore.userInfo.identity.id == server.value.owner;
console.log("Server info updated");
events.emit("GUILD_JOINED");
})
}
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
gap: 10px;;
}
.itm {
display: flex;
align-items: center;
justify-content: space-between;
}
.actions {
display: flex;
align-items: center;
gap: 10px;
}
.server-box {
width: 100%;
position: relative;
display: inline-block;
}
.collapsed-box {
height: auto;
}
.server-actions {
display: flex;
justify-content: space-between;
}
.server-actions p {
margin: 0;
font-size: 12px;
}
.menu {
display: flex;
flex-direction: column;
gap: 10px;
position: absolute;
background-color: var(--tertiary);
border-radius: 0px 0px 10px 10px;
}
.menu-content {
padding: 0px 10px 10px 10px;
display: flex;
flex-direction: column;
gap: 10px;
}
.users-container {
position: relative;
flex: 1;
max-height: 20vh;
overflow-y: auto;
}
.users-list {
display: flex;
flex-direction: column;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,35 @@
<template>
<div class="search">
<input type="text" placeholder="Search..." v-model="searchQuery" />
</div>
</template>
<script setup>
import { ref } from 'vue';
const searchQuery = ref('');
</script>
<style scoped>
.search {
display: flex;
align-items: center;
padding: 10px;
background-color: var(--tertiary);
border-radius: 10px;
}
.search input {
border: none;
outline: none;
background: transparent;
color: var(--text-primary);
flex: 1;
padding: 5px 10px;
border-radius: 5px;
}
.search input::placeholder {
color: var(--text-secondary);
}
</style>

View File

@@ -1,12 +1,13 @@
<template>
<div>
<img :src="`https://cdn.discordapp.com/avatars/${userId}/${avatarUrl}`" alt='User Avatar'>
<Icon v-if="tag === 'admin'" style="color: var(--main);" icon="fa-solid fa-star" class="tag" />
<Icon v-if="tag === 'owner'" style="color: #FFAA32;" icon="fa-solid fa-crown" class="tag" />
<Icon v-if="tag === 'mod'" style="color: #0BFF89;" icon="fa-solid fa-shield-halved" class="tag" />
<Icon v-if="tag === 'admin'" style="color: var(--admin-color);" icon="fa-solid fa-star" class="tag" />
<Icon v-if="tag === 'owner'" style="color: var(--owner-color);" icon="fa-solid fa-crown" class="tag" />
<Icon v-if="tag === 'mod'" style="color: var(--mod-color);" icon="fa-solid fa-shield-halved" class="tag" />
</div>
</template>
<script setup>
import { computed } from 'vue';

View File

@@ -1,5 +1,5 @@
<template>
<div :style="widthStyle" :class="activeClass">
<div :style="widthStyle + overboxStyle" :class="activeClass">
<slot></slot>
</div>
</template>
@@ -7,8 +7,6 @@
div {
background-color: var(--secondary);
border-radius: 10px;
}
.box-shadow {
@@ -55,6 +53,10 @@ const props = defineProps({
noShadow: {
type: Boolean,
default: false
},
overbox: {
type: Boolean,
default: false
}
});
@@ -65,6 +67,10 @@ const widthStyle = computed(() => {
return '';
});
const overboxStyle = computed(() => {
return props.overbox ? 'border-radius: 10px 10px 0 0; padding: 10px 10px 0 10px;' : '';
});
const activeClass = computed(() => {
return `${props.level} ${props.padding} ${props.boxClass} ${props.noShadow ? '' : 'box-shadow'}`;
});

View File

@@ -10,11 +10,15 @@ const props = defineProps({
disabled: {
type: Boolean,
default: false
},
colorLower: {
type: Boolean,
default: false
}
});
const disableClass = computed(() => {
return props.disabled ? 'disabled' : '';
return props.disabled ? props.colorLower ? 'disabled color-lower' : 'disabled' : '';
});
</script>
@@ -24,13 +28,14 @@ button {
color: rgb(255, 255, 255);
border: none;
border-radius: 5px;
padding: 10px;
padding: 7px;
display: flex;
gap: 10px;
gap: 5px;
justify-content: center;
align-items: center;
text-decoration: none;
transition: all 0.3s ease;
}
button:hover {
@@ -58,4 +63,16 @@ button:active {
background-color: var(--tertiary) !important;
transform: none !important ;
}
.color-lower {
background-color: var(--secondary) !important;
}
.color-lower:hover {
background-color: var(--secondary) !important;
}
.color-lower:active {
background-color: var(--secondary) !important;
}
</style>

View File

@@ -0,0 +1,113 @@
<template>
<div ref="contextMenu" class="contextmenu">
<slot></slot>
</div>
</template>
<script setup>
import { onMounted, ref } from 'vue';
import useMouse from '@/utils/Mouse';
const contextMenu = ref(null);
const mouse = useMouse();
onMounted(() => {
contextMenu.value.addEventListener('rightclick', (e) => {
e.preventDefault(); // Prevent default context menu
});
contextMenu.value.addEventListener('click', () => {
contextMenu.value.style.display = 'none'; // Hide context menu on click
});
contextMenu.value.addEventListener('mouseleave', () => {
contextMenu.value.style.display = 'none'; // Hide context menu on mouse leave
});
});
function show() {
const menu = contextMenu.value
const { x, y } = mouse.value
// Affiche temporairement pour mesurer
menu.style.display = 'flex'
menu.style.left = '0px'
menu.style.top = '0px'
// Force le DOM à calculer les dimensions
const menuRect = menu.getBoundingClientRect()
const windowWidth = window.innerWidth
const windowHeight = window.innerHeight
let left = x
let top = y
// Ajuste si dépasse à droite
if (x + menuRect.width > windowWidth) {
left = windowWidth - menuRect.width
} else {
left = x - 5
}
// Ajuste si dépasse en bas
if (y + menuRect.height > windowHeight) {
top = windowHeight - menuRect.height
} else {
top = y - 5
}
// Applique la position corrigée
menu.style.left = `${Math.max(left, 0)}px`
menu.style.top = `${Math.max(top, 0)}px`
}
defineExpose({
show
})
</script>
<style scoped>
.contextmenu {
position: fixed;
display: none;
flex-direction: column;
gap: 0px;
z-index: 1000;
border-radius: 10px;
}
:deep(div) {
background-color: var(--primary);
opacity: 0.9;
width: 100%;
gap: 10px;;
display: flex;
align-items: center;
justify-content: start;
padding: 0 7px;
font-size: 12px;
transition: all 0.2s ease-in-out;
}
:deep(div:first-child) {
border-radius: 5px 5px 0px 0px;
}
:deep(div:last-child) {
border-radius: 0px 0px 5px 5px;
}
:deep(div:only-child) {
border-radius: 5px;
}
:deep(div:hover) {
background-color: var(--primary-hover);
color: var(--text-inverse);
cursor: pointer;
}
</style>

View File

@@ -2,11 +2,12 @@
<Splash>
<Box :width="width" :style="`gap: ${props.gap};`" class="splash-box" box-class="splash-box">
<slot></slot>
</Box>
</Splash>
</template>
<script setup>
import Splash from '@/components/Layout/Splash.vue';
import Splash from '@/components/UI/Splash.vue';
import Box from '@/components/UI/Box.vue';
const props = defineProps({

View File

@@ -1,5 +1,5 @@
<template>
<Icon :color="color" :class="!fixed ? 'icon' : 'fixed'" :icon="icon" />
<Icon :style="{'font-size': size ? size : ''}" :color="color" :class="!fixed ? 'icon' : 'fixed'" :icon="icon" />
</template>
<script setup>
const props = defineProps({
@@ -14,6 +14,10 @@ const props = defineProps({
fixed: {
type: Boolean,
default: false
},
size: {
type: String,
default: null
}
});

View File

@@ -1,5 +1,5 @@
<template>
<div>
<div :class="{ second: secondary }">
<Icon icon="fa-solid fa-circle-info"></Icon>
<div class="info-message">
<slot></slot>
@@ -8,6 +8,14 @@
</div>
</template>
<script setup>
defineProps({
secondary: {
type: Boolean,
default: false
}
})
</script>
<style scoped>
@keyframes fadeIn {
@@ -19,7 +27,6 @@
display: flex;
align-items: center;
gap: 10px;
color: var(--text);
animation: fadeIn 0.5s ease-in-out;
}
@@ -27,4 +34,9 @@
display: flex;
text-align: justify;
}
.second {
color: var(--text-secondary) !important;
font-size: 12px;
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<div>
<LogoDark class="img" v-if="globalStore.theme == 'light'"/>
<LogoLight class="img" v-else/>
</div>
</template>
<script setup>
import LogoDark from '@/assets/LogoDark.vue';
import LogoLight from '@/assets/LogoLight.vue';
import { useGlobalStore } from '@/stores/globalStore';
const globalStore = useGlobalStore();
</script>
<style scoped>
.img {
width: 100px;
height: 100px;
}
div {
display: flex;
align-items: center;
gap: 10px;
}
</style>

146
src/components/UI/Modal.vue Normal file
View File

@@ -0,0 +1,146 @@
<template>
<Teleport to="body">
<div ref="modal" class="modal-overlay">
<div ref="modalContent" class="modal">
<div class="modal-header">
<div class="modal-title">
<Icon font-size="1.5em" v-if="icon" :icon="icon" />
<p>{{ title }}</p>
</div>
<IconAction @click="close()" icon="fa-solid fa-xmark" />
</div>
<div class="modal-content">
<slot></slot>
</div>
</div>
</div>
</Teleport>
</template>
<script setup>
import IconAction from '../UI/IconAction.vue';
import { onMounted, onUnmounted, ref } from 'vue';
import Events from '@/utils/Events';
const modal = ref(null);
const modalContent = ref(null);
// Close the modal when clicking outside of it
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
});
// Only clicking modal-overlay but not modal-content will close the modal
function handleClickOutside(event) {
if (event.target === modal.value && modalContent.value && !modalContent.value.contains(event.target)) {
close();
}
}
const props = defineProps({
title: {
type: String,
default: 'Modal Title'
},
icon: {
type: String,
default: null
}
})
function close() {
if (modal.value) {
modal.value.style.display = 'none';
}
Events.emit('modal:close');
}
function open() {
if (modal.value) {
modal.value.style.display = 'flex';
}
}
defineExpose({
open,
close
});
</script>
<style scoped>
.modal-content {
padding: 0 20px 20px 20px;
color: var(--text-primary);
display: flex;
flex-direction: column;
gap: 10px;
}
.modal-title {
display: flex;
align-items: center;
gap: 10px;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--border);
background-color: var(--primary);
border-radius: 10px 10px 0px 0px ;
padding: 20px;
}
.modal {
background-color: var(--secondary);
border-radius: 10px;
display: flex;
flex-direction: column;
gap: 15px;
max-width: 600px !important;
width: 100%;
max-width: 90%;
margin-right: 15px;
margin-left: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
animation: appear 0.2s ease-in-out;
animation-fill-mode: forwards;
}
.modal p {
margin: 0px;
font-weight: 600;
font-size: 1.2em;
font-family: 'Gunship', sans-serif;
font-weight: 500;
}
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(5px);
display: none;
align-items: center;
justify-content: center;
z-index: 1500;
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<div class="settings-container">
<Icon style="width: 20px;" class="settings-icon" :icon="icon"/>
<div class="settings-item">
<p class="settings-title">{{ title }}</p>
<slot></slot>
</div>
</div>
</template>
<script setup>
defineProps({
title: {
type: String,
default: 'Settings item'
},
icon: {
type: String,
default: 'fa-solid fa-cog'
}
})
</script>
<style scoped>
.settings-title {
font-size: 1.1em;
color: var(--text-primary);
font-weight: 600;
margin: 0;
}
.settings-icon {
margin-top: 1px;
}
.settings-container {
display: flex;
gap: 7px;
}
.settings-item {
width: 100%;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<Tag color="var(--admin-color)" v-if="user.identity.isAdmin">Administrateur</Tag>
<Tag color="var(--owner-color)" v-else-if="user.identity.isOwner">Propriétaire</Tag>
<Tag color="var(--mod-color)" v-else-if="user.identity.isMod">Modérateur</Tag>
<Tag color="var(--text-secondary)" v-else>Membre</Tag>
</template>
<script setup>
import Tag from '@/components/UI/Tag.vue';
const props = defineProps({
user: {
type: Object,
required: true,
default: {
identity: {
isAdmin: false,
isMod: false,
isOwner: false
}
}
}
});
</script>

View File

@@ -0,0 +1,163 @@
<script setup lang="ts">
import IconAction from './IconAction.vue';
import { ref, onMounted, watch, onUnmounted } from 'vue';
import { useSlots } from 'vue';
const slots = useSlots();
const boxWidth = ref(0);
const box = ref(null);
const allSlots = slots.default ? slots.default() : [];
var firstSlot = allSlots[0] || null;
var otherSlots = allSlots.slice(1);
const showMenu = ref(false);
function selectOption(index) {
// Make it like a select
if (otherSlots[index]) {
const oldFirstSlot = firstSlot;
firstSlot = otherSlots[index];
otherSlots.splice(index, 1);
otherSlots.push(oldFirstSlot);
showMenu.value = false; // Hide the menu after selection
}
updateModelValue(firstSlot ? firstSlot.props.value : '');
}
const props = defineProps({
modelValue: String
});
const emit = defineEmits(['update:modelValue']);
function updateModelValue(value) {
emit('update:modelValue', value);
}
onMounted(() => {
window.addEventListener('rightclick', (e) => {
e.preventDefault(); // Prevent default context menu
});
window.addEventListener('click', (e) => {
hideMenu(e);
});
window.addEventListener('resize', () => {
showMenu.value = false;
});
updateModelValue(firstSlot ? firstSlot.props.value : '');
});
onUnmounted(() => {
window.removeEventListener('rightclick', (e) => {
e.preventDefault(); // Prevent default context menu
});
window.removeEventListener('click', (e) => {
hideMenu(e);
});
window.removeEventListener('resize', () => {
showMenu.value = false;
});
});
function hideMenu(e) {
// Check if the click is outside the context menu
if (box.value && !box.value.contains(e.target)) {
showMenu.value = false; // Hide context menu on click outside
}
}
defineExpose({
firstSlot: () => {
return firstSlot;
}
})
</script>
<template>
<section>
<div ref="box" :class="showMenu ? `showed firstbox` : 'firstbox'" @click="showMenu = !showMenu">
<template v-if="firstSlot">
<component :is="firstSlot" />
</template>
<IconAction
:icon="showMenu ? 'fa-solid fa-angle-up' : 'fa-solid fa-angle-down'"
/>
</div>
<template v-if="otherSlots.length && showMenu">
<div class="container">
<div v-for="(slot, index) in otherSlots" :style="`width: ${box.offsetWidth - 10}px;`" class="option" @click="selectOption(index)" >
<component :is="slot" :key="index" />
</div>
</div>
</template>
</section>
</template>
<style scoped>
.firstbox {
background-color: var(--tertiary);
color: var(--text);
border: none;
border-radius: 5px;
padding: 5px;
font-size: 14px;
display: flex;
justify-content: space-between;
}
.container {
color: var(--text);
border: none;
border-radius: 0px 0px 5px 5px;
font-size: 14px;
display: flex;
flex-direction: column;
position: fixed;
justify-content: space-between;
user-select: none;
overflow-y: auto;
max-height: 16vh;
}
.container .option:last-child {
border-radius: 0px 0px 5px 5px !important;
}
.container .option:only-child {
border-radius: 0px 0px 5px 5px !important;
}
.container .option:first-child {
border-top: 1px solid var(--secondary) !important;
}
.option {
width: 100%;
padding: 5px;
background-color: var(--tertiary);
cursor: pointer;
}
.option:hover {
background-color: var(--primary-hover);
color: var(--text-inverse);
}
.showed {
/* margin-bottom: 0 !important;
padding-bottom: 0; */
border-radius: 5px 5px 0 0 !important;
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<div>
<Icon icon="fa-solid fa-check-circle"></Icon>
<div class="success-message">
<slot></slot>
</div>
</div>
</template>
<style scoped>
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
div {
display: flex;
align-items: center;
gap: 10px;
color: var(--text-success);
animation: fadeIn 0.5s ease-in-out;
}
.success-message {
display: flex;
text-align: justify;
}
</style>

33
src/components/UI/Tag.vue Normal file
View File

@@ -0,0 +1,33 @@
<template>
<div class="tag" :style="{ borderColor: props.color }">
<Icon :color="props.color" icon="fa-circle fa-solid"/>
<p><slot></slot></p>
</div>
</template>
<script setup>
const props = defineProps({
color: {
type: String,
default: ''
}
});
</script>
<style scoped>
.tag {
border: 1px solid #ccc;
border-radius: 20px;
padding: 5px;
margin: 0;
display: flex;
align-items: center;
gap: 5px;
font-size: 0.7em;
}
p {
margin: 0;
display: flex;
align-items: center;
}
</style>

View File

@@ -1,9 +1,12 @@
<template>
<div class="user-card">
<Avatar :avatar-url="user?.identity?.avatar" :user-id="user?.identity?.id" />
<Avatar :avatar-url="user?.identity?.avatar" :user-id="user?.identity?.id"
:isMod="user?.identity?.isMod"
:isOwner="user?.identity?.isOwner"
:isAdmin="user?.identity?.isAdmin"/>
<div class="user-info">
<p class="global">{{ user.identity?.global_name || 'Nom d\'affichage inconnu' }}</p>
<p class="username">{{ user.identity?.username || 'Identifiant inconnu' }}</p>
<p class="global">{{ user?.identity?.global_name || 'Nom d\'affichage inconnu' }}</p>
<p class="username">{{ user?.identity?.username || 'Identifiant inconnu' }}</p>
</div>
</div>
</template>

View File

@@ -0,0 +1,73 @@
<template>
<div class="container" v-for="(member, index) in server.members" :key="index">
<User :user="{identity: member}" />
<IconAction :title="`Action sur ${member.username}`" @click="setTargetUser(member)" v-if="(userStore.userInfo.labels.includes('ADMIN') || (userStore.userInfo.labels.includes('MOD_' + server.id) && !member.isMod) || server.owner == userStore.userInfo.identity.id) && (member.id != server.owner && !member.isAdmin) "
icon="fa-solid fa-ellipsis-vertical"
/>
</div>
<ContextMenu ref="menuRef" v-if="userStore.userInfo.labels.includes('ADMIN') || userStore.userInfo.labels.includes('MOD_' + server.id) || server.owner == userStore.userInfo.identity.id">
<div v-if="!targetUser?.isMod" @click="banUser()">
<Icon icon="fa-solid fa-hammer"/>
<p>Bannir</p>
</div>
<div @click="toogleMod()" v-if="userStore.userInfo.labels.includes('ADMIN') || userStore.userInfo.identity.id == server.owner">
<Icon icon="fa-solid fa-user-shield"/>
<p v-if="!targetUser?.isMod">Nommer modérateur</p>
<p v-else>Retirer les droits de modérateur</p>
</div>
</ContextMenu>
</template>
<script setup>
import User from '@/components/UI/User.vue';
import { ref } from 'vue';
import IconAction from '@/components/UI/IconAction.vue';
import ContextMenu from '@/components/UI/ContextMenu.vue';
import { useUserStore } from '@/stores/userStore';
import { IORequest } from '@/utils/IORequest';
import Events from '@/utils/Events';
const userStore = useUserStore();
const targetUser = ref(null);
const menuRef = ref(null);
function setTargetUser(user) {
targetUser.value = user;
if (menuRef.value && targetUser.value) {
menuRef.value.show();
}
}
const props = defineProps({
server: {
type: Object,
required: true
}
});
function toogleMod() {
if (!targetUser.value) return;
IORequest("/OWNER/USERS/SWITCH_MOD", () => {
console.log("Mod switched for user", targetUser.value.id);
}, targetUser.value.id)
}
function banUser() {
// : /MOD/USERS/BAN
if (!targetUser.value) return;
IORequest("/MOD/USERS/BAN", () => {
console.log("User banned:", targetUser.value.id);
}, targetUser.value.id);
//TODO: CHECK THE userListConnected which have sameUser
}
</script>
<style scoped>
.container {
display: flex;
align-items: center;
justify-content: space-between;
}
</style>

View File

@@ -0,0 +1,42 @@
<template>
<Modal ref="modal" icon="fa-solid fa-screwdriver-wrench" :title="`Gestion : ${server?.name}`">
<ModalTree title="Utilisateurs" icon="fa-solid fa-users">
<GuildUsers :server="server"/>
</ModalTree>
<ModalTree title="Statistiques" icon="fa-solid fa-chart-simple">
<GuildStats :server="server"/>
</ModalTree>
</Modal>
</template>
<script setup>
import Modal from '@/components/UI/Modal.vue';
import ModalTree from '@/components/UI/ModalTree.vue';
import { ref } from 'vue';
import GuildUsers from './Settings/GuildUsers.vue';
import GuildStats from './Settings/GuildStats.vue';
import Events from '@/utils/Events';
const modal = ref(null);
const props = defineProps({
server: {
type: Object,
required: true
}
});
defineExpose({
open() {
if (modal.value) {
modal.value.open();
Events.emit("GUILD_JOINED")
}
},
close() {
if (modal.value) {
modal.value.close();
}
}
});
</script>

View File

@@ -0,0 +1,128 @@
<template>
<div class="metrics" v-if="metrics">
<div class="metric" v-for="metric in metrics" :key="metric.name">
<div class="metric-header">
<Icon class="metric-icon" :icon="getIcons(metric.name)" />
<div class="metric-info">
<p class="metric-name">{{ metric.description.replace(server.id, "").replace(server.name, "").replace(":", "") }}</p>
<p class="metric-id">{{ metric.name }}</p>
</div>
</div>
<p class="metric-value" v-if="metric.name.includes('Seconds')">{{ getReadableDuration(metric.value) }}</p>
<p v-else class="metric-value">{{ metric.value }}</p>
</div>
</div>
<div v-else>
<p class="second">Aucune statistique enregistrée !</p>
</div>
</template>
<script setup>
import { IORequest } from '@/utils/IORequest';
import Events from '@/utils/Events';
import { ref } from 'vue';
import { useUserStore } from '@/stores/userStore';
import { getReadableDuration } from '@/utils/TimeConverter';
function getIcons(name) {
if(name.includes("Commands")) return "fa-solid fa-terminal";
if(name.includes("Music")) return "fa-solid fa-music";
if(name.includes("Seconds")) return "fa-solid fa-clock";
return "fa-solid fa-chart-bar";
}
const userStore = useUserStore();
const metrics = ref(null);
const props = defineProps({
server: {
type: Object,
required: true
}
});
Events.on("GUILD_JOINED", () => {
if(!(userStore.userInfo.identity.isAdmin || userStore.userInfo.identity.isMod || userStore.userInfo.identity.id == props.server.owner)) return;
IORequest("/MOD/STATS", (data) => {
metrics.value = data;
});
});
</script>
<style scoped>
.second {
color: var(--text-secondary);
font-size: 0.8em;
}
.metric {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
background-color: var(--tertiary);
border-radius: 10px;
}
.metrics {
display: flex;
flex-direction: column;
gap: 10px;
margin-top: 10px;
overflow-y: auto;
max-height: 30vh;
}
.metric-info {
display: flex;
flex-direction: column;
}
.metric-info p {
margin: 0;
}
@media screen and (max-width: 768px) {
.metric {
flex-direction: column;
}
.metric-name {
word-break: break-all;
}
}
.metric-name {
font-weight: bold;
color: var(--text-primary);
}
.metric-id {
color: var(--text-secondary);
font-size: 0.7em;
word-break: break-all;;
}
.metric-header {
display: flex;
align-items: center;
gap: 10px;
}
.metric-value {
background-color: var(--secondary);
padding: 5px;
border-radius: 5px;
}
.metric-icon {
background-color: var(--secondary);
padding: 20px;
border-radius: 100%;
width: 16px;
height: 16px;
}
</style>

View File

@@ -0,0 +1,139 @@
<template>
<div class="users">
<div class="user" v-for="member in users" :key="member.identity.id">
<div class="group">
<User :user="member" />
<div class="info">
<Role :user="member" />
<Tag v-if="member.isBanned" color="var(--text-error)">Banni</Tag>
</div>
</div>
<div>
<div v-if="member.identity.id == userStore.userInfo.identity.id">
<p class="you">Vous</p>
</div>
<div v-else-if="member.identity.isAdmin">
<p class="you">Administrateur</p>
</div>
<div v-else-if="member.identity.isOwner">
<p class="you">Propriétaire</p>
</div>
<div class="actions" v-else>
<IconAction title="Bannir" v-if="!member.identity.isMod" :color="member.isBanned ? 'var(--text)' : 'var(--text-tertiary)'" @click="banUser(member)" icon="fa-solid fa-hammer"/>
<IconAction title="Nommer / Retirer modérateur" v-if="userStore.userInfo.identity.id == server.owner && !member.isBanned" :color="member.identity.isMod ? 'var(--text)' : 'var(--text-tertiary)'" @click="toggleMod(member)" icon="fa-solid fa-user-shield"/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import Events from '@/utils/Events';
import { IORequest } from '@/utils/IORequest';
import { defineProps } from 'vue';
import { ref } from 'vue';
import User from '@/components/UI/User.vue';
import Role from '@/components/UI/Role.vue';
import IconAction from '@/components/UI/IconAction.vue';
import Tag from '@/components/UI/Tag.vue';
import { useUserStore } from '@/stores/userStore';
const users = ref(null)
const userStore = useUserStore();
const props = defineProps({
server: {
type: Object,
required: true
}
});
Events.on("GUILD_JOINED", () => {
if(!(userStore.userInfo.identity.isAdmin || userStore.userInfo.identity.isMod || userStore.userInfo.identity.id == props.server.owner)) return;
IORequest("/MOD/USERS/LIST", (data) => {
users.value = data;
});
});
function toggleMod(member) {
if(member.identity.id == userStore.userInfo.identity.id) return;
IORequest("/OWNER/USERS/SWITCH_MOD", (response) => {
console.log(response)
}, member.identity.id);
}
function banUser(member) {
if(member.identity.id == userStore.userInfo.identity.id) return;
IORequest("/MOD/USERS/BAN", (response) => {
console.log(response)
}, member.identity.id);
}
</script>
<style scoped>
@media screen and (max-width: 768px) {
.group {
flex-direction: column;
align-items: flex-start !important;
gap: 20px;
}
.user{
padding: 5px 10px 5px 5px !important;
}
}
.user{
background-color: var(--tertiary);
padding: 5px 10px 2px 5px;
border-radius: 10px;
}
.group {
display: flex;
align-items: center;
gap: 10px;
}
.users {
margin: 10px 0;
gap: 10px;
display: flex;
flex-direction: column;
overflow-y: auto;
max-height: 25vh;
}
.user {
display: flex;
align-items: center;
justify-content: space-between;
}
.info {
display: flex;
align-items: center;
gap: 10px;
}
.actions {
display: flex;
align-items: center;
gap: 10px;
}
.you {
font-size: 0.8em;
color: var(--text-secondary);
}
</style>

View File

@@ -1,21 +1,28 @@
<template>
<Box no-shadow level="second" padding="closed">
<div class="container-list">
<ServerItem :server="server"/>
<Server :server="server"/>
<ServerOnlinePicture class="sop" :key="server.id" v-if="server.members.length > 0" :members="server.members"/>
<Button class="btn" @click="access()">Accéder</Button>
<section>
<Error v-if="userStore.userInfo.labels.includes('BAN_' + server.id)">Banni</Error>
<Button color-lower :disabled="userStore.userInfo.labels.includes('BAN_' + server.id)" class="btn" @click="access()"><Icon icon="fa-solid fa-right-to-bracket"/>Accéder</Button>
</section>
</div>
</Box>
</template>
<script setup>
import Box from '@/components/UI/Box.vue';
import Button from '../UI/Button.vue';
import Button from '@/components/UI/Button.vue';
import { useGlobalStore } from '@/stores/globalStore';
import { useRouter } from 'vue-router';
import ServerOnlinePicture from '@/components/Widget/ServerOnlinePicture.vue';
import ServerItem from '@/components/Widget/ServerItem.vue';
import ServerOnlinePicture from '@/components/Widget/Server/ServerOnlinePicture.vue';
import Server from '@/components/UI/Server.vue';
import { useUserStore } from '@/stores/userStore';
import Error from '@/components/UI/Error.vue';
const router = useRouter();
const userStore = useUserStore();
const globalStore = useGlobalStore();
const props = defineProps({
@@ -61,6 +68,12 @@ function access() {
display: none;
}
section {
display: flex;
flex-direction: column;
width: 100%;
gap: 10px;
}
}
.container-list {
@@ -70,6 +83,10 @@ function access() {
position: relative;
}
.btn svg {
font-size: 1.1em;
}
.sop {
position: absolute;
top: 50%;
@@ -77,6 +94,11 @@ function access() {
transform: translate(-50%, -50%);
}
section {
display: flex;
align-items: center;
gap: 10px;
}
</style>

View File

@@ -15,8 +15,9 @@
</template>
<script setup>
import Avatar from '../UI/Avatar.vue';
import Circle from '../UI/Circle.vue';
import Avatar from '@/components/UI/Avatar.vue';
import Circle from '@/components/UI/Circle.vue';
import { ref, watch } from 'vue';
const props = defineProps({
members: {
@@ -26,8 +27,13 @@ const props = defineProps({
})
// ne garder que les quatres premiers membres
const length = props.members.length;
const members = props.members.slice(0, 4);
const length = ref(props.members.length);
const members = ref(props.members.slice(0, 4));
watch(() => props.members, (newMembers) => {
members.value = newMembers.slice(0, 4);
length.value = newMembers.length;
}, { immediate: true });
</script>
<style scoped>

View File

@@ -0,0 +1,100 @@
<script setup lang="ts">
import Button from '@/components/UI/Button.vue';
import Selector from '@/components/UI/Selector.vue';
import { onMounted, ref, watch } from 'vue';
import { IORequest } from '@/utils/IORequest';
import Success from '@/components/UI/Success.vue';
import Events from '@/utils/Events.js';
const reportDesc = ref(null)
const category = ref(null); // Default category
const sended = ref(false);
const selector = ref(null);
const showForm = ref(false);
function sendReport() {
if (reportDesc.value && category.value) {
IORequest('/REPORT', (answer) => {
if(answer) {
sended.value = true;
reportDesc.value = null;
category.value = null; // Reset category after sending
showForm.value = false; // Hide the form after sending
}
}, {
level: category.value,
desc: selector.value.firstSlot().children + " - " + reportDesc.value
})
}
}
watch(reportDesc, (newValue) => {
if (newValue) {
sended.value = false; // Reset sent status when description changes
}
});
Events.on('modal:close', () => {
showForm.value = false; // Close the form when modal is closed
reportDesc.value = null; // Reset the description
category.value = null; // Reset the category
sended.value = false; // Reset sent status
});
watch(showForm, (newValue) => {
if (newValue) {
reportDesc.value = null; // Reset description when form is shown
category.value = null; // Reset category when form is shown
sended.value = false; // Reset sent status when form is shown
}
});
</script>
<template>
<p>Si vous rencontrez un problème, vous pouvez le signaler via ce formulaire. Vous pouvez être contacté en cas de besoin par Raphix pour plus d'informations.</p>
<Button v-if="!showForm" @click="showForm = !showForm" class="margin"><Icon icon="fa-solid fa-paper-plane" /> Faire un rapport de bug</Button>
<Success class="margin" v-if="sended">Votre rapport a été envoyé avec succès</Success>
<div v-if="showForm" class="report-content">
<p>Catégorie</p>
<Selector ref="selector" v-model="category">
<option value="bug">Problème d'authentification (Déconnexion intempestive, Serveurs)</option>
<option value="bug">Problème avec l'interface (Affichage)</option>
<option value="bug">Problème avec le Bot Discord (Réponse vide, Erreur)</option>
<option value="bug">Problème avec le Player (Musique)</option>
<option value="bug">Problème de fonctionnalité (Non spécifié)</option>
<option value="suggestion">Suggestion</option>
</Selector>
<p>Description</p>
<textarea v-model="reportDesc" name="report-desc" class="report-desc"></textarea>
<Button @click="sendReport()" :disabled="!reportDesc" id="report-send-confirm" class="report-send">Envoyer</Button>
</div>
</template>
<style scoped>
p {
font-size: 0.8em;
color: var(--text-secondary);
margin: 10px 0;
}
.report-content {
width: 100%;
display: flex;
flex-direction: column;
gap: 10px;
animation: unfold 1.3s ease-in-out;
}
.report-content p {
margin: 0;
font-size: 0.9em;
color: var(--text);
}
.margin {
margin: 10px 0;
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="info">
<p class="name">Chopin - Fait avec <IconAction @click="modal.open()" size="10px" color="var(--text-secondary)" icon="fa-solid fa-heart"/> par Raphix</p>
<div class="version">
<p>Frontend: {{ versionFrontend }}</p>
<p>Backend: {{ versionBackend }}</p>
</div>
<Modal title="Merci" icon="fa-solid fa-heart" ref="modal">
<div class="thanks">
<Logo/>
</div>
<div class="text">
<p>Subsonics a été développé avec par Raphix et existe depuis le 9 avril 2023.</p>
<p> Merci énormément à tous le CLP en particulier : </p>
<ul>
<li>IcePlayer: Pour ses conseils, son "RAAAPHIX", sa confiance accordée et pour avoir créer le CLP</li>
<li>Gabouille : Pour ses conseils, sa bonne humeur, pour le magnifique logo et pour m'avoir aider à redesigner le site.</li>
<li>Roman: Pour ses conseils de musique mais aussi de vie, sa créativité et sa musique plus qu'excellente !</li>
<li>Mido : Pour son soutien plus que légendaire et d'être à mes côtés en toute circonstance ! (il a même l'application Subsonics !!!)</li>
<li>Immudelki : Pour ses conseils de vie, sa sagesse et d'avoir fait un petit jeu d'interface qui a bercé mon enfance</li>
<li>Alexmario : Pour sa compagnie et son talent indéniable et indétroné sur tout ce qui touche !</li>
<li>Ava : Pour ses conseils de vie, sa sagesse et d'avoir toujours été là pour moi !</li>
<li>DarkGuillaume : Pour sa créativité, et ... SHADOW !!!!!</li>
<li>Mako : Pour son humour, sa bonne humeur et d'être le saint patron des mouettes !</li>
<li>Et à tous les autres qui ont contribué à ce projet, que ce soit par des conseils, des idées ou simplement en étant !</li>
</ul>
<p>Un petit peu d'histoire sur Subsonics :</p>
<ul>
<li>Première version nommée "Bot", accompagné de son application "Manager", ayant marqué les prémices de Subsonics.</li>
<li>Deuxième version nommée "Web", avec une interface Web et l'ajout de beaucoup de fonctionnalités.</li>
<li>Troisième version nommée "Chopin", avec une refonte complète de l'interface, une architecture améliorée et une expérience utilisateur optimisée.</li>
</ul>
<p>Merci d'utiliser Subsonics ^^ !</p>
<p>Raphaël.P dit "Raphix"</p>
</div>
</Modal>
</div>
</template>
<script setup>
import IconAction from '@/components/UI/IconAction.vue';
import { version as versionFrontend } from '@/../package.json';
import { IORequest } from '@/utils/IORequest';
import { onMounted } from 'vue';
import { ref } from 'vue';
import Modal from '@/components/UI/Modal.vue';
import Logo from '@/components/UI/Logo.vue';
import Avatar from '@/components/UI/Avatar.vue';
const modal = ref(null);
const versionBackend = ref("X.X.X");
onMounted(() => {
IORequest("/VERSION", (data) => {
versionBackend.value = data;
});
})
</script>
<style scoped>
.text {
color: var(--text-secondary);
font-size: 12px;
margin: 5px 0;
}
.name {
font-size: 12px;
color: var(--text-secondary);
margin: 10px 0 0 0;
}
.info {
display: flex;
width: 100%;
justify-content: space-between;
align-self: center;
gap: 5px;
}
.version {
display: flex;
align-items: center;
gap: 10px;
margin: 5px 0 0 0 ;
}
.version p {
margin: 0;
font-size: 12px;
color: var(--text-secondary);
}
.thanks {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<p class="privacy">Toutes les données récupérées sont à des fins <strong>strictement</strong> nécessaires au bon fonctionnement de l'application.</p>
<Button @click="openModal()"><Icon icon="fa-solid fa-trash"/> Supprimer mon compte</Button>
<Modal icon="fa-solid fa-trash" ref="deleteAccountModal" title="Supprimer mon compte">
<p class="warning">
Êtes-vous sûr de vouloir supprimer votre compte ? <br/>
Cette action est irréversible et supprime vos playlists, votre historique, vos rôles ! <br/>
<span class="text-second">Cependant, en vertu de la politique de confidentialité, votre identifiant Discord sera conservé ainsi que les informations générées par Subsonics.</span>
</p>
<div @click="deleteAccount = !deleteAccount" class="options">
<input v-model="deleteAccount" type="checkbox" id="deleteAccount"/>
<p class="text-second">Je suis sur de vouloir supprimer mon compte et toutes les données associées.</p>
</div>
<Button @click="closeModal()">Annuler</Button>
<Button :disabled="!deleteAccount" @click="deleteAcc()">Confirmer la suppression</Button>
</Modal>
<div class="privacy-link">
<p class="secondtext"><router-link to="/terms">Conditions d'utilisation</router-link> </p>
<p class="secondtext"><router-link to="/privacy">Confidentialité</router-link></p>
</div>
</template>
<script setup>
import Button from '@/components/UI/Button.vue';
import Events from '@/utils/Events';
import { ref } from 'vue';
import Modal from '@/components/UI/Modal.vue';
import { IORequest } from '@/utils/IORequest';
import { useRouter } from 'vue-router';
import { useGlobalStore } from '@/stores/globalStore';
import { useUserStore } from '@/stores/userStore';
import { useLoginStore } from '@/stores/loginStore';
const deleteAccount = ref(false);
const deleteAccountModal = ref(null);
const loginStore = useLoginStore();
const router = useRouter();
Events.on('modal:close', () => {
deleteAccount.value = false; // Reset checkbox when modal is closed
});
function openModal() {
deleteAccountModal.value.open()
}
function closeModal() {
deleteAccountModal.value.close();
deleteAccount.value = false; // Reset checkbox when modal is closed
}
function deleteAcc() {
if (deleteAccount.value) {
IORequest('/USER/DELETE', () => {
loginStore.setToken(null); // Clear the token
router.push("/login?message=" + encodeURIComponent("Votre compte a été supprimé avec succès."));
});
}
}
</script>
<style scoped>
.privacy {
color: var(--text-secondary);
font-size: 14px;
}
.privacy-link {
display: flex;
margin-top: 10px;
gap: 10px;
font-size: 12px;
}
.secondtext {
color: var(--text-secondary);
font-size: 12px;
margin: 5px 0;
}
.text-second {
color: var(--text-secondary);
font-size: 12px;
margin-top: 10px;
}
.options {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.warning {
color: var(--text);
font-size: 14px;
margin-bottom: 10px;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="theme-selector">
<div @click="lightTheme.click()" class="theme-option">
<div>
<input ref="lightTheme" type="radio" name="theme" value="light" v-model="selectedTheme" />
<p>Clair</p>
</div>
</div>
<div @click="darkTheme.click()" class="theme-option">
<div>
<input ref="darkTheme" type="radio" name="theme" value="dark" v-model="selectedTheme" />
<p>Sombre</p>
</div>
</div>
<div @click="systemThemeInput.click()" class="theme-option">
<div>
<input ref="systemThemeInput" type="radio" name="theme" value="system" v-model="selectedTheme" />
<p>Système ({{ systemTheme }})</p>
</div>
</div>
</div>
</template>
<script setup>
import { useGlobalStore } from '@/stores/globalStore';
import { ref, watch } from 'vue';
const globalStore = useGlobalStore();
const selectedTheme = ref(localStorage.getItem('theme') || 'dark');
const systemTheme = ref(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'Sombre' : 'Clair');
const lightTheme = ref(null);
const darkTheme = ref(null);
const systemThemeInput = ref(null);
watch(() => selectedTheme.value, (newTheme) => {
globalStore.setTheme(newTheme);
});
</script>
<style scoped>
.theme-selector {
display: flex;
justify-content: space-between;
width: 100%;
gap: 10px;
}
.theme-selector div {
display: flex;
align-items: center;
gap: 5px;
}
.theme-selector span {
font-size: 1em;
color: var(--text-primary);
}
.theme-selector input[type="radio"]:checked + span {
opacity: 0.9;
}
.theme-option {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10px;;
gap: 20px !important;
transition: all 0.2s ease-in-out;
border-radius: 10px;
}
.theme-option:hover {
transition: backgroundColor 0s ease-in-out;
background-color: var(--primary);
cursor: pointer;
}
.theme-selector p {
margin: 0;
font-size: 0.9em;
color: var(--text-secondary);
text-align: center;
vertical-align: middle;
}
.theme-samples {
display: flex;
justify-content: space-around;
width: 100%;
gap: 10px;
margin-top: 20px;
}
input[type="radio"] {
margin: 0 !important;
margin-right: 5px;
}
</style>

View File

@@ -0,0 +1,47 @@
<template>
<Modal ref="modal" icon="fa-solid fa-gear" title="Paramètres">
<ModalTree title="Thème" icon="fa-solid fa-palette">
<ThemeSelector />
</ModalTree>
<ModalTree title="Support" icon="fa-solid fa-headset">
<BugReport/>
</ModalTree>
<ModalTree title="Mon compte & Confidentialité" icon="fa-solid fa-shield-halved">
<PrivacySettings />
</ModalTree>
<ModalTree title="Informations" icon="fa-solid fa-circle-info">
<InformationSettings/>
</ModalTree>
</Modal>
</template>
<script setup>
import Modal from '@/components/UI/Modal.vue';
import { ref, watch } from 'vue';
import ThemeSelector from '@/components/Widget/User/Settings/ThemeSelector.vue';
import ModalTree from '@/components/UI/ModalTree.vue';
import BugReport from './Settings/BugReport.vue';
import { useRouter } from 'vue-router';
import PrivacySettings from './Settings/PrivacySettings.vue';
import InformationSettings from './Settings/InformationSettings.vue';
const modal = ref(null);
const router = useRouter();
defineExpose({
open() {
if (modal.value) {
modal.value.open();
}
},
close() {
if (modal.value) {
modal.value.close();
}
}
});
</script>
<style scoped>
</style>

View File

@@ -19,4 +19,5 @@ export const socket = io(URL, {
token: localStorage.getItem("token") || null,
sessionId: localStorage.getItem("session") || null
},
});

View File

@@ -5,7 +5,11 @@ export const useGlobalStore = defineStore('global', () => {
const isLoading = ref(true);
const lastRoute = ref(null);
const lastGuild = ref(localStorage.getItem("lastGuild") || null);
const theme = ref(localStorage.getItem("theme") || "dark");
const theme = ref(localStorage.getItem("theme") || "system");
if(theme.value === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
theme.value = systemTheme;
}
document.documentElement.setAttribute("data-theme", theme.value);
function setLoading(loading) {
@@ -24,6 +28,17 @@ export const useGlobalStore = defineStore('global', () => {
localStorage.setItem("lastGuild", guild || null);
}
function setTheme(newTheme) {
console.log("Setting theme to", newTheme);
localStorage.setItem("theme", newTheme);
theme.value = newTheme;
if(theme.value === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
theme.value = systemTheme;
}
document.documentElement.setAttribute("data-theme", theme.value);
}
function toogleTheme() {
theme.value = theme.value === "dark" ? "light" : "dark";
localStorage.setItem("theme", theme.value);
@@ -39,6 +54,7 @@ export const useGlobalStore = defineStore('global', () => {
theme,
toogleTheme,
lastGuild,
setLastGuild
setLastGuild,
setTheme
};
})

View File

@@ -14,6 +14,7 @@ export default {
},
emit(eventName, data) {
if(Events.has(eventName)) {
console.log(`Event emitted: ${eventName}`, data)
Events.get(eventName).forEach(fn => fn(data))
}
},

View File

@@ -1,5 +1,5 @@
<script setup>
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
const defaultMessage = "On s'accorde et on prépare le concert !";
const connectMsg = "Erreur de connexion au serveur : xhr poll error"
@@ -16,11 +16,11 @@ const props = defineProps({
<template>
<DefaultSplash>
<h1 v-if="!interuptionMessage" class="separate"><Icon spin-pulse icon="fa-solid fa-spinner"/> Chargement de l'interface</h1>
<h1 v-else-if="interuptionMessage == connectMsg">Echec de connexion</h1>
<h1 v-else-if="interuptionMessage == connectMsg"><Icon icon="fa-solid fa-circle-exclamation"/> Echec de connexion</h1>
<h1 v-else><Icon icon="fa-solid fa-warning"/> Connexion interrompue</h1>
<div class="separate-col">
<p v-if="interuptionMessage" class="retry"><Icon spin-pulse icon="fa-solid fa-spinner"/> Tentative de reconnexion en cours</p>
<p v-if="interuptionMessage" class="second">Contactez Raphix (raphixscrap) en cas de problème prolongé.</p>
<p v-if="interuptionMessage" class="second">Si vous rencontrez des problèmes, contactez le support.</p>
</div>
<p v-if="interuptionMessage" class="error"><Icon icon="fa-solid fa-circle-xmark"/> {{ interuptionMessage }}</p>
<p v-else>{{ defaultMessage }}</p>

36
src/utils/Mouse.js Normal file
View File

@@ -0,0 +1,36 @@
import { ref, onMounted, onUnmounted } from 'vue';
export default function useMouse() {
const position = ref({
x: 0,
y: 0,
isDown: false,
});
function updateMousePosition(event) {
position.value.x = event.clientX;
position.value.y = event.clientY;
}
function setMouseDown() {
position.value.isDown = true;
}
function setMouseUp() {
position.value.isDown = false;
}
onMounted(() => {
window.addEventListener('mousemove', updateMousePosition);
window.addEventListener('mousedown', setMouseDown);
window.addEventListener('mouseup', setMouseUp);
});
onUnmounted(() => {
window.removeEventListener('mousemove', updateMousePosition);
window.removeEventListener('mousedown', setMouseDown);
window.removeEventListener('mouseup', setMouseUp);
});
return position;
};

View File

@@ -12,7 +12,7 @@
</DefaultSplash>
</template>
<script setup>
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
import { useLoginStore } from '@/stores/loginStore';
import { useRoute, useRouter } from 'vue-router';
import {socket} from '@/socket.js';

View File

@@ -0,0 +1,44 @@
export function getReadableDuration(duration) {
var max = ""
duration *= 1000;
const maxhours = Math.floor(duration / 3600000);
var maxmin = Math.trunc(duration / 60000) - (Math.floor(duration / 60000 / 60) * 60);
var maxsec = Math.floor(duration / 1000) - (Math.floor(duration / 1000 / 60) * 60);
if (maxsec < 10) {
maxsec = `0${maxsec}`;
}
if(maxhours != 0) {
if (maxmin < 10) {
maxmin = `0${maxmin}`;
}
max = maxhours + "h" + maxmin + "m" + maxsec
} else {
max = maxmin + "m" + maxsec + "s"
}
return max
}
export function getSecondsDuration(duration) {
// Duration is in format hh:mm:ss and can be just m:ss or mm:ss
var durationArray = duration.split(":");
var seconds = 0;
if(durationArray.length == 3) {
seconds = parseInt(durationArray[0]) * 3600 + parseInt(durationArray[1]) * 60 + parseInt(durationArray[2]);
} else if(durationArray.length == 2) {
seconds = parseInt(durationArray[0]) * 60 + parseInt(durationArray[1]);
} else {
seconds = parseInt(durationArray[0]);
}
return seconds;
}

View File

@@ -1,14 +1,16 @@
<template>
<SocketEnvironment>
<div class="container">
<HeaderGuild/>
<GuildHeader/>
<Box>
<Button @click="router.push('/servers')">Choisir un serveur</Button>
<Button @click="router.push('/terms')">Terms</Button>
<Button @click="router.push('/privacy')">Privacy</Button>
<Button @click="IORequest('/MOD/USERS/BAN', null, '582966873201704960')">Bannir Raphixscrap_</Button>
<Button @click="globalStore.toogleTheme()">Changer le thème</Button>
<p>{{ guildId }}</p>
</Box>
<Search/>
<Account/>
</div>
</SocketEnvironment>
@@ -23,9 +25,10 @@ import { useGlobalStore } from '@/stores/globalStore';
import { useUserStore } from '@/stores/userStore';
import { IOListener, IORequest } from '@/utils/IORequest';
import { onMounted, ref } from 'vue';
import Account from '@/components/Features/Account.vue';
import Account from '@/components/Layout/Account.vue';
import events from '@/utils/Events.js';
import HeaderGuild from '@/components/Features/HeaderGuild.vue';
import GuildHeader from '@/components/Layout/GuildHeader.vue';
import Search from '@/components/Layout/Search.vue';
const props = defineProps({
guildId: {
@@ -43,7 +46,6 @@ if (!guildId) {
const globalStore = useGlobalStore();
const userStore = useUserStore();
const router = useRouter();
const alreadyLoaded = ref(false);
watch(() => globalStore.isLoading, (value, oldValue) => {
if(!value && oldValue != value) {
@@ -52,7 +54,7 @@ watch(() => globalStore.isLoading, (value, oldValue) => {
})
onMounted(() => {
if(globalStore.lastGuild && !globalStore.isLoading) {
if(!globalStore.isLoading) {
loadInteface();
}
});
@@ -62,16 +64,9 @@ events.on("UPDATE", () => {
});
function loadInteface() {
if(alreadyLoaded.value) return;
console.log("Loading interface")
console.log("Guild ID:", guildId);
checkGuildAvailability();
IORequest("/USERS/LIST", (data) => {
console.log("User list received:", data);
})
alreadyLoaded.value = true;
}
function checkGuildAvailability() {
@@ -85,6 +80,7 @@ function checkGuildAvailability() {
IORequest("/GUILD/JOIN", (response) => {
if(response === true) {
console.log("Successfully joined guild:", guildId);
events.emit("GUILD_JOINED");
} else {
console.error("Failed to join guild:", response);
globalStore.setLastGuild(null);
@@ -102,11 +98,10 @@ function checkGuildAvailability() {
<style scoped>
.container {
padding: 20px;;
padding: 15px;;;
max-width: 400px;
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
}
</style>

View File

@@ -7,26 +7,27 @@
<Error v-if="callbackError">{{ callbackError }}</Error>
<Error v-if="error">{{ error }}</Error>
<Error v-if="errorServer">{{ errorServer }}</Error>
<Info v-if="message">{{ message }}</Info>
<Success v-if="message">{{ message }}</Success>
</div>
<div class="footer">
<p class="secondtext"><router-link to="/terms">Conditions d'utilisation</router-link> </p>
<p class="secondtext"><router-link to="/privacy">Privacy</router-link></p>
<p class="secondtext"><router-link to="/privacy">Confidentialité</router-link></p>
</div>
<p class="second">Session : {{ sessionId }}</p>
</DefaultSplash>
</template>
<script setup>
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
import Button from '@/components/UI/Button.vue';
import Error from '@/components/UI/Error.vue';
import Info from '@/components/UI/Info.vue';
import { useRoute, useRouter } from 'vue-router';
import { useLoginStore } from '@/stores/loginStore';
import { computed, ref, watch, onMounted } from 'vue';
import { computed, ref, watch } from 'vue';
import { socket } from '@/socket.js';
import { useGlobalStore } from '@/stores/globalStore';
import Success from '@/components/UI/Success.vue';
const loginStore = useLoginStore();

View File

@@ -16,7 +16,7 @@
</template>
<script setup>
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
import Button from '@/components/UI/Button.vue'
import Box from '@/components/UI/Box.vue';
import { socket } from '@/socket';

View File

@@ -1,6 +1,6 @@
<script setup>
import ReturnHomeButton from '@/components/Widget/ReturnHomeButton.vue';
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
import Box from '@/components/UI/Box.vue';
import Button from '@/components/UI/Button.vue';
</script>
@@ -8,7 +8,7 @@ import Button from '@/components/UI/Button.vue';
<template>
<DefaultSplash>
<ReturnHomeButton/>
<h1>Privacy</h1>
<h1>Confidentialité</h1>
<Box no-shadow level="second" padding="closed">
<div class="terms-content">
<p><strong>Date d'entrée en vigueur :</strong> 22 juillet 2025</p>
@@ -116,4 +116,9 @@ import Button from '@/components/UI/Button.vue';
color: var(--text-secondary);
text-align: justify;
}
h1 {
font-size: 1.6em;
margin-bottom: 10px;
color: var(--text);
}
</style>

View File

@@ -1,5 +1,5 @@
<script setup>
import Splash from '@/components/Layout/Splash.vue';
import Splash from '@/components/UI/Splash.vue';
import Box from '@/components/UI/Box.vue';
import SocketEnvironment from "../utils/SocketEnvironment.vue"
import { useRouter } from 'vue-router';
@@ -8,9 +8,9 @@ import { useUserStore } from '@/stores/userStore';
import Button from '@/components/UI/Button.vue';
import ReturnHomeButton from '@/components/Widget/ReturnHomeButton.vue';
import { ref } from 'vue';
import ServerListItem from '@/components/Widget/ServerListItem.vue';
import ServerListItem from '@/components/Widget/Server/ServerListItem.vue';
import Info from '@/components/UI/Info.vue';
import Account from '@/components/Features/Account.vue';
import Account from '@/components/Layout/Account.vue';
const globalStore = useGlobalStore();
const userStore = useUserStore();

View File

@@ -1,6 +1,6 @@
<script setup>
import ReturnHomeButton from '@/components/Widget/ReturnHomeButton.vue';
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
import Box from '@/components/UI/Box.vue';
import Button from '@/components/UI/Button.vue';
</script>
@@ -11,12 +11,16 @@ import Button from '@/components/UI/Button.vue';
<h1>Conditions d'utilisation</h1>
<Box no-shadow level="second" padding="closed">
<div class="terms-content">
<p>En utilisant ce site, vous consentez à l'utilisation de vos données fournis par Discord <Icon icon="fa-solid fa-copyright"/> afin d'assurer le bon fonctionnement de la plateforme. Vous avez la possibilité de demander la suppression de vos données à tout moment en contactant Raphix (raphixscrap). Tout acte intentionnel de dégradation du bot entraînera un bannissement du site.</p>
<p>Ce bot est destiné à un usage privé exclusivement et n'est pas conçu pour être utilisé par le grand public.</p>
<p>Toutes les musiques disponibles sur ce site sont hébergées sur les plateformes suivantes : Youtube, Soundcloud, Spotify.</p>
<p>En utilisant Subsonics, vous bénéficiez du droit d'accès au service et de l'écoute du contenu. Cependant, veuillez noter que Raphix n'est pas tenu de fournir la provenance ni l'autorisation d'exploitation des musiques par les ayants droits. Il vous incombe donc d'obtenir les autorisations nécessaires des ayants droits pour écouter le contenu.</p>
<p>En utilisant Subsonics, vous acceptez de ne pas utiliser le bot pour diffuser des contenus illégaux, violents, haineux, discriminatoires, ou à caractère sexuel.</p>
<p>En utilisant Subsonics, vous acceptez de ne pas utiliser le bot pour diffuser des contenus à caractère politique, religieux, incitant à la haine, publicitaire ou pornographique.</p>
<p>En accédant à cette plateforme et en vous connectant via Discord <Icon icon="fa-solid fa-copyright"/>, vous acceptez automatiquement les présentes conditions dutilisation ainsi que la <strong>politique de confidentialité</strong>.
Les données fournies par Discord sont utilisées uniquement pour assurer le bon fonctionnement du service. Vous pouvez à tout moment demander la suppression de vos données en contactant <strong>Raphix</strong> (<code>raphixscrap</code>) ou bien en allant dans les paramètres de votre compte en utilisant le petit engrenage à coté de votre photo de profil et en cliquant sur le boutton "Supprimer mon compte".</p>
<p>En vous connectant, vous acceptez que votre identifiant Discord (ID et @) soit gardé à jamais pour des raisons de sécurité (bannissement). Cependant toutes les playlists crées, votre historique de musique, les informations seront supprimés.</p>
<p>Tout acte de sabotage, de tentative de perturbation ou de dégradation intentionnelle du bot entraînera un bannissement immédiat et définitif du site.</p> <p><strong>Subsonics est un outil privé</strong>, non destiné à une utilisation publique ou commerciale.</p>
<p>Les musiques disponibles via le service sont diffusées à partir des plateformes suivantes : <strong>YouTube</strong>, <strong>SoundCloud</strong> et <strong>Spotify</strong>.
Subsonics ne stocke aucun fichier audio. Vous bénéficiez uniquement dun droit daccès au service et à lécoute du contenu, mais <strong>Raphix</strong> ne fournit ni la provenance, ni les autorisations légales liées à l'exploitation des œuvres. Il vous revient dobtenir les autorisations nécessaires auprès des ayants droit si nécessaire.</p>
<p>En utilisant Subsonics, vous vous engagez à ne pas diffuser de contenu :</p>
<ul> <li>illégal, violent, haineux, discriminatoire ou à caractère sexuel explicite,</li>
<li>à caractère politique, religieux, publicitaire, incitant à la haine ou pornographique.</li> </ul>
<p>Toute infraction à ces règles pourra entraîner des sanctions immédiates, y compris lexclusion définitive du service.</p>
</div>
</Box>
@@ -40,4 +44,10 @@ import Button from '@/components/UI/Button.vue';
color: var(--text-secondary);
text-align: justify;
}
h1 {
font-size: 1.6em;
margin-bottom: 10px;
color: var(--text);
}
</style>