Version 1.0.0 - Initialisation du depot

This commit is contained in:
2025-07-25 18:00:14 +02:00
commit e3d7a911f4
51 changed files with 4335 additions and 0 deletions

14
src/App.vue Normal file
View File

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

84
src/assets/Global.scss Normal file
View File

@@ -0,0 +1,84 @@
@font-face {
font-family: 'Gunship';
src: url("/gunship.ttf");
}
[data-theme='dark'] {
--main: #CD034F;
--main-hover: #A0023F;
--main-active: #7A002F;
--primary: #111210;
--secondary: #2A2B28;
--tertiary: #404040;
--text: #FFFFFF;
--text-secondary: #C5c3c3;
--text-error: #ff2b2b;
color: var(--text);
}
[data-theme='light'] {
--main: #CD034F;
--main-hover: #A0023F;
--main-active: #7A002F;
--primary: #FFFFFF;
--secondary: #EAEAEA;
--tertiary: #C5c3c3;
--text: #111210;
--text-secondary: #404040;
--text-error: #CD034F;
color: var(--text);
}
html, body {
margin: 0 !important;
font-family: 'Inter', sans-serif;
min-height: 100vh;
min-width: 100vw;
margin: 0;
padding: 0;
height: 100%;
overflow-x: hidden;
background-color: var(--primary);
}
.no-decoration {
text-decoration: none !important;
}
.underline {
text-decoration: underline !important;
}
h1 {
font-family: 'Gunship';
font-size: 30px;
color: var(--text-color);
}
a {
color: var(--text-secondary)
}
/*Scrollbar */
::-webkit-scrollbar {
width: 5px;
margin-right: 20px;
}
::-webkit-scrollbar-track {
border-radius: 12px;
}
::-webkit-scrollbar-thumb {
background: #ffffff56;
border-radius: 5px;
transition: 0.2s;
}
::-webkit-scrollbar-thumb:hover {
background: #ffffffa8;
}

View File

@@ -0,0 +1,30 @@
<template>
<IconAction color="white" class="return-home" v-if="globalStore.lastRoute" icon="fa-solid fa-xmark" @click="router.push(globalStore.lastRoute)"/>
</template>
<script setup>
import IconAction from '@/components/UI/IconAction.vue';
import { useRouter } from 'vue-router';
import { useGlobalStore } from '@/stores/globalStore';
const globalStore = useGlobalStore();
const router = useRouter();
</script>
<style scoped>
.return-home {
position: absolute;
top: 20px;
right: 20px;
color: #FFFFFF;
cursor: pointer;
}
.return-home:hover {
color: var(--text-hover);
}
.icon {
font-size: 1.3em;
transition: filter 0.3s ease;
}
.icon:hover {
cursor: pointer;
filter: brightness(0.8);
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<Box no-shadow level="second" padding="closed">
<div class="container-list">
<div class="container-avatar">
<ServerAvatar :server-id="server.id" :server-icon="server.icon"/>
<div class="info">
<p class="name">{{ server.name }}</p>
<p class="data"><Icon style="font-size: 10px;" :color="server.connected ? '#0BFF89' : '#FF0A0A'" icon="fa-solid fa-circle"/> {{ server.connected ? 'En ligne' : 'Hors ligne' }} - {{ server.serverMember }} membres</p>
</div>
</div>
<Button @click="access()">Accéder</Button>
</div>
</Box>
</template>
<script setup>
import Box from '@/components/UI/Box.vue';
import ServerAvatar from '../UI/ServerAvatar.vue';
import Button from '../UI/Button.vue';
import IconAction from '../UI/IconAction.vue';
import { useGlobalStore } from '@/stores/globalStore';
import { useRouter } from 'vue-router';
const router = useRouter();
const globalStore = useGlobalStore();
const props = defineProps({
server: {
type: Object,
required: true,
default: () => ({
id: '',
name: '',
owner: false,
icon: '',
members: [],
serverMember: 0,
connected: false
})
}
});
function access() {
router.push(`/servers/${props.server.id}`);
}
</script>
<style scoped>
.container-avatar {
display: flex;
align-items: center;
gap: 10px;;
}
.actions {
display: flex;
align-items: center;
gap: 10px;
}
.container-list {
display: flex;
justify-content: space-between;
align-items: center;
}
p {
margin: 0;
}
.data {
color: var(--text-secondary);
font-size: 0.8em;
display: flex;
align-items: center;
gap: 3px;
}
.info {
display: flex;
flex-direction: column;
gap: 5px;
}
</style>

View File

@@ -0,0 +1,50 @@
<template>
<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)"/>
</div>
</Box>
</template>
<script setup>
import Box from '../UI/Box.vue';
import IconAction from '../UI/IconAction.vue';
import User from '../UI/User.vue';
import { signOut } from '@/utils/UserRequest';
import { useRouter } from 'vue-router';
import { socket } from '@/socket.js';
if(!socket.connected) {
socket.connect();
}
import { useUserStore } from '@/stores/userStore';
const userStore = useUserStore();
const router = useRouter();
function goToSettings() {
console.log(router)
router.push("/settings");
}
</script>
<style scoped>
.box {
display: flex;
align-items: center;
justify-content: space-between;
flex-direction: row;
gap: 30px;
width: 100%;
}
.user-action {
display: flex;
align-items: center;
gap: 15px;
}
</style>

