Compare commits
10 commits
0a980603a4
...
6b80e242b8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b80e242b8 | ||
|
|
dfb8d1038b | ||
|
|
95a8bf99cb | ||
|
|
378af9ac96 | ||
|
|
4669f03f16 | ||
|
|
a57b0c203a | ||
|
|
86db1f5a0c | ||
|
|
2791bc4462 | ||
|
|
bb71da081b | ||
|
|
e73e25b1da |
11 changed files with 437 additions and 209 deletions
2
public/.user.ini
Normal file
2
public/.user.ini
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
; Augmentation temporaire de la limite mémoire pour le chargement des notifications
|
||||||
|
memory_limit = 512M
|
||||||
|
|
@ -3,7 +3,9 @@
|
||||||
// file.update:after
|
// file.update:after
|
||||||
return function ($newFile, $oldFile) {
|
return function ($newFile, $oldFile) {
|
||||||
$project = $newFile->parent()->template() == 'project' ? $newFile->parent() : $newFile->parent()->parents()->findBy('template', 'project');
|
$project = $newFile->parent()->template() == 'project' ? $newFile->parent() : $newFile->parent()->parents()->findBy('template', 'project');
|
||||||
if ($project) {
|
if ($project) {
|
||||||
$steps = $project->rebuildStepsCache();
|
$project->rebuildStepsCache();
|
||||||
|
// Invalider aussi le cache des notifications (commentaires sur fichiers, etc.)
|
||||||
|
$project->invalidateNotificationsCache();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
// page.update:after && page.changeStatus:after
|
// page.update:after && page.changeStatus:after
|
||||||
return function($newPage, $oldPage) {
|
return function($newPage, $oldPage) {
|
||||||
$project = $newPage->template() == 'project' ? $newPage : $newPage->parents()->findBy('template', 'project');
|
$project = $newPage->template() == 'project' ? $newPage : $newPage->parents()->findBy('template', 'project');
|
||||||
if ($project) {
|
if ($project) {
|
||||||
$steps = $project->rebuildStepsCache();
|
$project->rebuildStepsCache();
|
||||||
|
// Invalider aussi le cache des notifications (briefs validés, etc.)
|
||||||
|
$project->invalidateNotificationsCache();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -3,18 +3,62 @@
|
||||||
use adrienpayet\notifications\NotificationsPage;
|
use adrienpayet\notifications\NotificationsPage;
|
||||||
|
|
||||||
class ProjectPage extends NotificationsPage {
|
class ProjectPage extends NotificationsPage {
|
||||||
public function getSteps() {
|
public function getSteps() {
|
||||||
$apiCache = kirby()->cache('api');
|
$apiCache = kirby()->cache('api');
|
||||||
$stepsData = $apiCache?->get($this->slug() . '_' . 'steps');
|
$stepsData = $apiCache?->get($this->slug() . '_' . 'steps');
|
||||||
|
|
||||||
if ($stepsData === null || count($stepsData) === 0) {
|
if ($stepsData === null || count($stepsData) === 0) {
|
||||||
$this->rebuildStepsCache();
|
$this->rebuildStepsCache();
|
||||||
};
|
};
|
||||||
|
|
||||||
$stepsData = $apiCache->get($this->slug() . '_' . 'steps');
|
$stepsData = $apiCache->get($this->slug() . '_' . 'steps');
|
||||||
|
|
||||||
return $stepsData;
|
return $stepsData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère les notifications pour ce projet (version allégée avec cache).
|
||||||
|
* Cache par utilisateur pour inclure le isRead.
|
||||||
|
*/
|
||||||
|
public function getNotificationsLight($user) {
|
||||||
|
if (!$user) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$apiCache = kirby()->cache('api');
|
||||||
|
$cacheKey = $this->slug() . '_notifications_' . $user->uuid();
|
||||||
|
$notifications = $apiCache?->get($cacheKey);
|
||||||
|
|
||||||
|
// Si pas en cache, collecter et cacher
|
||||||
|
if ($notifications === null) {
|
||||||
|
$collector = kirby()->option('adrienpayet.pdc-notifications.collector');
|
||||||
|
if (!$collector) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$notifications = $collector->collectLight($this, $user);
|
||||||
|
$apiCache->set($cacheKey, $notifications);
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log("Error caching notifications for {$this->slug()}: " . $e->getMessage());
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $notifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalide le cache des notifications de ce projet pour tous les utilisateurs.
|
||||||
|
*/
|
||||||
|
public function invalidateNotificationsCache() {
|
||||||
|
$apiCache = kirby()->cache('api');
|
||||||
|
// Invalider pour tous les users
|
||||||
|
foreach (kirby()->users() as $user) {
|
||||||
|
$cacheKey = $this->slug() . '_notifications_' . $user->uuid();
|
||||||
|
$apiCache->remove($cacheKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public function rebuildStepsCache() {
|
public function rebuildStepsCache() {
|
||||||
// Create steps
|
// Create steps
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ class NotificationCollector
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collecte uniquement les données minimales des notifications (version allégée pour listing).
|
* Collecte uniquement les données minimales des notifications (version allégée pour listing).
|
||||||
* Retourne seulement id, type, isRead, date pour économiser la mémoire.
|
* Retourne les champs nécessaires à l'affichage mais sans les détails lourds.
|
||||||
*
|
*
|
||||||
* @param Page $project Le projet à scanner
|
* @param Page $project Le projet à scanner
|
||||||
* @param User $user L'utilisateur courant
|
* @param User $user L'utilisateur courant
|
||||||
|
|
@ -72,18 +72,33 @@ class NotificationCollector
|
||||||
foreach ($this->providers as $provider) {
|
foreach ($this->providers as $provider) {
|
||||||
try {
|
try {
|
||||||
$notifications = $provider->collect($project, $user);
|
$notifications = $provider->collect($project, $user);
|
||||||
// Ne garder que les champs essentiels
|
// Garder les champs nécessaires au frontend
|
||||||
foreach ($notifications as $notification) {
|
foreach ($notifications as $notification) {
|
||||||
$all[] = [
|
$light = [
|
||||||
'id' => $notification['id'] ?? null,
|
'id' => $notification['id'] ?? null,
|
||||||
'type' => $notification['type'] ?? null,
|
'type' => $notification['type'] ?? null,
|
||||||
'isRead' => $notification['isRead'] ?? false,
|
'isRead' => $notification['isRead'] ?? false,
|
||||||
'date' => $notification['date'] ?? null,
|
'date' => $notification['date'] ?? null,
|
||||||
// Garder location.project.uri pour le frontend
|
'text' => $notification['text'] ?? null,
|
||||||
'location' => [
|
'author' => $notification['author'] ?? null,
|
||||||
'project' => $notification['location']['project'] ?? []
|
'location' => $notification['location'] ?? []
|
||||||
]
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Garder les champs optionnels s'ils existent
|
||||||
|
if (isset($notification['dialogUri'])) {
|
||||||
|
$light['dialogUri'] = $notification['dialogUri'];
|
||||||
|
}
|
||||||
|
if (isset($notification['_briefUri'])) {
|
||||||
|
$light['_briefUri'] = $notification['_briefUri'];
|
||||||
|
}
|
||||||
|
if (isset($notification['_file'])) {
|
||||||
|
$light['_file'] = $notification['_file'];
|
||||||
|
}
|
||||||
|
if (isset($notification['_projectUri'])) {
|
||||||
|
$light['_projectUri'] = $notification['_projectUri'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$all[] = $light;
|
||||||
}
|
}
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
error_log("NotificationCollector: Error in {$provider->getType()}: " . $e->getMessage());
|
error_log("NotificationCollector: Error in {$provider->getType()}: " . $e->getMessage());
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
(function(){"use strict";function f(n,e,a,t,r,c,s,u){var o=typeof n=="function"?n.options:n;return e&&(o.render=e,o.staticRenderFns=a,o._compiled=!0),{exports:n,options:o}}const l={__name:"RefreshCacheButton",props:{pageUri:String,pageStatus:String,lastCacheUpdate:String},setup(n){const{pageUri:e,pageStatus:a,lastCacheUpdate:t}=n,r=Vue.ref("Rafraîchir"),c=Vue.ref("refresh"),s=Vue.ref("aqua-icon"),u=Vue.computed(()=>(t==null?void 0:t.length)>0?"Dernière mise à jour : "+t:"Mettre à jour le cache front");async function o(){r.value="En cours…",c.value="loader",s.value="orange-icon";const m={method:"POST","Content-Type":"application/json",body:JSON.stringify({pageUri:e})},i=await(await fetch("/refresh-cache.json",m)).json();i.status==="error"?(console.error(i),r.value="Erreur",c.value="alert",s.value="red-icon"):(console.log(i),r.value="Terminé",c.value="check",s.value="green-icon",setTimeout(()=>{location.href=location.href},1500))}return{__sfc:!0,text:r,icon:c,theme:s,title:u,refreshCache:o}}};var h=function(){var e=this,a=e._self._c,t=e._self._setupProxy;return a("div",{attrs:{id:"refresh-cache-button"}},[e.pageStatus!=="draft"?a("k-button",{attrs:{theme:t.theme,variant:"dimmed",icon:t.icon,title:t.title},on:{click:function(r){return t.refreshCache()}}},[e._v(e._s(t.text))]):e._e()],1)},_=[],p=f(l,h,_);const d=p.exports;window.panel.plugin("adrienpayet/refresh-cache-button",{components:{"refresh-cache-button":d}})})();
|
(function(){"use strict";function _(n,e,u,t,r,s,a,l){var c=typeof n=="function"?n.options:n;return e&&(c.render=e,c.staticRenderFns=u,c._compiled=!0),{exports:n,options:c}}const g={__name:"RefreshCacheButton",props:{pageUri:String,pageStatus:String,lastCacheUpdate:String},setup(n){const{pageUri:e,pageStatus:u,lastCacheUpdate:t}=n,r=Vue.ref("Rafraîchir"),s=Vue.ref("refresh"),a=Vue.ref("aqua-icon"),l=Vue.ref(!1),c=Vue.computed(()=>(t==null?void 0:t.length)>0?"Dernière mise à jour : "+t:"Mettre à jour le cache front");async function T(){l.value=!0,s.value="loader",a.value="orange-icon",e==="projects"?await d():await v()}async function d(){let f=0;const h=10;let i=!0,b=0;r.value="En cours 0%";try{for(;i;){const p={method:"POST","Content-Type":"application/json",body:JSON.stringify({pageUri:"projects",offset:f,limit:h})},o=await(await fetch("/refresh-cache.json",p)).json();if(o.status==="error")throw new Error(o.message);b=o.total,i=o.hasMore,f=o.nextOffset;const m=Math.round(o.processed/o.total*100);r.value=`En cours ${m}%`,console.log(`Batch terminé : ${o.processed}/${o.total} projets (${m}%)`)}r.value="Terminé",s.value="check",a.value="green-icon",setTimeout(()=>{location.href=location.href},2e3)}catch(p){console.error(p),r.value="Erreur",s.value="alert",a.value="red-icon",l.value=!1}}async function v(){r.value="En cours…";const f={method:"POST","Content-Type":"application/json",body:JSON.stringify({pageUri:e})};try{const i=await(await fetch("/refresh-cache.json",f)).json();if(i.status==="error")throw new Error(i.message);console.log(i),r.value="Terminé",s.value="check",a.value="green-icon",setTimeout(()=>{location.href=location.href},1500)}catch(h){console.error(h),r.value="Erreur",s.value="alert",a.value="red-icon",l.value=!1}}return{__sfc:!0,text:r,icon:s,theme:a,isProcessing:l,title:c,refreshCache:T,refreshAllProjects:d,refreshSingleProject:v}}};var j=function(){var e=this,u=e._self._c,t=e._self._setupProxy;return u("div",{attrs:{id:"refresh-cache-button"}},[e.pageStatus!=="draft"?u("k-button",{attrs:{theme:t.theme,variant:"dimmed",icon:t.icon,title:t.title,disabled:t.isProcessing},on:{click:function(r){return t.refreshCache()}}},[e._v(e._s(t.text))]):e._e()],1)},y=[],w=_(g,j,y);const S=w.exports;window.panel.plugin("adrienpayet/refresh-cache-button",{components:{"refresh-cache-button":S}})})();
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
:icon="icon"
|
:icon="icon"
|
||||||
:title="title"
|
:title="title"
|
||||||
@click="refreshCache()"
|
@click="refreshCache()"
|
||||||
|
:disabled="isProcessing"
|
||||||
>{{ text }}</k-button
|
>{{ text }}</k-button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -24,6 +25,8 @@ const { pageUri, pageStatus, lastCacheUpdate } = defineProps({
|
||||||
const text = ref("Rafraîchir");
|
const text = ref("Rafraîchir");
|
||||||
const icon = ref("refresh");
|
const icon = ref("refresh");
|
||||||
const theme = ref("aqua-icon");
|
const theme = ref("aqua-icon");
|
||||||
|
const isProcessing = ref(false);
|
||||||
|
|
||||||
const title = computed(() => {
|
const title = computed(() => {
|
||||||
return lastCacheUpdate?.length > 0
|
return lastCacheUpdate?.length > 0
|
||||||
? "Dernière mise à jour : " + lastCacheUpdate
|
? "Dernière mise à jour : " + lastCacheUpdate
|
||||||
|
|
@ -31,25 +34,91 @@ const title = computed(() => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function refreshCache() {
|
async function refreshCache() {
|
||||||
text.value = "En cours…";
|
isProcessing.value = true;
|
||||||
icon.value = "loader";
|
icon.value = "loader";
|
||||||
theme.value = "orange-icon";
|
theme.value = "orange-icon";
|
||||||
|
|
||||||
|
// Pour les projets multiples (batch processing)
|
||||||
|
if (pageUri === 'projects') {
|
||||||
|
await refreshAllProjects();
|
||||||
|
} else {
|
||||||
|
await refreshSingleProject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshAllProjects() {
|
||||||
|
let offset = 0;
|
||||||
|
const limit = 10; // 10 projets par batch
|
||||||
|
let hasMore = true;
|
||||||
|
let total = 0;
|
||||||
|
|
||||||
|
text.value = "En cours 0%";
|
||||||
|
|
||||||
|
try {
|
||||||
|
while (hasMore) {
|
||||||
|
const init = {
|
||||||
|
method: "POST",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
body: JSON.stringify({
|
||||||
|
pageUri: 'projects',
|
||||||
|
offset,
|
||||||
|
limit
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch("/refresh-cache.json", init);
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.status === "error") {
|
||||||
|
throw new Error(json.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
total = json.total;
|
||||||
|
hasMore = json.hasMore;
|
||||||
|
offset = json.nextOffset;
|
||||||
|
|
||||||
|
// Mise à jour de la progression dans le texte du bouton
|
||||||
|
const progress = Math.round((json.processed / json.total) * 100);
|
||||||
|
text.value = `En cours ${progress}%`;
|
||||||
|
|
||||||
|
console.log(`Batch terminé : ${json.processed}/${json.total} projets (${progress}%)`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Succès
|
||||||
|
text.value = "Terminé";
|
||||||
|
icon.value = "check";
|
||||||
|
theme.value = "green-icon";
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
location.href = location.href;
|
||||||
|
}, 2000);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
text.value = "Erreur";
|
||||||
|
icon.value = "alert";
|
||||||
|
theme.value = "red-icon";
|
||||||
|
isProcessing.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshSingleProject() {
|
||||||
|
text.value = "En cours…";
|
||||||
|
|
||||||
const init = {
|
const init = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
body: JSON.stringify({ pageUri }),
|
body: JSON.stringify({ pageUri }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = await fetch("/refresh-cache.json", init);
|
try {
|
||||||
const json = await res.json();
|
const res = await fetch("/refresh-cache.json", init);
|
||||||
|
const json = await res.json();
|
||||||
|
|
||||||
|
if (json.status === "error") {
|
||||||
|
throw new Error(json.message);
|
||||||
|
}
|
||||||
|
|
||||||
if (json.status === "error") {
|
|
||||||
console.error(json);
|
|
||||||
text.value = "Erreur";
|
|
||||||
icon.value = "alert";
|
|
||||||
theme.value = "red-icon";
|
|
||||||
} else {
|
|
||||||
console.log(json);
|
console.log(json);
|
||||||
text.value = "Terminé";
|
text.value = "Terminé";
|
||||||
icon.value = "check";
|
icon.value = "check";
|
||||||
|
|
@ -58,6 +127,13 @@ async function refreshCache() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
location.href = location.href;
|
location.href = location.href;
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
text.value = "Erreur";
|
||||||
|
icon.value = "alert";
|
||||||
|
theme.value = "red-icon";
|
||||||
|
isProcessing.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
set_time_limit(0);
|
set_time_limit(60);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'pattern' => '/refresh-cache.json',
|
'pattern' => '/refresh-cache.json',
|
||||||
|
|
@ -10,17 +10,42 @@ return [
|
||||||
|
|
||||||
if ($data->pageUri === 'projects') {
|
if ($data->pageUri === 'projects') {
|
||||||
$projects = page('projects')->children();
|
$projects = page('projects')->children();
|
||||||
foreach ($projects as $project) {
|
|
||||||
$project->rebuildStepsCache();
|
|
||||||
|
|
||||||
$formatter = new IntlDateFormatter('fr_FR', IntlDateFormatter::SHORT, IntlDateFormatter::SHORT, 'Europe/Paris');
|
// Support du batch processing
|
||||||
$project->update([
|
$offset = isset($data->offset) ? intval($data->offset) : 0;
|
||||||
'lastCacheUpdate' => $formatter->format(time())
|
$limit = isset($data->limit) ? intval($data->limit) : 10; // 10 projets par batch par défaut
|
||||||
]);
|
$total = $projects->count();
|
||||||
|
|
||||||
|
// Slice pour ne traiter qu'un batch
|
||||||
|
$batch = $projects->slice($offset, $limit);
|
||||||
|
$processed = 0;
|
||||||
|
|
||||||
|
foreach ($batch as $project) {
|
||||||
|
try {
|
||||||
|
$project->rebuildStepsCache();
|
||||||
|
$project->invalidateNotificationsCache();
|
||||||
|
|
||||||
|
$formatter = new IntlDateFormatter('fr_FR', IntlDateFormatter::SHORT, IntlDateFormatter::SHORT, 'Europe/Paris');
|
||||||
|
$project->update([
|
||||||
|
'lastCacheUpdate' => $formatter->format(time())
|
||||||
|
]);
|
||||||
|
$processed++;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
error_log("Error refreshing cache for project {$project->slug()}: " . $e->getMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$remaining = max(0, $total - ($offset + $processed));
|
||||||
|
$hasMore = $remaining > 0;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'satus' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Données des pages projets rafraîchies avec succès.'
|
'message' => "Batch terminé : $processed projets traités.",
|
||||||
|
'processed' => $offset + $processed,
|
||||||
|
'total' => $total,
|
||||||
|
'remaining' => $remaining,
|
||||||
|
'hasMore' => $hasMore,
|
||||||
|
'nextOffset' => $hasMore ? $offset + $limit : null
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
|
|
@ -41,7 +66,7 @@ return [
|
||||||
|
|
||||||
if (!$project) {
|
if (!$project) {
|
||||||
return [
|
return [
|
||||||
'satus' => 'error',
|
'status' => 'error',
|
||||||
'message' => 'Impossible de rafraîchir les données de la page ' . $data->pageUri . '. Aucun projet correspondant.'
|
'message' => 'Impossible de rafraîchir les données de la page ' . $data->pageUri . '. Aucun projet correspondant.'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +80,7 @@ return [
|
||||||
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'satus' => 'success',
|
'status' => 'success',
|
||||||
'message' => 'Données de la page ' . $data->pageUri . ' rafraîchie avec succès.'
|
'message' => 'Données de la page ' . $data->pageUri . ' rafraîchie avec succès.'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,15 @@ if (!$kirby->user()) {
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProjectData($project, $user, $collector)
|
function getProjectData($project, $user)
|
||||||
{
|
{
|
||||||
// Utiliser collectLight() pour économiser la mémoire (seulement id, type, isRead, date)
|
// Utiliser getNotificationsLight() avec cache pour optimiser les performances
|
||||||
$notifications = [];
|
$notifications = [];
|
||||||
if ($collector) {
|
try {
|
||||||
try {
|
$notifications = $project->getNotificationsLight($user);
|
||||||
$notifications = $collector->collectLight($project, $user);
|
} catch (\Throwable $e) {
|
||||||
} catch (\Throwable $e) {
|
error_log("Error getting notifications for project {$project->uri()}: " . $e->getMessage());
|
||||||
error_log("Error collecting light notifications for project {$project->uri()}: " . $e->getMessage());
|
$notifications = [];
|
||||||
$notifications = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
|
|
@ -43,14 +41,12 @@ function getProjectData($project, $user, $collector)
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer le collector de notifications
|
|
||||||
$notificationCollector = $kirby->option('adrienpayet.pdc-notifications.collector');
|
|
||||||
$currentUser = $kirby->user();
|
$currentUser = $kirby->user();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$children = $currentUser->role() == 'admin'
|
$children = $currentUser->role() == 'admin'
|
||||||
? $page->childrenAndDrafts()->map(fn($project) => getProjectData($project, $currentUser, $notificationCollector))->values()
|
? $page->childrenAndDrafts()->map(fn($project) => getProjectData($project, $currentUser))->values()
|
||||||
: $currentUser->projects()->toPages()->map(fn($project) => getProjectData($project, $currentUser, $notificationCollector))->values();
|
: $currentUser->projects()->toPages()->map(fn($project) => getProjectData($project, $currentUser))->values();
|
||||||
} catch (\Throwable $th) {
|
} catch (\Throwable $th) {
|
||||||
throw new Exception($th->getMessage() . ' line ' . $th->getLine() . ' in file ' . $th->getFile(), 1);
|
throw new Exception($th->getMessage() . ' line ' . $th->getLine() . ' in file ' . $th->getFile(), 1);
|
||||||
$children = [];
|
$children = [];
|
||||||
|
|
|
||||||
|
|
@ -76,112 +76,149 @@ const { items, label, isCompareModeEnabled, index } = defineProps({
|
||||||
|
|
||||||
// Local state
|
// Local state
|
||||||
const currentValue = ref(null);
|
const currentValue = ref(null);
|
||||||
const syncing = ref(false); // empêche les réactions pendant les mises à jour programmatiques
|
const syncing = ref(false);
|
||||||
|
|
||||||
// Store
|
|
||||||
const { activeTracks } = storeToRefs(useDialogStore());
|
const { activeTracks } = storeToRefs(useDialogStore());
|
||||||
|
|
||||||
// Utils
|
function normalizeSlug(slug) {
|
||||||
function isSame(a, b) {
|
return slug.replace(/_/g, '-');
|
||||||
if (!a || !b) return false;
|
|
||||||
if (a.slug && b.slug) return a.slug === b.slug;
|
|
||||||
return a.title === b.title;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toVariation(v) {
|
function areVariationsEqual(variationA, variationB) {
|
||||||
if (!v) return null;
|
if (!variationA || !variationB) return false;
|
||||||
return Array.isArray(v) ? v[v.length - 1] || null : v;
|
|
||||||
|
if (variationA.slug && variationB.slug) {
|
||||||
|
return normalizeSlug(variationA.slug) === normalizeSlug(variationB.slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
return variationA.title === variationB.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialisation : remplir le 1er select localement ET initialiser le store
|
function extractVariation(value) {
|
||||||
onBeforeMount(() => {
|
if (!value) return null;
|
||||||
|
return Array.isArray(value) ? value[value.length - 1] || null : value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertValueForCompareMode(value, shouldBeArray) {
|
||||||
|
if (shouldBeArray) {
|
||||||
|
return value && !Array.isArray(value) ? [value] : value;
|
||||||
|
} else {
|
||||||
|
return Array.isArray(value) ? value[0] || null : value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findMatchingVariationsInStore(storeVariations) {
|
||||||
|
return storeVariations.filter((storeVar) =>
|
||||||
|
items.some((item) => areVariationsEqual(item, storeVar))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncCurrentValueFromStore(storeVariations) {
|
||||||
syncing.value = true;
|
syncing.value = true;
|
||||||
|
|
||||||
if (index === 0) {
|
const matchedVariations = findMatchingVariationsInStore(storeVariations);
|
||||||
currentValue.value = items[0] || null;
|
|
||||||
// si le store est vide, initialiser avec la variation du premier sélecteur
|
if (isCompareModeEnabled) {
|
||||||
if (!activeTracks.value || activeTracks.value.length === 0) {
|
currentValue.value = matchedVariations.length ? [...matchedVariations] : [];
|
||||||
const v = toVariation(items[0]);
|
|
||||||
if (v) activeTracks.value = [v];
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// les autres ne forcent pas le store ; leur currentValue restera à null
|
currentValue.value = matchedVariations[0] || null;
|
||||||
currentValue.value = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
nextTick(() => (syncing.value = false));
|
nextTick(() => (syncing.value = false));
|
||||||
});
|
}
|
||||||
|
|
||||||
|
function detectVariationChanges(newValues, oldValues) {
|
||||||
|
const newList = Array.isArray(newValues)
|
||||||
|
? newValues
|
||||||
|
: newValues
|
||||||
|
? [newValues]
|
||||||
|
: [];
|
||||||
|
const oldList = Array.isArray(oldValues)
|
||||||
|
? oldValues
|
||||||
|
: oldValues
|
||||||
|
? [oldValues]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const addedVariation = newList.find(
|
||||||
|
(n) => !oldList.some((o) => areVariationsEqual(o, n))
|
||||||
|
);
|
||||||
|
const removedVariation = oldList.find(
|
||||||
|
(o) => !newList.some((n) => areVariationsEqual(n, o))
|
||||||
|
);
|
||||||
|
|
||||||
|
return { addedVariation, removedVariation };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleVariationChange(newValue, oldValue) {
|
||||||
|
if (syncing.value) return;
|
||||||
|
|
||||||
|
const { addedVariation, removedVariation } = detectVariationChanges(
|
||||||
|
newValue,
|
||||||
|
oldValue
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
addedVariation &&
|
||||||
|
items.some((item) => areVariationsEqual(item, addedVariation))
|
||||||
|
) {
|
||||||
|
updateActiveTracks(addedVariation, 'add');
|
||||||
|
} else if (
|
||||||
|
removedVariation &&
|
||||||
|
items.some((item) => areVariationsEqual(item, removedVariation))
|
||||||
|
) {
|
||||||
|
updateActiveTracks(removedVariation, 'remove');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Quand on bascule compare mode (objet <-> tableau)
|
|
||||||
watch(
|
watch(
|
||||||
() => isCompareModeEnabled,
|
() => isCompareModeEnabled,
|
||||||
(flag) => {
|
(shouldBeArray) => {
|
||||||
syncing.value = true;
|
syncing.value = true;
|
||||||
if (flag) {
|
currentValue.value = convertValueForCompareMode(
|
||||||
if (currentValue.value && !Array.isArray(currentValue.value)) {
|
currentValue.value,
|
||||||
currentValue.value = [currentValue.value];
|
shouldBeArray
|
||||||
}
|
);
|
||||||
} else {
|
|
||||||
if (Array.isArray(currentValue.value)) {
|
|
||||||
currentValue.value = currentValue.value[0] || null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
nextTick(() => (syncing.value = false));
|
nextTick(() => (syncing.value = false));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Détection ajout / suppression dans le MultiSelect (côté composant)
|
watch(currentValue, handleVariationChange, { deep: true });
|
||||||
// On n'agit que si l'ajout/suppression concerne une variation appartenant à `items`
|
|
||||||
watch(
|
|
||||||
currentValue,
|
|
||||||
(newVal, oldVal) => {
|
|
||||||
if (syncing.value) return;
|
|
||||||
|
|
||||||
const newItems = Array.isArray(newVal) ? newVal : newVal ? [newVal] : [];
|
|
||||||
const oldItems = Array.isArray(oldVal) ? oldVal : oldVal ? [oldVal] : [];
|
|
||||||
|
|
||||||
const added = newItems.find((n) => !oldItems.some((o) => isSame(o, n)));
|
|
||||||
const removed = oldItems.find((o) => !newItems.some((n) => isSame(n, o)));
|
|
||||||
|
|
||||||
if (added && items.some((it) => isSame(it, added))) {
|
|
||||||
selectTrack(added, 'add');
|
|
||||||
} else if (removed && items.some((it) => isSame(it, removed))) {
|
|
||||||
selectTrack(removed, 'remove');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ deep: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// Quand activeTracks change elsewhere -> synchroniser l'affichage local
|
|
||||||
// Mais n'adopter que les variations qui appartiennent à ce Selector (`items`)
|
|
||||||
watch(
|
watch(
|
||||||
activeTracks,
|
activeTracks,
|
||||||
(newVal) => {
|
(storeVariations) => {
|
||||||
syncing.value = true;
|
const variationsList = Array.isArray(storeVariations)
|
||||||
|
? storeVariations
|
||||||
const storeList = Array.isArray(newVal) ? newVal : [];
|
: [];
|
||||||
// ne garder que les variations du store qui sont dans `items`
|
syncCurrentValueFromStore(variationsList);
|
||||||
const matched = storeList.filter((av) =>
|
|
||||||
items.some((it) => isSame(it, av))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isCompareModeEnabled) {
|
|
||||||
currentValue.value = matched.length ? [...matched] : [];
|
|
||||||
} else {
|
|
||||||
currentValue.value = matched[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
nextTick(() => (syncing.value = false));
|
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true, immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
// Logique centrale de sélection (ajout / suppression)
|
function removeVariationFromActiveTracks(variation) {
|
||||||
// Règles :
|
activeTracks.value = activeTracks.value.filter(
|
||||||
// - mode normal -> activeTracks = [variation]
|
(track) => !areVariationsEqual(track, variation)
|
||||||
// - mode comparaison -> conserver activeTracks[0] si possible; second élément ajouté/remplacé; suppression gère le cas de la suppression de la première
|
);
|
||||||
function selectTrack(track, action = 'add') {
|
}
|
||||||
const variation = toVariation(track);
|
|
||||||
|
function addVariationToActiveTracks(variation) {
|
||||||
|
const isAlreadyPresent = activeTracks.value.some((track) =>
|
||||||
|
areVariationsEqual(track, variation)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isAlreadyPresent) return;
|
||||||
|
|
||||||
|
if (activeTracks.value.length === 0) {
|
||||||
|
activeTracks.value = [variation];
|
||||||
|
} else if (activeTracks.value.length === 1) {
|
||||||
|
activeTracks.value = [activeTracks.value[0], variation];
|
||||||
|
} else {
|
||||||
|
activeTracks.value = [activeTracks.value[0], variation];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateActiveTracks(track, action = 'add') {
|
||||||
|
const variation = extractVariation(track);
|
||||||
if (!variation) return;
|
if (!variation) return;
|
||||||
|
|
||||||
if (!isCompareModeEnabled) {
|
if (!isCompareModeEnabled) {
|
||||||
|
|
@ -190,34 +227,12 @@ function selectTrack(track, action = 'add') {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (action === 'remove') {
|
if (action === 'remove') {
|
||||||
const wasFirst =
|
removeVariationFromActiveTracks(variation);
|
||||||
activeTracks.value.length && isSame(activeTracks.value[0], variation);
|
|
||||||
activeTracks.value = activeTracks.value.filter(
|
|
||||||
(t) => !isSame(t, variation)
|
|
||||||
);
|
|
||||||
|
|
||||||
// si on a retiré la première et qu'il reste une piste, elle devient naturellement index 0
|
|
||||||
// pas d'action supplémentaire nécessaire ici (déjà assuré par le filter)
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// action === 'add'
|
|
||||||
if (activeTracks.value.some((t) => isSame(t, variation))) {
|
|
||||||
// déjà présent -> ignore
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeTracks.value.length === 0) {
|
|
||||||
activeTracks.value = [variation];
|
|
||||||
} else if (activeTracks.value.length === 1) {
|
|
||||||
activeTracks.value = [activeTracks.value[0], variation];
|
|
||||||
} else {
|
} else {
|
||||||
// remplacer le 2e
|
addVariationToActiveTracks(variation);
|
||||||
activeTracks.value = [activeTracks.value[0], variation];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers pour affichage (inchangés)
|
|
||||||
function getFrontViewUrl(item) {
|
function getFrontViewUrl(item) {
|
||||||
if (!item) return '';
|
if (!item) return '';
|
||||||
if (Array.isArray(item)) {
|
if (Array.isArray(item)) {
|
||||||
|
|
@ -231,8 +246,8 @@ function getFrontViewUrl(item) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function setImage() {
|
function setImage() {
|
||||||
return getFrontViewUrl(currentValue.value)
|
return getFrontViewUrl(currentValue.value)
|
||||||
? '--image: url(\'' + getFrontViewUrl(currentValue.value) + '\')'
|
? "--image: url('" + getFrontViewUrl(currentValue.value) + "')"
|
||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -250,7 +265,8 @@ function setImage() {
|
||||||
padding: var(--space-8) var(--space-48) var(--space-8) var(--space-16);
|
padding: var(--space-8) var(--space-48) var(--space-8) var(--space-16);
|
||||||
}
|
}
|
||||||
.selector-dropdown.has-image,
|
.selector-dropdown.has-image,
|
||||||
.selector-dropdown.has-image :is(#selector-select, #selector-multiselect, [role='combobox']) {
|
.selector-dropdown.has-image
|
||||||
|
:is(#selector-select, #selector-multiselect, [role='combobox']) {
|
||||||
padding-left: var(--space-64);
|
padding-left: var(--space-64);
|
||||||
}
|
}
|
||||||
.selector-dropdown.has-image:before {
|
.selector-dropdown.has-image:before {
|
||||||
|
|
@ -290,7 +306,9 @@ function setImage() {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
[role='combobox'] p,
|
[role='combobox'] p,
|
||||||
.selector-dropdown [data-pc-section="labelcontainer"] > [data-pc-section='label'] {
|
.selector-dropdown
|
||||||
|
[data-pc-section='labelcontainer']
|
||||||
|
> [data-pc-section='label'] {
|
||||||
max-height: 1lh;
|
max-height: 1lh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
|
||||||
|
|
@ -61,13 +61,14 @@ import { storeToRefs } from 'pinia';
|
||||||
import { usePageStore } from '../../../stores/page';
|
import { usePageStore } from '../../../stores/page';
|
||||||
import { useDialogStore } from '../../../stores/dialog';
|
import { useDialogStore } from '../../../stores/dialog';
|
||||||
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
import { useVirtualSampleStore } from '../../../stores/virtualSample';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import Interactive360 from './Interactive360.vue';
|
import Interactive360 from './Interactive360.vue';
|
||||||
import SingleImage from './SingleImage.vue';
|
import SingleImage from './SingleImage.vue';
|
||||||
import Selector from '../../Selector.vue';
|
import Selector from '../../Selector.vue';
|
||||||
import slugify from 'slugify';
|
import slugify from 'slugify';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const { page } = storeToRefs(usePageStore());
|
const { page } = storeToRefs(usePageStore());
|
||||||
const { isCommentsOpen, isCommentPanelEnabled, activeTracks, openedFile } =
|
const { isCommentsOpen, isCommentPanelEnabled, activeTracks, openedFile } =
|
||||||
|
|
@ -92,41 +93,74 @@ const tracks = computed(() => {
|
||||||
return list;
|
return list;
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- INITIALISATION ----------
|
function normalizeSlug(slug) {
|
||||||
// onBeforeMount : on initialise toujours activeTracks avec une VARIATION (jamais un track)
|
return slug.replace(/_/g, '-');
|
||||||
onBeforeMount(() => {
|
}
|
||||||
// essayer la hash en priorité
|
|
||||||
let initialVariation = null;
|
|
||||||
|
|
||||||
|
function getVariationSlug(variation) {
|
||||||
|
return variation.slug || (variation.title ? slugify(variation.title) : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findVariationByHash(hashValue) {
|
||||||
|
const allVariations = tracks.value.flatMap((track) => track.variations || []);
|
||||||
|
const normalizedHash = normalizeSlug(hashValue);
|
||||||
|
|
||||||
|
return allVariations.find((variation) => {
|
||||||
|
const variationSlug = getVariationSlug(variation);
|
||||||
|
if (!variationSlug) return false;
|
||||||
|
|
||||||
|
const normalizedVariationSlug = normalizeSlug(variationSlug);
|
||||||
|
return normalizedVariationSlug === normalizedHash;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialVariation() {
|
||||||
if (route?.hash && route.hash.length > 0) {
|
if (route?.hash && route.hash.length > 0) {
|
||||||
const variations = tracks.value.flatMap((t) => t.variations || []);
|
const hashValue = route.hash.substring(1);
|
||||||
initialVariation =
|
const variationFromHash = findVariationByHash(hashValue);
|
||||||
variations.find((v) => v.slug === route.hash.substring(1)) || null;
|
if (variationFromHash) return variationFromHash;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fallback : première variation du premier track
|
return tracks.value[0]?.variations?.[0] || null;
|
||||||
if (!initialVariation) {
|
}
|
||||||
initialVariation = tracks.value[0]?.variations?.[0] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (initialVariation) {
|
function initializeActiveTracks() {
|
||||||
activeTracks.value = [initialVariation];
|
const initialVariation = getInitialVariation();
|
||||||
} else {
|
activeTracks.value = initialVariation ? [initialVariation] : [];
|
||||||
activeTracks.value = []; // aucun contenu disponible
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// scroll si hash présent
|
function normalizeUrlHash() {
|
||||||
onMounted(() => {
|
if (route?.hash && route.hash.includes('_')) {
|
||||||
if (route.query?.comments) isCommentsOpen.value = true;
|
const normalizedHash = normalizeSlug(route.hash);
|
||||||
|
router.replace({ ...route, hash: normalizedHash });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCommentsIfRequested() {
|
||||||
|
if (route.query?.comments) {
|
||||||
|
isCommentsOpen.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollToHashTarget() {
|
||||||
if (!route?.hash || route.hash.length === 0) return;
|
if (!route?.hash || route.hash.length === 0) return;
|
||||||
|
|
||||||
const selector = route.hash.replace('#', '#track--');
|
const selectorId = route.hash.replace('#', '#track--');
|
||||||
const targetBtn = document.querySelector(selector);
|
const targetButton = document.querySelector(selectorId);
|
||||||
if (targetBtn) targetBtn.scrollIntoView();
|
if (targetButton) {
|
||||||
|
targetButton.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeMount(() => {
|
||||||
|
initializeActiveTracks();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- COMPUTED / WATCH ----------
|
onMounted(() => {
|
||||||
|
openCommentsIfRequested();
|
||||||
|
normalizeUrlHash();
|
||||||
|
scrollToHashTarget();
|
||||||
|
});
|
||||||
|
|
||||||
const isSingleImage = computed(() => {
|
const isSingleImage = computed(() => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -139,38 +173,52 @@ const singleFile = computed(() => {
|
||||||
return isSingleImage.value ? activeTracks.value[0].files[0] : null;
|
return isSingleImage.value ? activeTracks.value[0].files[0] : null;
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
function updateOpenedFile(file) {
|
||||||
singleFile,
|
if (file) {
|
||||||
(newValue) => {
|
openedFile.value = file;
|
||||||
if (newValue) openedFile.value = newValue;
|
|
||||||
},
|
|
||||||
{ immediate: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
// gestion du mode comparaison : fermer les commentaires, etc.
|
|
||||||
watch(isCompareModeEnabled, (newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
isCommentsOpen.value = false;
|
|
||||||
isCommentPanelEnabled.value = false;
|
|
||||||
} else {
|
|
||||||
isCommentPanelEnabled.value = true;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// quand on quitte le mode comparaison on retire l'élément secondaire si nécessaire
|
function enableCompareModeUI() {
|
||||||
if (!newValue && activeTracks.value.length === 2) {
|
isCommentsOpen.value = false;
|
||||||
|
isCommentPanelEnabled.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableCompareModeUI() {
|
||||||
|
isCommentPanelEnabled.value = true;
|
||||||
|
|
||||||
|
if (activeTracks.value.length === 2) {
|
||||||
activeTracks.value.pop();
|
activeTracks.value.pop();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUrlHash(firstTrack) {
|
||||||
|
const trackSlug = getVariationSlug(firstTrack);
|
||||||
|
if (!trackSlug) return;
|
||||||
|
|
||||||
|
const currentHash = route.hash ? route.hash.substring(1) : '';
|
||||||
|
const normalizedTrackSlug = normalizeSlug(trackSlug);
|
||||||
|
|
||||||
|
if (currentHash !== normalizedTrackSlug) {
|
||||||
|
router.replace({ ...route, hash: '#' + normalizedTrackSlug });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(singleFile, updateOpenedFile, { immediate: true });
|
||||||
|
|
||||||
|
watch(isCompareModeEnabled, (isEnabled) => {
|
||||||
|
isEnabled ? enableCompareModeUI() : disableCompareModeUI();
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------- UTIL / helper ----------
|
watch(
|
||||||
function getCommentsCount(track) {
|
activeTracks,
|
||||||
if (!track || !Array.isArray(track.files)) return undefined;
|
(tracks) => {
|
||||||
let count = 0;
|
if (tracks && tracks.length > 0) {
|
||||||
for (const file of track.files) {
|
updateUrlHash(tracks[0]);
|
||||||
count += file?.comments?.length || 0;
|
}
|
||||||
}
|
},
|
||||||
return count > 0 ? count : undefined;
|
{ deep: true }
|
||||||
}
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue