Files
TodoFlow/widget/src/app.js
T
Claude 27d3fb7d50 Corrige la restauration du widget masqué sans icône de barre des tâches
Le bouton Réduire appelait minimize() alors que la fenêtre utilise
skipTaskbar:true : sans entrée dans la barre des tâches, la fenêtre
minimisée devenait irrécupérable. Le bouton masque désormais vers le
system tray.

La logique de bascule du tray gère maintenant correctement l'état
minimisé (is_minimized) et restaure la fenêtre via unminimize/show/
set_focus, faisant de l'icône de la barre d'état un point de
restauration fiable dans tous les cas.
2026-05-31 01:27:33 +00:00

521 lines
18 KiB
JavaScript

// --- CONFIGURATIONS INITIALES ET ÉTATS ---
// Références vers l'API Tauri (uniquement si exécuté sous Tauri)
const tauri = window.__TAURI__;
const appWindow = tauri ? tauri.window.getCurrent() : null;
const invoke = tauri ? tauri.invoke : null;
// État local de l'application
let state = {
apiUrl: "http://localhost:8000",
apiKey: "widget-token-secure-789",
alwaysOnTop: false,
autostart: false,
opacity: 70,
tasks: [],
pollingInterval: null
};
// --- SÉLECTEURS DOM ---
const statusIndicator = document.getElementById("connection-status");
const taskProgress = document.getElementById("task-progress");
const tasksList = document.getElementById("tasks-list");
const addTaskForm = document.getElementById("add-task-form");
const taskInput = document.getElementById("task-input");
// Paramètres panel
const settingsPanel = document.getElementById("settings-panel");
const btnSettings = document.getElementById("btn-settings");
const btnSettingsClose = document.getElementById("btn-settings-close");
const btnSaveSettings = document.getElementById("btn-save-settings");
// Inputs de configuration
const inputApiUrl = document.getElementById("setting-api-url");
const inputApiKey = document.getElementById("setting-api-key");
const inputAlwaysOnTop = document.getElementById("setting-always-on-top");
const inputAutostart = document.getElementById("setting-autostart");
// Contrôles de fenêtre
const btnMinimize = document.getElementById("btn-minimize");
const btnClose = document.getElementById("btn-close");
const inputOpacity = document.getElementById("setting-opacity");
const opacityValue = document.getElementById("opacity-value");
let taskToDeleteId = null;
// --- INITIALISATION ---
document.addEventListener("DOMContentLoaded", async () => {
// Chargement des paramètres stockés localement
loadSettingsFromStorage();
// Remplissage initial des champs du panneau de configuration
inputApiUrl.value = state.apiUrl;
inputApiKey.value = state.apiKey;
inputAlwaysOnTop.checked = state.alwaysOnTop;
inputOpacity.value = state.opacity;
opacityValue.textContent = state.opacity;
// Real-time opacity slider updates
inputOpacity.addEventListener("input", () => {
const val = inputOpacity.value;
opacityValue.textContent = val;
document.documentElement.style.setProperty("--widget-opacity", val / 100);
});
// Modal de confirmation de suppression personnalisé
const confirmModal = document.getElementById("confirm-modal");
const btnConfirmCancel = document.getElementById("btn-confirm-cancel");
const btnConfirmDelete = document.getElementById("btn-confirm-delete");
btnConfirmCancel.addEventListener("click", () => {
confirmModal.classList.remove("open");
taskToDeleteId = null;
});
btnConfirmDelete.addEventListener("click", async () => {
if (taskToDeleteId) {
confirmModal.classList.remove("open");
await executeDeleteTask(taskToDeleteId);
taskToDeleteId = null;
}
});
// Événements d'ouverture/fermeture des paramètres
btnSettings.addEventListener("click", () => settingsPanel.classList.add("open"));
btnSettingsClose.addEventListener("click", () => settingsPanel.classList.remove("open"));
btnSaveSettings.addEventListener("click", saveSettings);
// Événements Tauri pour le contrôle de la fenêtre de bureau
if (appWindow) {
// La fenêtre utilise skipTaskbar:true : elle n'apparaît jamais dans la barre des tâches.
// Un minimize() classique la rendrait donc irrécupérable. On masque plutôt vers le
// system tray, d'où elle peut toujours être restaurée via l'icône de la barre d'état.
btnMinimize.addEventListener("click", () => appWindow.hide());
btnClose.addEventListener("click", () => appWindow.hide()); // Masquer vers le tray au clic sur 'Fermer'
// Récupération de l'état réel du démarrage automatique depuis le registre Windows
try {
state.autostart = await invoke("get_autostart");
inputAutostart.checked = state.autostart;
} catch (e) {
console.warn("Impossible de lire l'état de démarrage automatique :", e);
}
} else {
// Si exécuté hors de Tauri (ex: navigateur classique), masquer les boutons de fenêtre
btnMinimize.style.display = "none";
btnClose.style.display = "none";
inputAutostart.closest(".setting-toggle").style.display = "none"; // Cacher l'autostart
}
// Événement d'ajout de tâche
addTaskForm.addEventListener("submit", handleAddTask);
// Démarrage de la synchronisation en temps réel (Polling toutes les 4 secondes)
fetchTasks();
state.pollingInterval = setInterval(fetchTasks, 4000);
});
// --- ENREGISTREMENT ET CHARGEMENT DES PARAMÈTRES ---
function loadSettingsFromStorage() {
// Charge la configuration pour assurer la persistance des ordinateurs de travail
const storedApiUrl = localStorage.getItem("todoflow_api_url");
const storedApiKey = localStorage.getItem("todoflow_api_key");
const storedAlwaysOnTop = localStorage.getItem("todoflow_always_on_top");
const storedOpacity = localStorage.getItem("todoflow_opacity");
if (storedApiUrl) state.apiUrl = storedApiUrl;
if (storedApiKey) state.apiKey = storedApiKey;
if (storedAlwaysOnTop) {
state.alwaysOnTop = storedAlwaysOnTop === "true";
// Appliquer directement la préférence au démarrage
if (appWindow && invoke) {
invoke("toggle_always_on_top", { enabled: state.alwaysOnTop }).catch(console.error);
}
}
if (storedOpacity) {
state.opacity = parseInt(storedOpacity);
document.documentElement.style.setProperty("--widget-opacity", state.opacity / 100);
} else {
state.opacity = 70;
document.documentElement.style.setProperty("--widget-opacity", 0.70);
}
}
async function saveSettings() {
const newUrl = inputApiUrl.value.trim().replace(/\/$/, ""); // Retirer le slash de fin éventuel
const newKey = inputApiKey.value.trim();
const newAlwaysOnTop = inputAlwaysOnTop.checked;
const newAutostart = inputAutostart.checked;
const newOpacity = inputOpacity.value;
state.apiUrl = newUrl;
state.apiKey = newKey;
state.alwaysOnTop = newAlwaysOnTop;
state.opacity = parseInt(newOpacity);
localStorage.setItem("todoflow_api_url", newUrl);
localStorage.setItem("todoflow_api_key", newKey);
localStorage.setItem("todoflow_always_on_top", newAlwaysOnTop.toString());
localStorage.setItem("todoflow_opacity", newOpacity.toString());
// Appel des fonctions système Windows natives via Tauri
if (appWindow && invoke) {
try {
await invoke("toggle_always_on_top", { enabled: newAlwaysOnTop });
await invoke("set_autostart", { enabled: newAutostart });
state.autostart = newAutostart;
} catch (e) {
alert(`Erreur système : ${e}`);
}
}
// Fermer le volet et forcer un rafraîchissement immédiat de la liste
settingsPanel.classList.remove("open");
fetchTasks();
}
// --- APPELS API (CRUD) ---
async function fetchTasks() {
// Suspendre la synchronisation si l'utilisateur est en train d'éditer une tâche pour éviter de perdre le focus et fermer l'édition
if (document.querySelector(".task-card.editing")) {
return;
}
// Récupération asynchrone des tâches avec la clé API
try {
const response = await fetch(`${state.apiUrl}/api/v1/tasks`, {
method: "GET",
headers: {
"X-API-Key": state.apiKey,
"Accept": "application/json"
}
});
if (!response.ok) throw new Error("Accès refusé ou serveur indisponible");
const tasks = await response.json();
state.tasks = tasks;
// Mise à jour de l'indicateur lumineux sur le header (Connecté)
statusIndicator.className = "status-indicator connected";
statusIndicator.title = "Connecté à la base centralisée";
renderTasksList();
} catch (error) {
console.error("Erreur de synchronisation :", error);
statusIndicator.className = "status-indicator disconnected";
statusIndicator.title = "Erreur de connexion avec le serveur";
}
}
async function handleAddTask(e) {
e.preventDefault();
const title = taskInput.value.trim();
if (!title) return;
// Création directe via un appel POST à l'API centralisée
try {
const response = await fetch(`${state.apiUrl}/api/v1/tasks`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": state.apiKey
},
json: true,
body: JSON.stringify({
title: title,
status: "A faire" // Statut initial par défaut
})
});
if (!response.ok) throw new Error("Échec de la création de la tâche");
taskInput.value = ""; // Vider le champ après création
fetchTasks(); // Rafraîchir pour afficher la tâche créée avec son animation
} catch (error) {
alert("Impossible d'ajouter la tâche : vérifiez votre connexion ou clé API.");
}
}
async function toggleTaskDone(taskId, currentStatus) {
// Permet de cocher ou décocher une tâche
const targetStatus = currentStatus === "Termine" ? "A faire" : "Termine";
try {
const response = await fetch(`${state.apiUrl}/api/v1/tasks/${taskId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-API-Key": state.apiKey
},
body: JSON.stringify({
status: targetStatus
})
});
if (!response.ok) throw new Error("Échec de mise à jour");
fetchTasks();
} catch (error) {
console.error("Erreur de mise à jour du statut :", error);
}
}
async function toggleTaskInProgress(taskId, currentStatus) {
// Permet de basculer spécifiquement les travaux "En cours" ou "À faire"
if (currentStatus === "Termine") return; // On ne modifie pas directement les tâches terminées par ici
const targetStatus = currentStatus === "En cours" ? "A faire" : "En cours";
try {
const response = await fetch(`${state.apiUrl}/api/v1/tasks/${taskId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-API-Key": state.apiKey
},
body: JSON.stringify({
status: targetStatus
})
});
if (!response.ok) throw new Error("Échec de mise à jour");
fetchTasks();
} catch (error) {
console.error("Erreur de mise à jour de l'état de travail :", error);
}
}
// --- AFFICHAGE ET LOGIQUE DE RENDU ---
function renderTasksList() {
if (state.tasks.length === 0) {
tasksList.innerHTML = `
<div class="empty-state">
<p>Aucune tâche en cours</p>
<span class="empty-subtext">Tout est à jour ! Excellent travail.</span>
</div>
`;
taskProgress.style.width = "0%";
return;
}
// Calcul et mise à jour de la barre de progression linéaire
const completedTasks = state.tasks.filter(t => t.status === "Termine").length;
const progressPercent = Math.round((completedTasks / state.tasks.length) * 100);
taskProgress.style.width = `${progressPercent}%`;
tasksList.innerHTML = "";
state.tasks.forEach(task => {
const isCompleted = task.status === "Termine";
// Structure de la carte de tâche
const card = document.createElement("div");
card.className = `task-card ${isCompleted ? 'completed' : ''}`;
// Clic pour étendre/réduire le titre si long
card.addEventListener("click", (e) => {
if (e.target.closest(".custom-checkbox") || e.target.closest(".btn-task-action") || e.target.closest(".task-edit-input")) {
return;
}
card.classList.toggle("expanded");
});
// Checkbox personnalisée cochable
const checkbox = document.createElement("div");
checkbox.className = `custom-checkbox ${isCompleted ? 'checked' : ''}`;
checkbox.innerHTML = `
<svg width="10" height="8" viewBox="0 0 10 8" fill="none">
<path d="M1.5 4L3.75 6.25L8.5 1.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`;
checkbox.addEventListener("click", (e) => {
e.stopPropagation();
toggleTaskDone(task.id, task.status);
});
// Bloc de contenu (Titre & Date)
const content = document.createElement("div");
content.className = "task-content";
const title = document.createElement("span");
title.className = "task-title";
title.textContent = task.title;
title.title = task.title; // Tooltip d'accessibilité
const date = document.createElement("span");
date.className = "task-date";
date.textContent = `Créé le ${formatDate(task.created_at)}`;
content.appendChild(title);
content.appendChild(date);
// Container d'actions (Edit / Delete)
const actions = document.createElement("div");
actions.className = "task-actions";
// Bouton Modifier
const btnEdit = document.createElement("button");
btnEdit.className = "btn-task-action btn-edit";
btnEdit.title = "Modifier la tâche";
btnEdit.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4z"/></svg>`;
btnEdit.addEventListener("click", (e) => {
e.stopPropagation();
startEditingTask(task.id, card, title, task.title);
});
// Double-clic sur le titre pour éditer également
title.addEventListener("dblclick", (e) => {
e.stopPropagation();
startEditingTask(task.id, card, title, task.title);
});
// Bouton Supprimer
const btnDelete = document.createElement("button");
btnDelete.className = "btn-task-action btn-delete";
btnDelete.title = "Supprimer la tâche";
btnDelete.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>`;
btnDelete.addEventListener("click", (e) => {
e.stopPropagation();
deleteTask(task.id);
});
actions.appendChild(btnEdit);
actions.appendChild(btnDelete);
// Badge de statut (En cours / À faire)
const badge = document.createElement("span");
if (task.status === "Termine") {
badge.className = "status-badge done";
badge.textContent = "Terminé";
} else if (task.status === "En cours") {
badge.className = "status-badge in-progress";
badge.textContent = "En cours";
badge.title = "Cliquer pour marquer 'À faire'";
badge.addEventListener("click", () => toggleTaskInProgress(task.id, task.status));
} else {
badge.className = "status-badge todo";
badge.textContent = "À faire";
badge.title = "Cliquer pour marquer 'En cours'";
badge.addEventListener("click", () => toggleTaskInProgress(task.id, task.status));
}
card.appendChild(checkbox);
card.appendChild(content);
card.appendChild(actions);
card.appendChild(badge);
tasksList.appendChild(card);
});
}
// Formatage de la date en français épuré
function formatDate(dateString) {
try {
const date = new Date(dateString);
// Format : "26 mai • 14:15"
const optionsDay = { day: 'numeric', month: 'short' };
const dayStr = date.toLocaleDateString('fr-FR', optionsDay);
const hours = date.getHours().toString().padStart(2, '0');
const minutes = date.getMinutes().toString().padStart(2, '0');
return `${dayStr} • ${hours}:${minutes}`;
} catch (e) {
return dateString;
}
}
// --- NOUVELLES FONCTIONS DE SUPPRESSION ET MODIFICATION ---
function startEditingTask(taskId, card, titleElement, currentTitle) {
if (card.classList.contains("editing")) return;
card.classList.add("editing");
const input = document.createElement("input");
input.type = "text";
input.className = "task-edit-input";
input.value = currentTitle;
input.maxLength = 100;
titleElement.innerHTML = "";
titleElement.appendChild(input);
input.focus();
input.select();
const finishEditing = async (save) => {
const newTitle = input.value.trim();
card.classList.remove("editing");
if (save && newTitle && newTitle !== currentTitle) {
titleElement.textContent = newTitle;
await updateTaskTitle(taskId, newTitle);
} else {
titleElement.textContent = currentTitle;
}
};
input.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
finishEditing(true);
} else if (e.key === "Escape") {
finishEditing(false);
}
});
input.addEventListener("blur", () => {
finishEditing(true);
});
input.addEventListener("click", (e) => e.stopPropagation());
}
async function updateTaskTitle(taskId, newTitle) {
try {
const response = await fetch(`${state.apiUrl}/api/v1/tasks/${taskId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-API-Key": state.apiKey
},
body: JSON.stringify({
title: newTitle
})
});
if (!response.ok) throw new Error("Échec de la mise à jour");
fetchTasks();
} catch (error) {
console.error("Erreur de mise à jour du titre :", error);
alert("Impossible de modifier la tâche.");
}
}
function deleteTask(taskId) {
taskToDeleteId = taskId;
const modal = document.getElementById("confirm-modal");
modal.classList.add("open");
}
async function executeDeleteTask(taskId) {
try {
const response = await fetch(`${state.apiUrl}/api/v1/tasks/${taskId}`, {
method: "DELETE",
headers: {
"X-API-Key": state.apiKey
}
});
if (!response.ok) throw new Error("Échec de la suppression");
fetchTasks();
} catch (error) {
console.error("Erreur de suppression de la tâche :", error);
alert("Impossible de supprimer la tâche.");
}
}