View File

@@ -0,0 +1,37 @@
<template>
<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 Box from '@/components/UI/Box.vue';
const props = defineProps({
gap: {
type: String,
default: '10px'
},
width: {
type: String,
default: ''
}
})
</script>
<style scoped>
.splash-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
margin-left: 20px;
margin-right: 20px;
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div class="splash-container">
<div class="splash-title">
<img src="/logo-white.svg" alt="Logo"/>
<h1>Subsonics</h1>
</div>
<slot></slot>
</div>
</template>
<script setup>
</script>
<style scoped>
.splash-container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
width: 100vw;
gap: 46px;
background-color: var(--main); /* Example background color */
}
h1 {
font-family: 'Gunship';
font-size: 30px;
color: white;
user-select: none;
}
img {
width: 100px;
height: 100px;
}
.splash-title {
display: flex;
align-items: center;
}
</style>

View File

@@ -0,0 +1,23 @@
<template>
<img :src="`https://cdn.discordapp.com/avatars/${userId}/${avatarUrl}`" alt='User Avatar'>
</template>
<script setup>
const props = defineProps({
userId: {
type: String,
required: true
},
avatarUrl: {
type: String,
required: true
}
});
</script>
<style scoped>
img {
border-radius: 100%;
width: 50px;
height: 50px;
object-fit: cover;
}
</style>

73
src/components/UI/Box.vue Normal file
View File

@@ -0,0 +1,73 @@
<template>
<div :style="widthStyle" :class="activeClass">
<slot></slot>
</div>
</template>
<style scoped>
div {
background-color: var(--secondary);
border-radius: 10px;
}
.box-shadow {
box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.50);
}
.closed {
padding: 10px;
}
.far {
padding: 30px;
}
.first {
background-color: var(--secondary);
}
.second {
background-color: var(--tertiary);
}
</style>
<script setup>
import { computed } from 'vue';
const props = defineProps({
level: {
type: String,
default: 'first'
},
padding: {
type: String,
default: 'far'
},
boxClass: {
type: String,
default: ''
},
width: {
type: String,
default: ''
},
noShadow: {
type: Boolean,
default: false
}
});
const widthStyle = computed(() => {
if (props.width) {
return `max-width: ${props.width}; width: 70%;`;
}
return '';
});
const activeClass = computed(() => {
return `${props.level} ${props.padding} ${props.boxClass} ${props.noShadow ? '' : 'box-shadow'}`;
});
</script>

View File

@@ -0,0 +1,61 @@
<template>
<button :disabled="props.disabled" :class="disableClass" @click="$emit('click')">
<slot></slot>
</button>
</template>
<script setup>
import { computed } from 'vue';
const events = defineEmits(['click']);
const props = defineProps({
disabled: {
type: Boolean,
default: false
}
});
const disableClass = computed(() => {
return props.disabled ? 'disabled' : '';
});
</script>
<style scoped>
button {
background-color: var(--main);
color: rgb(255, 255, 255);
border: none;
border-radius: 5px;
padding: 10px;
display: flex;
gap: 10px;
justify-content: center;
align-items: center;
text-decoration: none;
transition: all 0.3s ease;
}
button:hover {
background-color: var(--main-hover);
cursor: pointer;
}
button:active {
background-color: var(--main-active);
transform: scale(0.98);
}
.disabled {
background-color: var(--tertiary);
color: var(--text-secondary);
cursor: not-allowed;
}
.disabled:hover {
background-color: var(--tertiary) !important;
cursor: not-allowed;
}
.disabled:active {
background-color: var(--tertiary) !important;
transform: none !important ;
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<div>
<Icon icon="fa-solid fa-circle-exclamation"></Icon>
<div class="error-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-error);
animation: fadeIn 0.5s ease-in-out;
}
.error-message {
display: flex;
text-align: justify;
}
</style>

View File

@@ -0,0 +1,36 @@
<template>
<Icon :color="color" :class="!fixed ? 'icon' : 'fixed'" :icon="icon" />
</template>
<script setup>
const props = defineProps({
icon: {
type: String,
required: true
},
color: {
type: String,
default: 'var(--text)'
},
fixed: {
type: Boolean,
default: false
}
});
</script>
<style scoped>
.icon {
font-size: 1.3em;
transition: filter 0.3s ease;
}
.fixed {
font-size: 1.3em;
transition: filter 0.3s ease;
}
.icon:hover {
cursor: pointer;
filter: brightness(0.8);
}
</style>

View File

@@ -0,0 +1,30 @@
<template>
<div>
<Icon icon="fa-solid fa-circle-info"></Icon>
<div class="info-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);
animation: fadeIn 0.5s ease-in-out;
}
.info-message {
display: flex;
text-align: justify;
}
</style>

View File

@@ -0,0 +1,24 @@
<template>
<img v-if="serverIcon" :src="`https://cdn.discordapp.com/icons/${serverId}/${serverIcon}.png?`" />
<img v-else :src="`https://cdn.discordapp.com/embed/avatars/${serverId % 5}.png`" />
</template>
<script setup>
const props = defineProps({
serverId: {
type: String,
required: true
},
serverIcon: {
type: String,
default: ''
}
});
</script>
<style scoped>
img {
width: 70px;
height: 70px;
border-radius: 10px;
background-color: var(--secondary);
}
</style>

