Compare commits

...

18 Commits

Author SHA1 Message Date
ea9cf2ce42 Version 1.2.0 - Ajout des suggestions et de paramètres
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 35s
2025-09-07 18:18:54 +02:00
b2a95c0241 Version 1.1.0 - Modification et ajout de fonctionnalités
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 34s
2025-09-06 15:47:05 +02:00
febe1f90a2 Version 1.0.3 - Modification du Splash
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 35s
2025-08-31 16:58:04 +02:00
b0ed846ef9 Version 1.0.2-rc2 - Modif erreur pointeur
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 34s
2025-08-29 23:53:52 +02:00
a19472c6cc Version 1.0.2 - Changement Pointeur Default Thumbnail
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 36s
2025-08-29 23:51:52 +02:00
f137bd664a Version 1.0.1-rc3 - Modif erreur responsive
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 32s
2025-08-29 23:36:58 +02:00
de2be3c213 Version 1.0.1-rc2 - Modification Responsive
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 33s
2025-08-29 23:35:32 +02:00
8636571eac Version 1.0.1 - Modification Carousel
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 33s
2025-08-29 23:32:17 +02:00
807f784ca5 Version 1.0.0-rc7.1 - Modif Carousel
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 32s
2025-08-29 15:31:29 +02:00
595e0b9cf9 Version 1.0.0-rc7 - Modification Carousel
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 31s
2025-08-29 15:29:02 +02:00
3c9fca85e3 Version 1.0.0-rc7 - Modification Bug Carousel
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 34s
2025-08-29 15:26:00 +02:00
bc72044270 Version 1.0.0-rc6 - Modif Information.json
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 31s
2025-08-29 11:44:29 +02:00
718c2df916 Version 1.0.0-rc5 - Modif Vite Builder
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 31s
2025-08-29 11:27:44 +02:00
17d91b9064 Version 1.0.0-rc4 - Pipeline Confirmation
All checks were successful
Frontend Deployment / deploy-frontend (push) Successful in 31s
2025-08-29 11:19:45 +02:00
08547bb8f3 Version 1.0.0-rc3 - Pipeline edit
Some checks failed
Frontend Deployment / deploy-frontend (push) Failing after 21s
2025-08-29 10:36:08 +02:00
76017d20b2 Version 1.0.0 - Frontend Pipeline
Some checks failed
Frontend Deployment / deploy-frontend (push) Failing after 11s
2025-08-29 00:38:26 +02:00
4eb6492247 Version 1.0.0-rc2 - Modif Pipeline
Some checks failed
Frontend Deployment / deploy-frontend (push) Failing after 43s
2025-08-29 00:31:57 +02:00
d54d500ebf Version 1.0.0-rc1 - Pipeline
Some checks failed
Frontend Deployment Pipeline / deploy-frontend (push) Failing after 28s
2025-08-29 00:26:18 +02:00
38 changed files with 552 additions and 117 deletions

View File