View File

@@ -0,0 +1,61 @@
<template>
<div class="user-card">
<Avatar :avatar-url="user?.identity?.avatar" :user-id="user?.identity?.id" />
<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>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
import Avatar from './Avatar.vue';
const props = defineProps({
user: {
type: Object,
default: () => ({
identity: {
id: '261175887393718273',
username: 'Identifiant inconnu',
global_name: 'Nom d\'affichage inconnu',
avatar: '96ecebdefe9fb9483b2e1b17a417ba32'
}
})
}
});
</script>
<style scoped>
img {
border-radius: 100%;
width: 50px;
height: 50px;
}
p {
margin: 0;
}
.user-card {
display: flex;
align-items: center;
gap: 10px;
user-select: none;
}
.user-info {
display: flex;
gap: 5px ;
flex-direction: column;
}
.global {
font-size: 15px;
}
.username {
color: var(--text-secondary);
font-size: 12px;
}
</style>

40
src/main.js Normal file
View File

@@ -0,0 +1,40 @@
/**
* [Subsonics Chopin - Frontend] - Raphix - 07/2025
* File: main.js
* Description: Main entry point for the frontend application
*/
import { createApp } from 'vue'
import { createHead } from '@unhead/vue/client'
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
import { library } from '@fortawesome/fontawesome-svg-core'
import { fas } from '@fortawesome/free-solid-svg-icons'
import { fab } from '@fortawesome/free-brands-svg-icons'
import { far } from '@fortawesome/free-regular-svg-icons'
import "@/assets/Global.scss"
import App from './App.vue'
import router from './routes'
import { createPinia } from 'pinia'
let app = createApp(App)
let head = createHead({
init: [{
title: 'Subsonics',
meta: [
{ name: 'description', content: 'Interface de gestion du Bot Discord Subsonics' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ name: "charset", content: "utf-8" },
]
}]
})
let pinia = createPinia()
app.component('Icon', FontAwesomeIcon)
library.add(fas, fab, far)
app.use(pinia)
app.use(head)
app.use(router)
app.mount('#app')

100
src/routes.js Normal file
View File

@@ -0,0 +1,100 @@
import { createRouter, createWebHistory } from 'vue-router'
import PageError from './views/PageError.vue'
import Access from './utils/Access.vue'
import Login from './views/Login.vue'
import Privacy from './views/Privacy.vue'
import Terms from './views/Terms.vue'
import Redirect from './utils/Redirect.vue'
import Servers from './views/Servers.vue'
import { socket } from "@/socket"
import { useLoginStore } from './stores/loginStore'
import { useGlobalStore } from './stores/globalStore'
import Interface from './views/Interface.vue'
const router = createRouter({
history: createWebHistory(),
routes: [
{path: "/", component: Access, alias: "/home"},
{path: "/login", component: Login},
{path: "/privacy", component: Privacy},
{path: "/terms", component: Terms},
{path: "/redirect", component: Redirect},
{path: "/servers", component: Servers},
{path: "/servers/:guildId", component: Interface, props: true},
{path: "/:pathMatch(.*)*", component: PageError, props: {message: "404 - Page inexistante"}},
]
})
router.beforeEach((to, from, next) => {
socket.removeAllListeners()
console.log("Removing all socket listeners before navigation")
applyListeners()
console.log(`Navigation de ${from.path} vers ${to.path}`)
const loginStore = useLoginStore()
const globalStore = useGlobalStore()
console.log(from.matched)
if(from.matched.length == 0) {
globalStore.setLastRoute(null)
} else {
globalStore.setLastRoute(from.path)
}
if (to.path === "/") {
if (loginStore.token) {
next()
} else {
next("/login")
}
return
}
if (to.path === "/servers") {
if (loginStore.token) {
next()
} else {
next("/login")
}
return
}
if (to.path === "/login" && localStorage.getItem("token")) {
next("/")
return
}
if (to.path === "/redirect" && localStorage.getItem("token")) {
next("/")
return
}
if (to.path === "/home") {
next("/")
return
}
next()
})
function applyListeners() {
socket.on("connect", () => {
console.log(`Connexion au serveur Subsonics réussi - ID : ${socket.id}`);
});
socket.on("disconnect", () => {
console.log("Déconnecté du serveur Subsonics");
});
socket.on("connect_error", (error) => {
console.error("Erreur de connexion au serveur Subsonics :", error);
});
socket.on("error", (error) => {
console.error("Erreur du serveur Subsonics :", error);
});
}
export default router

22
src/socket.js Normal file
View File

@@ -0,0 +1,22 @@
import { io } from "socket.io-client";
var URL = null
await fetch("/information.json").then(response => response.json())
.then(data => {
if (import.meta.env.DEV) {
URL = data.backend.development;
} else {
URL = data.backend.production;
}
}).catch(error => {
console.error("Erreur lors de la récupération de l'URL du backend :", error);
});
export const socket = io(URL, {
autoConnect: false,
reconnectionAttempts: 0,
auth: {
token: localStorage.getItem("token") || null,
sessionId: localStorage.getItem("session") || null
},
});

44
src/stores/globalStore.js Normal file
View File

@@ -0,0 +1,44 @@
import { defineStore } from "pinia";
import { ref, watch } from 'vue';
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");
document.documentElement.setAttribute("data-theme", theme.value);
function setLoading(loading) {
console.log("Setting loading state to", loading);
isLoading.value = loading;
}
function setLastRoute(route) {
console.log("Setting last route to", route);
lastRoute.value = route;
}
function setLastGuild(guild) {
console.log("Setting last guild to", guild);
lastGuild.value = guild;
localStorage.setItem("lastGuild", guild || null);
}
function toogleTheme() {
theme.value = theme.value === "dark" ? "light" : "dark";
localStorage.setItem("theme", theme.value);
document.documentElement.setAttribute("data-theme", theme.value);
console.log("Theme toggled to", theme.value);
}
return {
isLoading,
setLoading,
lastRoute,
setLastRoute,
theme,
toogleTheme,
lastGuild,
setLastGuild
};
})

58
src/stores/loginStore.js Normal file
View File

@@ -0,0 +1,58 @@
import { defineStore } from "pinia";
import { ref, watch } from "vue";
export const useLoginStore = defineStore('login',() =>{
const session = ref(localStorage.getItem("session"));
const token = ref(localStorage.getItem("token"));
watch(session, (newValue) => {
if (newValue) {
localStorage.setItem('session', newValue)
} else {
localStorage.removeItem('session')
}
})
watch(token, (newValue) => {
if (newValue) {
localStorage.setItem('token', newValue)
} else {
localStorage.removeItem('token')
}
})
function setSession(newSession) {
console.log("Setting new session");
session.value = newSession;
localStorage.setItem("session", newSession);
}
function clearSession() {
console.log("Clearing session");
session.value = null;
localStorage.removeItem("session");
}
function setToken(newToken) {
console.log("Setting new token");
token.value = newToken;
localStorage.setItem("token", newToken);
}
function clear() {
localStorage.removeItem("session");
localStorage.removeItem("token");
session.value = null;
token.value = null;
}
return {
session,
token,
setSession,
setToken,
clear,
clearSession
}
})

30
src/stores/userStore.js Normal file
View File

@@ -0,0 +1,30 @@
import { defineStore } from 'pinia';
import { ref, watch } from 'vue';
export const useUserStore = defineStore('user', () => {
const userInfo = ref(null);
watch(userInfo, (newValue) => {
if (newValue) {
localStorage.setItem('userInfo', JSON.stringify(newValue));
} else {
localStorage.removeItem('userInfo');
}
});
function setUserInfo(newUserInfo) {
console.log("Setting new user info");
userInfo.value = newUserInfo;
}
function clearUserInfo() {
console.log("Clearing user info");
userInfo.value = null;
}
return {
userInfo,
setUserInfo,
clearUserInfo,
};
})

24
src/utils/Access.vue Normal file
View File

@@ -0,0 +1,24 @@
<template>
</template>
<script setup>
import { useRouter } from 'vue-router';
import { useUserStore } from '@/stores/userStore';
import { useGlobalStore } from '@/stores/globalStore';
const userStore = useUserStore();
const globalStore = useGlobalStore();
const router = useRouter();
console.log("Access Vue Loaded");
console.log(globalStore.lastGuild + "LAST GUILD FROM ACCESS VUE");
if(!globalStore.lastGuild && globalStore.lastGuild !== null) {
router.push("/servers");
} else {
router.push(`/servers/${globalStore.lastGuild}`);
}
</script>

22
src/utils/IORequest.js Normal file
View File

@@ -0,0 +1,22 @@
import { socket } from '@/socket';
export function IORequest(event, callback, data) {
console.log(`IORequest: Emitting event ${event} with data:`, data);
socket.emit(event, data);
if (callback && typeof callback === 'function') {
socket.once(event, (response) => {
console.log(`IORequest: Received response for event ${event}:`, response);
callback(response);
});
}
}
export function IOListener(event, callback) {
console.log(`IOListener: Listen for event ${event}`);
socket.on(event, (data) => {
if (callback && typeof callback === 'function') {
console.log(`IOListener: Received data for event ${event}:`, data);
callback(data);
}
});
}

40
src/utils/Loader.vue Normal file
View File