@@ -0,0 +1,57 @@
name: Frontend Deployment
on:
push:
branches:
- main
jobs:
deploy-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H alpha.raphix.fr >> ~/.ssh/known_hosts
- name: Deploy frontend
run: |
ssh raphix@alpha.raphix.fr "
set -e;
APP_DIR=/home/gitlab-ci/chopin-frontend;
FRONTEND_DIR=/home/gitlab-ci/frontend/dist;
REPO_URL=https://git.raphix.fr/subsonics/chopin-frontend;
BRANCH=main;
echo '[Frontend-Deploy] - START';
# Nettoyage ancien dossier temporaire
rm -rf \$APP_DIR;
mkdir -p \$APP_DIR;
echo 'Cloning repository...';
git clone -b \$BRANCH \$REPO_URL \$APP_DIR;
cd \$APP_DIR;
echo 'Installing dependencies...';
npm ci;
echo 'Building app...';
npm run build;
echo 'Deploying build to frontend/dist...';
mkdir -p \$FRONTEND_DIR;
rm -rf \$FRONTEND_DIR/*;
cp -r dist/* \$FRONTEND_DIR/;
echo 'Cleaning up temporary build directory...';
rm -rf \$APP_DIR;
echo '[Frontend-Deploy] - DONE';
"

View File

@@ -1,6 +1,6 @@
{
"name": "chopin-frontend",
"version": "1.0.0",
"version": "1.2.0",
"private": true,
"scripts": {
"dev": "vite --host --port 8080",

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -5,7 +5,7 @@
},
"backend": {
"development": "http://192.168.1.77:3000",
"production": "http://alpha.raphix.fr:3000"
"production": "https://backend.subsonics.raphix.fr"
},
"bot_invite": {
"development": "https://discord.com/api/oauth2/authorize?client_id=1342913183744004158&permissions=40546675842624&scope=bot%20applications.commands",

View File

@@ -31,8 +31,6 @@ const router = useRouter();
const userSettings = ref(null);
//FIXME: Set to dispatcher
</script>
<style scoped>
.box {

View File

@@ -25,8 +25,8 @@
</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>
<Button icon="fa-solid fa-gear" v-if="gestion" @click="settings.open()">Gestion</Button>
<Button icon="fa-solid fa-arrow-right" :style="{width: gestion ? '' : '100%'}" @click="router.push(`/servers`)">Changer de serveur</Button>
</div>
</div>
</div>
@@ -54,6 +54,7 @@ import GuildHeaderUsers from '../Widget/Guild/GuildHeaderUsers.vue';
import GuildSettings from '../Widget/Guild/GuildSettings.vue';
import { useUserStore } from '@/stores/userStore';
import { useGlobalStore } from '@/stores/globalStore';
import Events from '@/utils/Events';
const userStore = useUserStore();
@@ -109,8 +110,13 @@ function updateServerInfo() {
console.log("Server info updated");
events.emit("GUILD_JOINED", data);
})
}
Events.on("GUILD_LIST_UPDATE", (guilds) => {
server.value = guilds.find(g => g.id === globalStore.lastGuild);
});
</script>
<style scoped>

View File

@@ -112,9 +112,6 @@ const playerOpen = ref(false);
const durationBar = ref(null);
const buffering = ref(false);
//TODO: Rework Animation Both Side
//FIXME: Animation weird 550px
const playerMobile = ref(null);
const playerMobileToggle = ref(null);
const playerHeight = ref(0);

View File

@@ -12,7 +12,7 @@
</p>
</div>
</div>
<Button class="add-playlist" @click="openModal()"><Icon icon="fa-add"/> Ajouter une playlist</Button>
<Button icon="fa-add" class="add-playlist" @click="openModal()">Ajouter une playlist</Button>
</div>
<Modal icon="fa-list" title="Ajouter une Playlist" ref="modal">
<div class="p-modal-content">
@@ -23,7 +23,7 @@
<label>Importer depuis Youtube</label>
</div>
<Button :disabled="(!isYoutube && newPlaylistTitle.trim() === '') || (isYoutube && urlLink?.trim() === '')" @click="addPlaylist">Ajouter</Button>
<Button icon="fa-add" :disabled="(!isYoutube && newPlaylistTitle.trim() === '') || (isYoutube && urlLink?.trim() === '')" @click="addPlaylist">Ajouter</Button>
<p class="text-loading" v-if="isLoading"><Icon icon="fa-spinner" spin-pulse /> Création en cours...</p>
<p class="text-loading" v-if="error"><Error> {{ error }}</Error></p>
</div>

View File

@@ -1,7 +1,11 @@
<template>
<div class="search">
<Icon color="#FFFFFF" icon="fa-solid fa-magnifying-glass" style="width: 20px;" />
<input name="search" ref="searchBar" type="text" placeholder="Insérer votre recherche ici" v-model="searchQuery" />
<div class="search-bar">
<input autocomplete="off" name="search" ref="searchBar" type="text" placeholder="Insérer votre recherche ici" v-model="searchQuery" />
<Icon color="#FFFFFF" v-if="searchQuery.trim() !== ''" icon="fa-solid fa-xmark" class="clear-icon" @click="searchQuery = ''" />
<SearchSuggestions v-if="searchQuery.trim() !== '' && suggestClose" :query="searchQuery" />
</div>
<div class="search-actions">
<IconAction
icon="fa-solid fa-cloud-arrow-up"
@@ -25,17 +29,24 @@ import { onBeforeUnmount, onMounted, ref } from 'vue';
import IconAction from '../UI/IconAction.vue';
import { IORequest } from '@/utils/IORequest';
import Events from '@/utils/Events';
import SearchSuggestions from '@/components/Widget/View/Search/SearchSuggestions.vue';
const searchBar = ref(null);
const searchQuery = ref('');
const suggestClose = ref(true);
onMounted(() => {
searchBar.value.addEventListener('change', find);
searchBar.value.addEventListener('change', () => {
find();
suggestClose.value = true;
});
//TODO: Faire un systême de suggestions.
searchBar.value.addEventListener('keydown', (event) => {
suggestClose.value = true;
});
});
function find() {
function find(queryByResult) {
// If on mobile close the keyboard
if (window.innerWidth < 768) {
searchBar.value.blur();
@@ -43,9 +54,15 @@ function find() {
if (searchQuery.value.trim() === '') {
return;
}
if(queryByResult == "result") {
Events.emit("SEARCH_RESULT_SENDED");
}
Events.emit("SEARCH_STARTED");
IORequest("/SEARCH", (data) => {
Events.emit("SEARCH_RESULT", {data: data, query: searchQuery.value});
if(queryByResult == "result") {
Events.emit("SEARCH_RESULT_SENDED");
}
}, searchQuery.value);
}
@@ -53,6 +70,13 @@ Events.on("VIEW_CLOSED", () => {
searchQuery.value = '';
});
Events.on("SEARCH_RESULT_SELECTED", (result) => {
searchQuery.value = result;
suggestClose.value = false;
Events.emit("SEARCH_RESULT_SENDED");
find("result");
});
</script>
@@ -68,6 +92,19 @@ Events.on("VIEW_CLOSED", () => {
box-shadow: 2px 2px 10px 0px rgba(0, 0, 0, 0.50);
}
.clear-icon {
cursor: pointer;
position: absolute;
right: 10px;
}
.search-bar {
position: relative;
flex: 1;
display: flex;
align-items: center;
}
.search input {
border: none;
outline: none;

View File

@@ -26,8 +26,6 @@ const disableClass = computed(() => {
return props.disabled ? props.colorLower ? 'disabled color-lower' : 'disabled' : '';
});
//TODO: Refactor every button component to use the icon prop
</script>
<style scoped>
button {

View File

@@ -131,21 +131,23 @@ function updateSlots() {
return;
} else {
if(window.innerWidth > 769 && window.innerHeight > 607) {
if(actualComponent.value.props.mobile == '') {
if (actualComponent.value?.props?.mobile) {
actualIndex.value = 0; // Reset index when resizing to desktop
}
}
allSlots.value.forEach((slot, index) => {
if(typeof slot.props?.mobile !== 'undefined') {
if (slot.props.mobile == '' && (window.innerWidth > 769 && window.innerHeight > 607)) {
allSlots.value.splice(index, 1);
}
}
});
}
}
ready.value = true;
}, 100)
}
</script>
<style scoped>
.notactive {

View File

@@ -52,7 +52,8 @@ function getIcons(name) {
margin: 0;
}
@media screen and (max-width: 768px) {
@media screen and (max-width: 768px),
screen and (max-height: 607px) {
.metric {
flex-direction: column;

View File

@@ -65,6 +65,7 @@ function open() {
if (modal.value) {
modal.value.style.display = 'flex';
}
Events.emit('modal:open');
}
defineExpose({

View File

@@ -21,7 +21,8 @@ defineProps({
</script>
<style scoped>
@media screen and (max-width: 768px) {
@media screen and (max-width: 768px),
screen and (max-height: 607px) {
.settings-icon {
display: none;
}

View File

@@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup>
import IconAction from './IconAction.vue';
import { ref, onMounted, watch, onUnmounted } from 'vue';
import { useSlots } from 'vue';

View File

@@ -107,6 +107,7 @@ generateNotes();
color: rgba(255, 255, 255, 0.2);
animation: float 10s infinite linear;
opacity: 0;
user-select: none;
}

View File

@@ -2,7 +2,11 @@
<div class="subsonics-logo">
<LogoDark class="img" v-if="globalStore.theme == 'light'"/>
<LogoLight class="img" v-else/>
<div v-if="dev" class="text-p">
<h1>Subsonics</h1>
<p>Developement</p>
</div>
<h1 v-if="!dev">Subsonics</h1>
</div>
</template>
<script setup>
@@ -10,6 +14,8 @@ import LogoDark from '@/assets/LogoDark.vue';
import LogoLight from '@/assets/LogoLight.vue';
import { useGlobalStore } from '@/stores/globalStore';
const dev = import.meta.env.DEV;
const globalStore = useGlobalStore();
</script>
<style scoped>
@@ -27,6 +33,25 @@ const globalStore = useGlobalStore();
justify-content: center;
}
.text-p {
display: flex;
align-items: flex-start;
flex-direction: column;
position: relative;
}
.text-p h1 {
margin: 0 !important;
}
.text-p p {
margin: 0 !important;
font-family: 'Gunship', sans-serif;
position: absolute;
bottom: -10px;
font-size: 0.8em;
}
@media screen and (max-width: 768px),
screen and (max-height: 607px) {
.img {

View File

@@ -3,9 +3,11 @@
<ModalTree title="Utilisateurs" icon="fa-solid fa-users">
<GuildUsers :server="server"/>
</ModalTree>
<ModalTree v-if="userStore.userInfo.labels.includes('ADMIN') || userStore.userInfo.identity.id == server.owner" title="Sécurité" icon="fa-solid fa-shield-halved">
<GuildSecurity :server="server"/>
</ModalTree>
<ModalTree title="Statistiques" icon="fa-solid fa-chart-simple">
<GuildStats :server="server"/>
</ModalTree>
</Modal>
</template>
@@ -15,13 +17,14 @@ import ModalTree from '@/components/UI/ModalTree.vue';
import { ref } from 'vue';
import GuildUsers from './Settings/GuildUsers.vue';
import GuildStats from './Settings/GuildStats.vue';
import GuildSecurity from './Settings/GuildSecurity.vue';
import Events from '@/utils/Events';
import { useUserStore } from '@/stores/userStore';
const userStore = useUserStore();
const modal = ref(null);
//TODO: Ajouter la sécurité des roles pour empêcher l'utilisation publique du Bot
//TODO: Ajout de Log pour serveur
//TODO: Paramétérer une liste des channels autorisé !
const props = defineProps({
server: {

View File

@@ -0,0 +1,93 @@
<template>
<div class="setting">
<p class="text-secondary"><Icon icon="fa-solid fa-user-shield" /> Autoriser les utilisateurs avec un rôle spécifique</p>
<Selector v-model="roleSelected" ref="roleSelector" v-if="roles.length > 0 && modalOpen">
<Tag v-for="role in roles" :key="role.id" :value="role.id" :color="decimalToHex(role.color)">{{role.name.replace("@everyone", "Tout le monde (@everyone)")}}</Tag>
</Selector>
<Button @click="updateRole" icon="fa-solid fa-rotate">Mettre à jour</Button>
<Success v-if="roleUpdated">Rôle mis à jour avec succès !</Success>
</div>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue';
import Selector from '@/components/UI/Selector.vue';
import { IORequest } from '@/utils/IORequest';
import Tag from '@/components/UI/Tag.vue';
import Events from '@/utils/Events';
import Button from '@/components/UI/Button.vue';
import Success from '@/components/UI/Success.vue';
const roles = ref([])
const roleSelector = ref(null);
const roleUpdated = ref(false);
const roleSelected = ref(null);
const modalOpen = ref(false);
const props = defineProps({
server: {
type: Object,
required: true
}
});
//TODO: FINISH IMPLEMENTATION
onMounted(() => {
actualizeRoles();
})
Events.on("modal:open", () => {
actualizeRoles();
modalOpen.value = true;
roleUpdated.value = false;
})
Events.on("modal:close", () => {
modalOpen.value = false;
roleSelected.value = null;
roleUpdated.value = false;
})
function actualizeRoles() {
IORequest("/OWNER/ROLES/GET", (data) => {
roles.value = data;
roleSelected.value = null;
});
}
function updateRole() {
console.log(roleSelector.value?.firstSlot());
IORequest("/OWNER/ROLES/SET", (data) => {
actualizeRoles();
roleUpdated.value = true;
setTimeout(() => {
roleUpdated.value = false;
}, 3000);
}, roles.value.find(r => r.id === roleSelected.value) || null);
}
function decimalToHex(decimal) {
if(decimal == 0) return "var(--quaternary)"; // Default color
let hex = Number(decimal).toString(16);
hex = "000000".substring(0, 6 - hex.length) + hex;
return `#${hex}`;
}
</script>
<style scoped>
.text-secondary {
color: var(--text-secondary);
font-size: 0.875rem;
margin-bottom: 0.5rem;
text-decoration: underline;
margin: 0;
}
.setting {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
}
</style>

View File

@@ -73,7 +73,8 @@ function banUser(member) {
</script>
<style scoped>
@media screen and (max-width: 768px) {
@media screen and (max-width: 768px),
screen and (max-height: 607px) {
.group {
flex-direction: column;

View File

@@ -41,7 +41,7 @@
<p class="selectorp" :value="playlist.playlistId" v-for="playlist in userStore.playlists" :key="playlist.playlistId"><Icon :icon="playlist.type === 'youtube' ? 'fa-brands fa-youtube' : 'fa-solid fa-music'" /> {{ playlist.title }}</p>
</Selector>
<p v-else class="info-no">Vous n'avez pas encore de playlist. Créez-en une pour sauvegarder ce titre.</p>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: nextList[selectedIndex], playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()"><Icon icon="fa-solid fa-save" /> Sauvegarder</Button>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: nextList[selectedIndex], playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()" icon="fa-solid fa-save">Sauvegarder</Button>
</Modal>
</section>
</template>
@@ -151,7 +151,6 @@ function onDragEnd(event) {
display: flex;
flex-direction: column;
gap: 10px;
padding-right: 5px;
width: 100%;
height: 100%;
overflow-y: auto;
@@ -192,6 +191,7 @@ function onDragEnd(event) {
gap: 10px;
margin-top: 2.5px;
margin-bottom: 2.5px;
padding-right: 5px;
}
.allSpace {
flex: 1;

View File

@@ -30,7 +30,7 @@
<p class="selectorp" :value="playlist.playlistId" v-for="playlist in userStore.playlists" :key="playlist.playlistId"><Icon :icon="playlist.type === 'youtube' ? 'fa-brands fa-youtube' : 'fa-solid fa-music'" /> {{ playlist.title }}</p>
</Selector>
<p v-else class="info-no">Vous n'avez pas encore de playlist. Créez-en une pour sauvegarder ce titre.</p>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: previousList[selectedIndex], playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()"><Icon icon="fa-solid fa-save" /> Sauvegarder</Button>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: previousList[selectedIndex], playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()" icon="fa-solid fa-save">Sauvegarder</Button>
</Modal>
</section>
</template>

View File

@@ -5,7 +5,8 @@
<ServerOnlinePicture class="sop" :key="server.id" v-if="server.members.length > 0" :members="server.members"/>
<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>
<Error v-else-if="server.restricted">Restreint</Error>
<Button color-lower :disabled="userStore.userInfo.labels.includes('BAN_' + server.id) || server.restricted" class="btn" @click="access()" icon="fa-solid fa-right-to-bracket">Accéder</Button>
</section>
</div>
</Box>

View File

@@ -54,7 +54,7 @@ watch(showForm, (newValue) => {
<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>
<Button v-if="!showForm" @click="showForm = !showForm" class="margin" 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>

View File

@@ -1,7 +1,7 @@
<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>
<Button @click="openModal()" 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/>

View File

@@ -1,8 +1,8 @@
<template>
<Video :video="video" ref="videoContainer">
<div ref="controls" class="controls">
<span v-if="globalStore.currentChannel" title="Ajouter à la liste de lecture" @click="disableAction(); playSong(false)" class="control-icon"><AddList /></span>
<span v-if="globalStore.currentChannel" title="Lire maintenant" @click="disableAction(); playSong(true)" class="control-icon"><Icon icon="fa-play" /></span>
<span title="Ajouter à la liste de lecture" @click="disableAction(); playSong(false)" :class="{'control-icon': globalStore.currentChannel, 'control-icon-disable': !globalStore.currentChannel}"><AddList /></span>
<span title="Lire maintenant" @click="disableAction(); playSong(true)" :class="{'control-icon': globalStore.currentChannel, 'control-icon-disable': !globalStore.currentChannel}"><Icon icon="fa-play" /></span>
<span v-if="!props.delete && video.type != 'attachment'" title="Enregistrer dans une playlist" @click="disableAction(); saveModal.open()" class="control-icon"><Icon icon="fa-save" /></span>
<span v-if="props.delete" title="Supprimer" @click="disableAction(); Events.emit('video:delete', { video: props.video })" class="control-icon"><Icon icon="fa-trash" /></span>
</div>
@@ -10,13 +10,13 @@
<Modal icon="fa-solid fa-video" title="Actions" ref="modal">
<Video :video="video"/>
<Button v-if="globalStore.currentChannel" @click="playSong(false)"><AddList /> Ajouter à la liste de lecture</Button>
<Button v-if="globalStore.currentChannel" @click="playSong(true)"><Icon icon="fa-solid fa-play"/> Lire maintenant</Button>
<div v-else>
<Button icon="fa-solid fa-play" v-if="globalStore.currentChannel" @click="playSong(true)"> Lire maintenant</Button>
<div v-if="!globalStore.currentChannel" >
<p class="text-secondary">Connectez vous à un salon audio sur le serveur {{ globalStore.actualServer ? globalStore.actualServer.name : '' }}, pour lancer un titre</p>
<ActualChannel/>
</div>
<Button v-if="!props.delete && video.type != 'attachment'" @click="saveModal.open()"><Icon icon="fa-solid fa-save" /> Enregistrer dans une playlist</Button>
<Button v-if="props.delete" @click="Events.emit('video:delete', { video: props.video })"><Icon icon="fa-solid fa-trash" /> Supprimer</Button>
<Button v-if="!props.delete && video.type != 'attachment'" @click="saveModal.open()" icon="fa-solid fa-save"> Enregistrer dans une playlist</Button>
<Button v-if="props.delete" @click="Events.emit('video:delete', { video: props.video })" icon="fa-solid fa-trash"> Supprimer</Button>
</Modal>
<Modal ref="saveModal" icon="fa-save" title="Sauvegarder dans une playlist">
<Video class="save-video" :video="video"/>
@@ -24,7 +24,7 @@
<p class="selectorp" :value="playlist.playlistId" v-for="playlist in userStore.playlists" :key="playlist.playlistId"><Icon :icon="playlist.type === 'youtube' ? 'fa-brands fa-youtube' : 'fa-solid fa-music'" /> {{ playlist.title }}</p>
</Selector>
<p v-else class="info-no">Vous n'avez pas encore de playlist. Créez-en une pour sauvegarder ce titre.</p>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: props.video, playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()"><Icon icon="fa-solid fa-save" /> Sauvegarder</Button>
<Button :disabled="userStore.playlists?.length === 0" @click="Events.emit('video:add', { video: props.video, playlistId: playlistSelector.firstSlot().props.value }); saveModal.close()" icon="fa-solid fa-save" > Sauvegarder</Button>
</Modal>
</template>
<script setup>
@@ -64,6 +64,7 @@ const props = defineProps({
let nativeVideo = {}
function disableAction() {
if(!globalStore.currentChannel) return;
controls.value.style.display = "none";
}
@@ -74,7 +75,7 @@ let activePointerId = null;
const SLIDE_THRESHOLD = 10; // ajuster si besoin
function playSong(now) {
if(!globalStore.currentChannel) return;
IORequest("/SEARCH/PLAY", (data) => {
modal.value.close();
}, {song: nativeVideo, now: now})
@@ -84,7 +85,7 @@ onMounted(() => {
Object.assign(nativeVideo, props.video);
if(props.video.createdAt) {
props.video.author = 'Ajoutée le ' + new Date(props.video.createdAt).toLocaleString()
props.video.thumbnail = '/src/assets/default_thumbnail.png';
props.video.thumbnail = '/default_thumbnail.png';
nativeVideo.author = userStore.userInfo.identity.username;
}
if(!videoContainer.value) return
@@ -118,9 +119,12 @@ onMounted(() => {
if (activePointerId != null) videoContainer.value.releasePointerCapture(activePointerId);
} catch (e) { /* ignore */ }
if(!videoContainer.value) return
try {
videoContainer.value.removeEventListener('pointermove', onPointerMove);
videoContainer.value.removeEventListener('pointerup', onPointerUp);
videoContainer.value.removeEventListener('pointercancel', onPointerCancel);
} catch (e) { console.log(videoContainer.value) }
activePointerId = null;
};
@@ -149,10 +153,16 @@ onMounted(() => {
isSliding = false;
activePointerId = ev.pointerId;
if(!videoContainer.value) return
try {
thumbnailContainer.value = videoContainer.value.getThumbnailContainer();
videoContainer.value = videoContainer.value.getVideoContainer();
} catch (e) { }
try { videoContainer.value.setPointerCapture(activePointerId); } catch (e) { /* ignore */ }
try {
videoContainer.value.addEventListener('pointermove', onPointerMove);
videoContainer.value.addEventListener('pointerup', onPointerUp);
videoContainer.value.addEventListener('pointercancel', onPointerCancel);
} catch (e) { console.log(videoContainer.value) }
};
videoContainer.value.addEventListener('pointerdown', onPointerDown);
@@ -218,6 +228,20 @@ onMounted(() => {
opacity: 0.8;
}
.control-icon-disable {
font-size: 1.2em;
background-color: var(--text-inverse);
color: var(--secondary);
border-radius: 100%;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0.8;
}
.video:hover .controls {
opacity: 1;

View File

@@ -1,10 +1,6 @@
<template>
<div class="default">
<!-- <Carousel v-show="!loading" class="child"> -->
<Changelog/>
<!-- <History/>
<Advice/>
</Carousel> -->
<p v-show="loading" class="loading">
<Icon icon="fa-spinner fa-solid" spin-pulse/> Chargement en cours
</p>
@@ -47,18 +43,13 @@ p {
}
.default {
display: grid;
grid-template-rows: 1fr;
grid-template-columns: 1fr;
display: flex;
gap: 5px;
flex: 1;
}
@media screen and (max-width: 768px),
screen and (max-height: 607px) {
.default {
grid-template-rows: 1fr;
}
}
.child {

View File

@@ -2,7 +2,7 @@
<section class="changelog">
<Welcome/>
<h2 class="changelog-title">📝 Changelog</h2>
<div class="changelog-overflow" v-if="changelog" >
<div v-if="changelog" class="changelog-overflow">
<div class="changelog-container" v-html="changelog" ></div>
</div>
<div class="textSecond" v-else-if="!error"><Icon icon="fa-spinner fa-solid" spin-pulse/> Chargement en cours</div>
@@ -39,6 +39,8 @@ function loadChangelog() {
data = data.replaceAll("-*", "</span>");
data = data.replaceAll("*_", "<span class='underline'>");
data = data.replaceAll("_*", "</span>");
data = data.replaceAll('/#', "<span class='changelog-tags'>");
data = data.replaceAll('#/', "</span>");
changelog.value = data;
} else {
@@ -102,12 +104,13 @@ function loadChangelog() {
}
.changelog-container {
display: grid;
grid-template-columns: 1fr;
display: flex;
flex-direction: column;
gap: 15px;
overflow-y: auto;
position: absolute;
flex: 1;
width: 100%;
grid-template-rows: auto;
gap: 20px;
}
.changelog {
@@ -119,7 +122,10 @@ function loadChangelog() {
.changelog-overflow {
overflow-y: auto;
position: relative;
max-height: 100%;
flex: 1;
}
.tags {
@@ -225,20 +231,57 @@ function loadChangelog() {
white-space: nowrap; /* Prevent line breaks */
}
@media screen and (max-width: 768px),
screen and (max-height: 607px) {
.changelog-container {
grid-template-columns: 1fr !important;
.changelog-overflow {
position: relative;
}
.changelog-container ul {
padding: 0
display: flex;
flex-direction: column;
gap: 10px;
}
.changelog-tags {
display: flex;
flex-direction: row;
gap: 5px;
background-color: var(--quaternary);
padding: 5px;
border-radius: 10px;
}
@media screen and (max-width: 768px),
screen and (max-height: 607px) {
.changelog-container ul {
padding: 0;
}
.changelog-version ul li::before {
display: none;
}
.welcome-container {
flex-direction: column;
align-items: center;
gap: 10px;
width: 100%;
}
.welcome-actions {
flex-direction: column;
width: 100%;
}
.changelog-tags {
flex-direction: column;
}
}
</style>

View File

@@ -4,7 +4,7 @@
<img v-if="results.thumbnail" class="search-playlist-thumbnail" :src="results.thumbnail" alt="Playlist Thumbnail" />
<div v-else class="search-playlist-thumbnail"><div class="defaultIcon"><Icon icon="fa-music" /></div></div>
<div class="search-playlist-info">
<p class="search-playlist-title">{{ results.title }} <Icon @click="openPlaylistPage()" class="link-icon" icon="fa-solid fa-link" /></p>
<p class="search-playlist-title">{{ results.title }} <Icon v-if="results.url" @click="openPlaylistPage()" class="link-icon" icon="fa-solid fa-link" /></p>
<p class="search-playlist-stats"><Tag color="var(--text-warning)">{{ results.songs.length }} titres</Tag><Tag v-if="results.views" color="var(--text-success)">{{ results.views }} vues</Tag ><Tag v-if="results.songs.length > 0">Durée : {{ results.readduration }}</Tag></p>
<div @click="openAuthorPage()" class="search-playlist-author-info">
<img v-if="results.authorAvatar" :src="results.authorAvatar" alt="Author Thumbnail" class="search-playlist-author" />
@@ -126,6 +126,7 @@ const props = defineProps({
function openAuthorPage() {
if (!props.results.authorId) return;
if(!props.results.author) return;
window.open(props.results.authorId, '_blank');
}

View File

@@ -0,0 +1,116 @@
<template>
<div ref="suggestions" class="search-suggestions">
<ul>
<li class="search-suggestion" v-for="result in results" :key="result.id" @click="selectResult(result[0])">
{{ result[0] }}
<Icon class="search-icon" icon="fa-solid fa-arrow-up-right-from-square" style="float: right;"/>
</li>
</ul>
</div>
</template>
<script setup>
import { onMounted, ref, watch } from 'vue';
import Events from '@/utils/Events';
const props = defineProps({
query: String
});
const results = ref([]);
const suggestions = ref(null);
onMounted(() => {
window.addEventListener('click', (event) => {
if (suggestions.value && !suggestions.value.contains(event.target)) {
results.value = [];
}
});
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
results.value = [];
}
if (event.key === 'Enter') {
results.value = [];
}
});
})
watch(() => props.query, (newQuery) => {
if (newQuery) {
fetchSuggest(newQuery)
.then(data => {
results.value = data[1];
});
} else {
results.value = [];
}
});
function fetchSuggest(query) {
return new Promise((resolve, reject) => {
const callbackName = "jsonp_callback_" + Math.random().toString(36).substr(2, 5);
window[callbackName] = function(data) {
resolve(data);
delete window[callbackName];
script.remove();
};
const script = document.createElement("script");
script.src = `https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&q=${encodeURIComponent(query)}&callback=${callbackName}`;
script.onerror = reject;
document.body.appendChild(script);
});
}
function selectResult(result) {
Events.emit("SEARCH_RESULT_SELECTED", result);
}
Events.on("SEARCH_RESULT_SENDED", () => {
results.value = [];
})
</script>
<style scoped>
.search-suggestions {
position: absolute;
background: var(--secondary);
opacity: 0.95;
z-index: 1000;
width: 100%;
top: 40px;
max-height: 30vh;
overflow-y: auto;
border-radius: 10px;
}
.search-suggestions ul {
list-style: none;
padding: 0;
margin: 0;
}
.search-suggestions li {
padding: 10px;
cursor: pointer;
}
.search-icon {
display: none;
opacity: 0.5;
}
.search-suggestion:hover .search-icon {
display: block !important;
}
.search-suggestions li:hover {
background: var(--tertiary);
}
</style>

View File

@@ -4,7 +4,7 @@
<h2><Icon icon="fa-folder"/> Mes fichiers</h2>
<Box box-class="area-container" padding="close" level="second">
<div class="upload-area">
<Button :color-lower="status === 'download'" :disabled="status === 'download'" class="upload-button" @click="uploadFile()"><Icon icon="fa-upload"/> Ajouter un fichier</Button>
<Button :color-lower="status === 'download'" :disabled="status === 'download'" class="upload-button" @click="uploadFile()" icon="fa-upload"> Ajouter des fichiers</Button>
<p v-if="status === 'download'" class="upload-file-name" ref="uploadStatus"><Icon icon='fa-spinner' spin-pulse/> Téléchargement en cours...</p>
<p v-else-if="status === 'error'" class="upload-status" ref="uploadStatus"><Error>Erreur lors du téléchargement</Error></p>
<p v-else-if="status === 'toohigh'" class="upload-status" ref="uploadStatus"><Error>Le fichier est trop volumineux</Error></p>
@@ -14,15 +14,17 @@
<p class="text-secondary infosup"><Icon icon="fa-circle-info"/> Ce système n'est pas un stockage permanent de données car il dépend du CDN de Discord. Vos fichiers peuvent exprirer à tout moment.</p>
</Box>
</div>
<div v-if="myFiles && myFiles.length > 0" class="uploaded-files">
<div v-if="myFiles && myFiles.length > 0" class="uploaded-files-container">
<div class="uploaded-files">
<span v-for="file in myFiles" :key="file.id"><VideoComposable :video="file" delete/></span>
</div>
</div>
<p v-else-if="isLoading" class="none"><Icon icon="fa-spinner" spin-pulse/> Chargement des fichiers...</p>
<p v-else class="none"><Icon icon="fa-circle-xmark"/> Aucun fichier enregistré</p>
<Modal icon="fa-upload" title="Uploader un fichier" ref="uploadModal">
<p>Etes-vous sûr de vouloir uploader ce fichier ?</p>
<p class="text-secondary">Ce fichier sera stocké sur le CDN de Discord et sera à jamais accessible. Ne diffusez rien de sensible.</p>
<p v-if="fileSelected" class="upload-modal-name"><Icon icon="fa-file"/> {{ fileSelected.name }}</p>
<p>Etes-vous sûr de vouloir uploader ces fichiers ?</p>
<p class="text-secondary">Ces fichiers seront stockés sur le CDN de Discord et seront à jamais accessibles. Ne diffusez rien de sensible.</p>
<p v-if="fileSelected.length > 0" v-for="value in fileSelected" class="upload-modal-name"><Icon icon="fa-file"/> {{ value.name }}</p>
<div class="upload-actions">
<Button @click="closeModal()">Annuler</Button>
<Button @click="confirmUpload()">Confirmer</Button>
@@ -41,7 +43,7 @@ import { IORequest } from '@/utils/IORequest';
import { onMounted, onUnmounted, ref } from 'vue';
import Events from '@/utils/Events';
const fileSelected = ref(null);
const fileSelected = ref([]);
const status = ref(false);
const uploadModal = ref(null);
const myFiles = ref([]);
@@ -66,53 +68,68 @@ function uploadFile() {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.mp3,.wav,.ogg'; // Accept audio files
input.multiple = true;
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
fileSelected.value = file;
// Here you would typically handle the file upload to the server
console.log(`File selected: ${file.name}`);
// Reset the input for future uploads
const files = Array.from(event.target.files);
if (files.length > 0) {
fileSelected.value = files;
console.log(`Files selected:`, files.map(f => f.name));
input.value = '';
// destroy input
input.remove();
uploadModal.value.open();
} else {
fileSelected.value = null;
fileSelected.value = [];
}
};
input.click();
}
function confirmUpload() {
console.log(`Uploading file: ${fileSelected.value.name}`);
async function confirmUpload() {
if (fileSelected.value.length === 0) return;
status.value = 'download';
uploadModal.value.close();
if(fileSelected.value) {
// Send the file to the server
const reader = new FileReader();
reader.onload = () => {
const fileBuffer = reader.result;
// If it's higher than 300mb
let errorOccurred = false;
for (const file of fileSelected.value) {
try {
const fileBuffer = await readFileAsArrayBuffer(file);
if (fileBuffer.byteLength > 300 * 1024 * 1024) {
status.value = 'toohigh';
console.error('File is too large');
return;
errorOccurred = true;
continue;
}
await new Promise(resolve => {
IORequest('/UPLOAD/FILE', (response) => {
if(!response) {
status.value = 'error';
} else if(response === "TOOHIGH") {
status.value = 'toohigh';
} else {
status.value = 'success';
if (!response || response === "TOOHIGH") {
errorOccurred = true;
status.value = (response === "TOOHIGH") ? 'toohigh' : 'error';
}
resolve();
}, { name: file.name, file: fileBuffer });
});
} catch (err) {
console.error("Erreur upload fichier:", file.name, err);
errorOccurred = true;
}
}
status.value = errorOccurred ? status.value : 'success';
refreshUploadedFiles();
}, {name: fileSelected.value.name, file: fileBuffer})
fileSelected.value = null;
};
reader.readAsArrayBuffer(fileSelected.value);
fileSelected.value = [];
}
// Petite fonction utilitaire pour transformer FileReader en promesse
function readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsArrayBuffer(file);
});
}
@@ -149,6 +166,17 @@ function refreshUploadedFiles() {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
position: absolute;
width: 100%;
}
.uploaded-files-container {
position: relative;
flex: 1;
display: flex;
width: 100%;
height: 100%;
overflow-y: auto;
}
.none {

View File

@@ -1,10 +1,11 @@
<script setup>
import DefaultSplash from '@/components/UI/DefaultSplash.vue';
import MusicAnimation from '@/components/UI/MusicAnimation.vue';
import { version } from '@/../package.json';
const defaultMessage = "On s'accorde et on prépare le concert !";
const connectMsg = "Erreur de connexion au serveur : xhr poll error"
const props = defineProps({
interuptionMessage: {
type: String,
@@ -26,6 +27,7 @@ const props = defineProps({
<p v-if="interuptionMessage" class="error"><Icon icon="fa-solid fa-circle-xmark"/> {{ interuptionMessage }}</p>
<p v-else>{{ defaultMessage }}</p>
<MusicAnimation />
<p class="version">Version : {{ version }} - Chopin</p>
</DefaultSplash>
</template>
<style scoped>
@@ -63,6 +65,12 @@ const props = defineProps({
color: var(--text-secondary);
}
.version {
font-size: 0.6em;
margin-bottom: 0;
color: var(--text-secondary);
}
@media screen and (max-width: 768px),
screen and (max-height: 607px) {
h1 {

View File

@@ -12,6 +12,7 @@ import { IOListener, IORequest } from '@/utils/IORequest';
import { useUserStore } from '@/stores/userStore';
import { useGlobalStore } from '@/stores/globalStore';
import events from '@/utils/Events.js';
import Events from '@/utils/Events.js';
const router = useRouter();
const interuptionMessage = ref(null);
@@ -43,6 +44,7 @@ import events from '@/utils/Events.js';
IORequest("/GUILD/LIST", (response) => {
if(response) {
userStore.userInfo.guilds = response;
Events.emit("GUILD_LIST_UPDATE", response);
}
})
})

View File

@@ -10,7 +10,7 @@
</div>
<br>
<router-link class="no-decoration" to="/">
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
<Button icon="fa-solid fa-house"> Revenir au concert</Button>
</router-link>
</DefaultSplash>

View File

@@ -103,7 +103,7 @@ onMounted(() => {
</Box>
<br/>
<router-link class="no-decoration" to="/">
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
<Button icon="fa-solid fa-house"> Revenir au concert</Button>
</router-link>
<!-- Add more content here as needed -->
</DefaultSplash>

View File

@@ -61,7 +61,7 @@ function inviteSubsonics() {
</div>
</div>
<Button :disabled="!hasLink" @click="inviteSubsonics()"><Icon icon="fa-solid fa-user-plus"/>Inviter Subsonics</Button>
<Button :disabled="!hasLink" @click="inviteSubsonics()" icon="fa-solid fa-user-plus">Inviter Subsonics</Button>
</div>
</Box>
<Account class="full"/>

View File

@@ -31,7 +31,7 @@ onMounted(() => {
</Box>
<br>
<router-link class="no-decoration" to="/">
<Button><Icon icon="fa-solid fa-house"/> Revenir au concert</Button>
<Button icon="fa-solid fa-house">Revenir au concert</Button>
</router-link>
<!-- Add more content here as needed -->
</DefaultSplash>

View File

@@ -3,8 +3,8 @@ import vue from '@vitejs/plugin-vue'
import path from 'path'
import VitePluginVueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
base: '/', // <-- IMPORTANT : chemins des assets corrects
plugins: [vue(), VitePluginVueDevTools()],
resolve: {
alias: {