@@ -0,0 +1,40 @@
<script setup>
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
const defaultMessage = "On s'accorde et on prépare le concert !";
const props = defineProps({
interuptionMessage: {
type: String,
default: null
}
});
</script>
<template>
<DefaultSplash>
<h1 v-if="!interuptionMessage" class="separate"><Icon spin-pulse icon="fa-solid fa-spinner"/> Chargement de l'interface</h1>
<h1 v-else><Icon icon="fa-solid fa-warning"/> Connexion interrompue</h1>
<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="error"><Icon icon="fa-solid fa-circle-xmark"/> {{ interuptionMessage }}</p>
<p v-else>{{ defaultMessage }}</p>
</DefaultSplash>
</template>
<style scoped>
.retry {
font-size: 1.2em;
margin-top: 10px;
}
.error {
color: var(--text-error);
}
.separate {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
</style>

85
src/utils/Redirect.vue Normal file
View File

@@ -0,0 +1,85 @@
<template>
<DefaultSplash width="600px;">
<h1><Icon spin-pulse icon="fa-solid fa-spinner"/> Connexion en cours</h1>
<p>Vous allez être redirigé vers l'application.</p>
<div class="info">
<p class="second">Session : {{ loginStore.session }}</p>
<p class="second">Auth_Code : {{ route.query.code }}</p>
</div>
</DefaultSplash>
</template>
<script setup>
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
import { useLoginStore } from '@/stores/loginStore';
import { useRoute, useRouter } from 'vue-router';
import {socket} from '@/socket.js';
const loginStore = useLoginStore();
const route = useRoute();
const router = useRouter();
if(!loginStore.session) {
router.push("/login?error=" + encodeURIComponent("Session expirée ou non valide"));
}
if(!route.query.code) {
if(route.query.error) {
router.push("/login?error=" + encodeURIComponent("Connexion refusée par l'utilisateur"));
} else {
// If no error is specified, redirect to login with a generic error message
router.push("/login?error=" + encodeURIComponent("Code d'authentification Discord manquant"));
}
} else {
// Edit socket auth
socket.auth = {
sessionId: loginStore.session,
auth_code: route.query.code
};
socket.connect();
}
socket.on("NEW_TOKEN", (token) => {
loginStore.setToken(token);
loginStore.setSession(null)
router.push("/");
});
socket.on("error", () => {
router.push("/login?error=" + encodeURIComponent("Déconnexion du serveur lors de la connexion"));
});
socket.on("disconnect", () => {
router.push("/login?error=" + encodeURIComponent("Erreur lors de l'authentification, veuillez réessayer"));
});
socket.on("connect_error", (error) => {
router.push("/login?error=" + encodeURIComponent("Erreur de connexion au serveur : " + error.message));
});
socket.on("NEW_SESSION", (data) => {
console.log("Nouvelle session reçue :", data);
loginStore.setSession(data);
});
</script>
<style scoped>
.second {
color: var(--text-secondary);
font-size: 12px;;
word-break: break-all;
}
.info p {
margin: 0;
padding: 0;
}
.info {
display: flex;
flex-direction: column;
align-items: center;
gap: -10px;
}
</style>

View File

@@ -0,0 +1,88 @@
<template>
<Loader :interuption-message='interuptionMessage' v-if="globalStore.isLoading || interuptionMessage" />
<slot v-else></slot>
</template>
<script setup>
import Loader from '@/utils/Loader.vue';
import { ref } from 'vue';
import { useLoginStore } from '@/stores/loginStore';
import { socket } from '@/socket.js';
import { useRouter } from 'vue-router';
import { IOListener, IORequest } from '@/utils/IORequest';
import { useUserStore } from '@/stores/userStore';
import { useGlobalStore } from '@/stores/globalStore';
const router = useRouter();
const interuptionMessage = ref(null);
const needReload = ref(true)
const loginStore = useLoginStore();
const userStore = useUserStore();
const globalStore = useGlobalStore();
loginStore.clearSession();
socket.auth.sessionId = null;
socket.auth.token = loginStore.token;
if(!socket.connected) {
socket.connect();
}
IOListener("/USER/READY", () => {
IORequest("/USER/INFO", (data) => {
interuptionMessage.value = null;
userStore.setUserInfo(data);
console.log("User info received:", data);
globalStore.setLoading(false);
});
});
IOListener("AUTH_ERROR", (error) => {
console.error("Authentication error:", error);
loginStore.setToken(null);
globalStore.setLoading(true)
router.push("/login?error=" + encodeURIComponent("Erreur d'authentification : " + error));
socket.removeAllListeners();
needReload.value = false
})
socket.on("connect", () => {
interuptionMessage.value = null;
globalStore.setLoading(true);
});
socket.on("connect_error", (error) => {
interuptionMessage.value = "Erreur de connexion au serveur : " + error.message;
tryReconnect();
});
socket.on("error", () => {
interuptionMessage.value = "Erreur de connexion au serveur, veuillez réessayer plus tard";
tryReconnect();
})
socket.on("disconnect", () => {
interuptionMessage.value = "Déconnecté du serveur";
tryReconnect();
})
function tryReconnect() {
if(socket.connected) {
interuptionMessage.value = null;
return;
}
setInterval(() => {
console.log("Tentative de reconnexion au serveur...");
if(!socket.connected || !needReload.value) {
socket.auth.token = loginStore.token;
socket.connect();
} else {
clearInterval(this);
}
}, 3000); // Essai de reconnexion après 5 secondes
}
</script>r

11
src/utils/UserRequest.js Normal file
View File

@@ -0,0 +1,11 @@
import { useLoginStore } from "@/stores/loginStore";
import { IORequest } from "@/utils/IORequest";
export function signOut(router) {
const loginStore = useLoginStore();
IORequest("/USER/SIGNOUT", () => {
loginStore.setToken(null);
router.push("/login?message=" + encodeURIComponent("Vous avez été déconnecté"));
})
}

95
src/views/Interface.vue Normal file
View File

@@ -0,0 +1,95 @@
<template>
<SocketEnvironment>
<div class="container">
<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="globalStore.toogleTheme()">Changer le thème</Button>
<p>{{ guildId }}</p>
</Box>
<UserAction/>
</div>
</SocketEnvironment>
</template>
<script setup>
import Box from '@/components/UI/Box.vue';
import Button from '@/components/UI/Button.vue';
import SocketEnvironment from '@/utils/SocketEnvironment.vue';
import UserAction from '@/components/Features/UserAction.vue';
import { watch } from 'vue';
import { useRouter } from 'vue-router';
import { useGlobalStore } from '@/stores/globalStore';
import { useUserStore } from '@/stores/userStore';
import { IOListener } from '@/utils/IORequest';
import { onMounted } from 'vue';
const props = defineProps({
guildId: {
type: String,
default: null
}
});
const guildId = props.guildId;
if (!guildId) {
globalStore.setLastGuild(null);
router.push('/servers');
}
const globalStore = useGlobalStore();
const userStore = useUserStore();
const router = useRouter();
watch(() => globalStore.isLoading, (value) => {
if(!value) {
loadInteface();
}
})
onMounted(() => {
if(globalStore.lastGuild && !globalStore.isLoading) {
loadInteface();
}
});
function loadInteface() {
console.log("Loading interface")
console.log("Guild ID:", guildId);
checkGuildAvailability();
}
function checkGuildAvailability() {
if(userStore.userInfo.guilds.filter(guild => guild.id === guildId).length === 0) {
globalStore.setLastGuild(null);
console.warn("Guild not found, redirecting to servers list.");
router.push('/servers');
} else {
globalStore.setLastGuild(guildId);
}
}
</script>
<style scoped>
.container {
padding: 20px;
max-width: 800px;
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
}
.container {
padding: 20px;;
max-width: 800px;
display: flex;
flex-direction: column;
gap: 20px;
width: 100%;
}
</style>

193
src/views/Login.vue Normal file
View File

@@ -0,0 +1,193 @@
<template>
<DefaultSplash width="500px;" gap="30px">
<h1>Votre ticket ?</h1>
<p>Connexion par Discord (obligatoire)</p>
<Button :disabled='!loginReady' @click="handleLogin()"><Icon v-if="loading" spin-pulse icon="fa-solid fa-spinner"/><img src="/discord-logo-white.png"></Button>
<div v-if="error || errorServer || callbackError || message" class="error-container">
<Error v-if="callbackError">{{ callbackError }}</Error>
<Error v-if="error">{{ error }}</Error>
<Error v-if="errorServer">{{ errorServer }}</Error>
<Info v-if="message">{{ message }}</Info>
</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>
</div>
<p class="second">Session : {{ sessionId }}</p>
</DefaultSplash>
</template>
<script setup>
import DefaultSplash from '@/components/Layout/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 { socket } from '@/socket.js';
import { useGlobalStore } from '@/stores/globalStore';
const loginStore = useLoginStore();
const globalStore = useGlobalStore();
const route = useRoute();
const router = useRouter();
const props = defineProps({
callbackError: {
type: String,
default: undefined
}
})
const callbackError = ref(undefined);
const message = ref(undefined);
const errorServer = ref(undefined);
const error = ref(undefined);
const serverError = ref(true);
const loading = ref(true);
const sessionId = computed(() => loginStore.session ? loginStore.session : "Aucune session active");
const loginReady = computed(() => loginStore.session && discordUrl.value && !serverError.value);
var discordUrl = ref(null);
if(socket.connected) {
socket.disconnect()
}
socket.connect();
if(props.callbackError) {
callbackError.value = props.callbackError;
}
if(route.query.error) {
callbackError.value = route.query.error;
}
if(route.query.message) {
message.value = route.query.message;
}
if(!globalStore.lastRoute) {
callbackError.value = null;
message.value = null;
error.value = null;
errorServer.value = null;
}
var errorSessionMsg = "Aucune session n'est active. Le serveur est-il en ligne ?";
watch(loginReady, (ready) => {
if(ready) {
loading.value = false;
} else {
loading.value = true;
}
});
if(!loginStore.session) {
error.value = errorSessionMsg;
}
watch(() => loginStore.session, (session) => {
error.value = !session ? errorSessionMsg: undefined;
});
fetch("/information.json")
.then(response => response.json())
.then(data => {
discordUrl.value = import.meta.env.DEV ? data.discord.development : data.discord.production;
})
.catch(e => {
console.error("Error fetching Discord URL:", e);
error.value = "Une erreur est survenue lors de la récupération de l'URL Discord.";
loading.value = false;
});
function handleLogin() {
window.location.href = discordUrl.value;
}
socket.on("NEW_SESSION", (data) => {
console.log("Nouvelle session reçue :", data);
loginStore.setSession(data);
socket.io.opts.reconnection = false;
socket.disconnect();
});
socket.on("connect", () => {
serverError.value = false;
if(loginStore.token) {
window.location.href = discordUrl.value;
}
console.log("Login : Socket connected successfully");
});
socket.on("connect_error", (error) => {
loading.value = false;
errorServer.value = "Erreur de connexion au serveur. Veuillez réessayer plus tard.";
error.value = "Erreur de connexion au serveur. Veuillez réessayer plus tard.";
serverError.value = true;
});
</script>
<style scoped>
img {
width: 80%;
height: 100%;
}
h1 {
margin: 0 !important;
font-size: 25px;
}
Button {
width: 160px;;
}
p {
max-width: 100vh;
margin: 0 !important;
}
.secondtext {
font-size: 12px;
color: var(--text-secondary);
margin-top: 10px;
text-align: justify;
}
.footer {
display: flex;
flex-direction: column;
gap: 10px;
text-align: center;
align-items: center;
}
.error-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
width: 100%;
}
.second {
color: var(--text-secondary);
font-size: 8px;
margin-top: 10px;
text-align: center;
padding-right: 5px;
padding-left: 5px;
word-break: break-all;
}
</style>

45
src/views/PageError.vue Normal file
View File

@@ -0,0 +1,45 @@
<template>
<DefaultSplash width="600px">
<div class="error-page">
<h1>Erreur ! </h1>
<strong><p>On s'est trompé de mesure !</p></strong>
<Box no-shadow level="second" padding="closed">
<p>Raison : {{ message }}</p>
</Box>
</div>
<br>
<router-link class="no-decoration" to="/">
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
</router-link>
</DefaultSplash>
</template>
<script setup>
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
import Button from '@/components/UI/Button.vue'
import Box from '@/components/UI/Box.vue';
import { socket } from '@/socket';
defineProps({
message: {
type: String,
default: "Une erreur est survenue"
}
})
if(socket.connected) {
socket.disconnect();
}
</script>
<style scoped>
.error-page {
max-width: 600px;
width: 100%;
text-align: center;;
}
</style>

119
src/views/Privacy.vue Normal file
View File

@@ -0,0 +1,119 @@
<script setup>
import ReturnHomeButton from '@/components/Actions/ReturnHomeButton.vue';
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
import Box from '@/components/UI/Box.vue';
import Button from '@/components/UI/Button.vue';
</script>
<template>
<DefaultSplash>
<ReturnHomeButton/>
<h1>Privacy</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>
<p><strong>Responsable du traitement des données :</strong> Raphix (<code>raphixscrap</code> sur Discord)</p>
<h2>1. Introduction</h2>
<p>Subsonics (le "bot" et l"application") est un bot Discord et une application Web proposant des fonctionnalités audio pour enrichir lexpérience de vos serveurs. </p>
<p>Cette politique de confidentialité décrit quelles données sont collectées, comment elles sont utilisées, et quels sont vos droits.</p>
<p>En utilisant Subsonics ou son interface Web, vous acceptez les conditions de cette politique de confidentialité.</p>
<h2>2. Données collectées</h2>
<p><strong>Données collectées automatiquement (pour le bon fonctionnement) :</strong></p>
<ul>
<li>Identifiant utilisateur Discord (User ID)</li>
<li>Identifiant de serveur (Guild ID)</li>
<li>Identifiant de salon (Channel ID)</li>
<li>Commandes utilisées et journaux dexécution</li>
<li>Données de lecture audio (titres de morceaux, horodatage, etc.)</li>
</ul>
<p><strong>Concernant lapplication Web :</strong></p>
<ul>
<li>Adresse IP (logs de sécurité)</li>
<li>Informations du navigateur (User-Agent)</li>
<li>Cookies (préférences, sécurité, analytics si activé)</li>
</ul>
<p>Nous <strong>ne collectons pas</strong> vos messages privés, vos conversations vocales ni aucune donnée personnelle sensible.</p>
<h2>3. Utilisation des données</h2>
<p>Les données collectées sont utilisées uniquement pour :</p>
<ul>
<li>Assurer le bon fonctionnement du bot</li>
<li>Corriger les bugs et améliorer lexpérience utilisateur</li>
<li>Analyser lutilisation de certaines commandes (statistiques anonymes)</li>
<li>Empêcher les abus et sécuriser linfrastructure</li>
</ul>
<p>Nous ne revendons ni ne partageons vos données avec des tiers.</p>
<h2>4. Conservation des données</h2>
<ul>
<li>Les données des utilisateurs et serveurs sont conservées <strong>tant que le bot est présent sur le serveur et tant qu'une demande de supression n'a été expressément demandé</strong>.</li>
</ul>
<h2>5. Vos droits</h2>
<p>Conformément au RGPD, vous avez le droit de :</p>
<ul>
<li>Demander laccès à vos données</li>
<li>Demander la suppression de vos données</li>
<li>Demander la rectification dinformations incorrectes</li>
<li>Retirer votre consentement à tout moment (en retirant le bot de votre serveur ou via une demande)</li>
</ul>
<h2>6. Suppression des données / Demandes</h2>
<p>Pour toute demande liée à vos données personnelles, vous pouvez contacter le développeur :</p>
<ul>
<li><strong>Nom :</strong> Raphix</li>
<li><strong>Contact Discord :</strong> <code>raphixscrap</code></li>
<li><strong>Informations à fournir :</strong> votre identifiant Discord (User ID) ou lID du serveur concerné</li>
</ul>
<h2>7. Sécurité</h2>
<p>Nous mettons en œuvre des mesures techniques et organisationnelles raisonnables pour protéger vos données, incluant :</p>
<ul>
<li>Accès limité aux données</li>
<li>Stockage sécurisé</li>
<li>Mises à jour régulières des dépendances</li>
<li>Surveillance et alertes en cas danomalie</li>
</ul>
<p>Malgré cela, aucun système nest totalement invulnérable et nous ne pouvons garantir une sécurité absolue.</p>
<h2>8. Services tiers</h2>
<p>Subsonics utilise lAPI Discord ainsi que des services dhébergement. Ces plateformes ont leurs propres politiques de confidentialité :</p>
<p><a href="https://discord.com/privacy" target="_blank">Politique de confidentialité de Discord</a></p>
<h2>9. Modifications de cette politique</h2>
<p>Cette politique peut être mise à jour à tout moment. N'hésitez pas à consulter régulièrement cette page pour rester informé des changements.</p>
<h2>10. Contact</h2>
<p>Pour toute question, suggestion ou réclamation :</p>
<ul>
<li><strong>Développeur :</strong> Raphix</li>
<li><strong>Contact Discord :</strong> <code>raphixscrap</code></li>
</ul>
</div>
</Box>
<br/>
<router-link class="no-decoration" to="/">
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
</router-link>
<!-- Add more content here as needed -->
</DefaultSplash>
</template>
<style scoped>
.terms-content {
max-width: 800px;
margin: 0 auto;
padding-right: 10px;
padding-left: 10px;
font-size: 12px;
line-height: 1.6;
max-height: 35vh;
overflow-y: auto;
color: var(--text-secondary);
text-align: justify;
}
</style>

117
src/views/Servers.vue Normal file
View File

@@ -0,0 +1,117 @@
<script setup>
import UserAction from '@/components/Features/UserAction.vue';
import Splash from '@/components/Layout/Splash.vue';
import Box from '@/components/UI/Box.vue';
import SocketEnvironment from "../utils/SocketEnvironment.vue"
import { useRouter } from 'vue-router';
import { useGlobalStore } from '@/stores/globalStore';
import { useUserStore } from '@/stores/userStore';
import Button from '@/components/UI/Button.vue';
import ReturnHomeButton from '@/components/Actions/ReturnHomeButton.vue';
import { ref } from 'vue';
import ServerListItem from '@/components/Features/ServerListItem.vue';
const globalStore = useGlobalStore();
const userStore = useUserStore();
console.log("Last route:", globalStore.lastRoute);
const router = useRouter();
const hasLink = ref(false);
const botInviteUrl = ref("");
console.log(globalStore.lastGuild);
fetch('/information.json')
.then(response => response.json())
.then(data => {
botInviteUrl.value = import.meta.env.DEV ? data.bot_invite.development : data.bot_invite.production;
hasLink.value = true
})
.catch(error => {
console.error("Error fetching bot invite URL:", error);
hasLink.value = false;
});
function inviteSubsonics() {
window.open(botInviteUrl.value, '_blank');
}
</script>
<template>
<SocketEnvironment>
<Splash>
<ReturnHomeButton v-if="globalStore.lastGuild"/>
<div class="servers-div">
<Box box-class="server-box" padding="closed">
<div class="servers-content">
<div class="servers-container">
<div class="servers-list">
<ServerListItem v-for="server in userStore.userInfo.guilds" :key="server.id" :server="server" />
</div>
</div>
<Button :disabled="!hasLink" @click="inviteSubsonics()"><Icon icon="fa-solid fa-user-plus"/>Inviter Subsonics</Button>
</div>
</Box>
<UserAction/>
</div>
</Splash>
</SocketEnvironment>
</template>
<style scoped>
.servers-div {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
width: 90%;
max-width: 800px;;
margin-left: 20px;
margin-right: 20px;
}
.servers-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 20px;
}
.server-box {
width: 100%;
max-width: 800px;
}
.servers-list {
display: flex;
flex-direction: column;
gap:10px;
overflow-y: auto;
width: 100%;
}
.servers-container {
overflow-y: auto;
flex: 1;
padding-right: 5px;
padding-left: 5px;
width: 100%;
position: relative;
max-height: 45vh;
}
h1 {
font-size: 20px;;
}
</style>

43
src/views/Terms.vue Normal file
View File

@@ -0,0 +1,43 @@
<script setup>
import ReturnHomeButton from '@/components/Actions/ReturnHomeButton.vue';
import DefaultSplash from '@/components/Layout/DefaultSplash.vue';
import Box from '@/components/UI/Box.vue';
import Button from '@/components/UI/Button.vue';
</script>
<template>
<DefaultSplash>
<ReturnHomeButton/>
<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>
</div>
</Box>
<br>
<router-link class="no-decoration" to="/">
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
</router-link>
<!-- Add more content here as needed -->
</DefaultSplash>
</template>
<style scoped>
.terms-content {
max-width: 800px;
margin: 0 auto;
padding-right: 10px;
padding-left: 10px;
font-size: 12px;
line-height: 1.6;
max-height: 35vh;
overflow-y: auto;
color: var(--text-secondary);
text-align: justify;
}
</style>