feat: système de profils — historiques et drop stats séparés par profil
- Profils stockés dans profiles.json (userData), profil "Défaut" rétro-compatible - Chaque profil a son propre historique de sessions et app state (fichiers suffixés par id) - Drop stats : mode Privé (fichier propre) ou Partagé (pool dropstats-shared.json commun) - Bouton 👤 dans la topbar ouvre une modal de gestion des profils - Créer, renommer (double-clic), supprimer, activer un profil - Basculer le mode de drop stats (Privé ↔ Partagé) par profil - timers et alertes isolés par profil via clés localStorage préfixées par profil id - Switch de profil = rechargement de page (données propres sans état résiduel) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -348,6 +348,24 @@
|
||||
.sound-card-actions{display:flex;gap:6px;align-items:center;margin-top:2px;}
|
||||
.sound-card-status{font-size:10px;font-family:var(--font-mono);color:var(--moss);margin-left:auto;}
|
||||
|
||||
/* --- Profils --- */
|
||||
#profileBtn{font-size:11px;max-width:160px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.profile-modal-overlay{position:fixed;inset:0;background:rgba(0,0,0,.65);z-index:900;display:flex;align-items:center;justify-content:center;}
|
||||
.profile-modal-overlay[hidden]{display:none;}
|
||||
.profile-modal-box{background:var(--bg-panel);border:1px solid var(--border);padding:22px 24px;width:460px;max-width:95vw;max-height:80vh;overflow-y:auto;display:flex;flex-direction:column;gap:14px;}
|
||||
.profile-modal-box h3{font-size:14px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;margin:0;}
|
||||
.profile-list{display:flex;flex-direction:column;gap:6px;}
|
||||
.profile-row{display:flex;align-items:center;gap:8px;padding:8px 10px;border:1px solid var(--border);background:var(--bg);}
|
||||
.profile-row.active{border-color:var(--moss);}
|
||||
.profile-row-name{flex:1;font-size:13px;font-weight:600;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
|
||||
.profile-row-mode{font-size:10px;font-family:var(--font-mono);padding:2px 6px;border:1px solid var(--border);color:var(--text-dim);cursor:pointer;white-space:nowrap;}
|
||||
.profile-row-mode.shared{color:var(--corrupt);border-color:var(--corrupt);}
|
||||
.profile-create-form{display:flex;flex-direction:column;gap:8px;padding:10px;border:1px solid var(--border);background:var(--bg);}
|
||||
.profile-create-form input[type=text]{width:100%;box-sizing:border-box;}
|
||||
.profile-mode-row{display:flex;gap:16px;font-size:12px;color:var(--text-dim);}
|
||||
.profile-mode-row label{display:flex;align-items:center;gap:5px;cursor:pointer;}
|
||||
.profile-mode-desc{font-size:11px;color:var(--text-dim);line-height:1.5;}
|
||||
|
||||
#chartTip{position:fixed;z-index:1001;background:var(--bg-panel);border:1px solid var(--border);
|
||||
padding:4px 10px;font-family:var(--font-mono);font-size:11px;color:var(--text);
|
||||
pointer-events:none;display:none;white-space:nowrap;box-shadow:0 2px 8px rgba(0,0,0,.5);}
|
||||
@@ -359,11 +377,39 @@
|
||||
<div class="shell-topbar">
|
||||
<div class="brand" id="brandHome"><span class="brand-mark">⛏</span><span class="brand-text">FARM TRACKER</span></div>
|
||||
<div class="spacer"></div>
|
||||
<button class="btn-ghost small" id="profileBtn">👤 …</button>
|
||||
<button class="btn-ghost" id="homeBtn" hidden>🏠 Accueil</button>
|
||||
<button class="btn-ghost" id="changeFolderBtn" hidden>Changer de dossier</button>
|
||||
<button class="btn-ghost small" id="checkUpdateBtn" title="Vérifier les mises à jour">Mises à jour</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal profils -->
|
||||
<div class="profile-modal-overlay" id="profileModal" hidden>
|
||||
<div class="profile-modal-box">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;">
|
||||
<h3>Profils</h3>
|
||||
<button class="btn-ghost small" id="profileModalClose">✕</button>
|
||||
</div>
|
||||
<div class="profile-list" id="profileList"></div>
|
||||
<div class="profile-create-form" id="profileCreateForm" hidden>
|
||||
<input type="text" id="profileNewName" placeholder="Nom du profil" maxlength="40">
|
||||
<div class="profile-mode-row">
|
||||
<label><input type="radio" name="newProfileMode" value="private" checked> Stats de drop privées</label>
|
||||
<label><input type="radio" name="newProfileMode" value="shared"> Stats de drop partagées</label>
|
||||
</div>
|
||||
<p class="profile-mode-desc" id="profileModeDesc">
|
||||
<strong>Privé :</strong> ce profil a ses propres chances de drop, indépendantes des autres.<br>
|
||||
<strong>Partagé :</strong> ce profil partage le même pool de drop stats que tous les autres profils en mode partagé.
|
||||
</p>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<button class="btn-ghost small" id="profileCreateCancel">Annuler</button>
|
||||
<button class="btn-ghost accent small" id="profileCreateConfirm">Créer le profil</button>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-ghost small" id="profileNewBtn" style="align-self:flex-start;">+ Nouveau profil</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============ ÉCRAN 1 : choix du dossier (une seule fois, global) ============ -->
|
||||
<section class="connect-screen" id="folderScreen">
|
||||
<div class="connect-card">
|
||||
@@ -901,6 +947,126 @@
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
// ======================================================================
|
||||
// PROFILS
|
||||
// ======================================================================
|
||||
|
||||
let _profiles = null; // { activeProfileId, profiles: [...] }
|
||||
let _activeProfile = null; // profil actif
|
||||
|
||||
async function loadProfiles() {
|
||||
_profiles = await window.api.getProfiles();
|
||||
_activeProfile = _profiles.profiles.find(p => p.id === _profiles.activeProfileId) || _profiles.profiles[0];
|
||||
renderProfileBtn();
|
||||
}
|
||||
|
||||
function renderProfileBtn() {
|
||||
const btn = $('profileBtn');
|
||||
if (btn && _activeProfile) btn.textContent = '👤 ' + _activeProfile.name;
|
||||
}
|
||||
|
||||
// Clé localStorage préfixée par profil (compat : profil "default" = clé sans préfixe)
|
||||
function lsKey(key) {
|
||||
if (!_activeProfile || _activeProfile.id === 'default') return key;
|
||||
return _activeProfile.id + ':' + key;
|
||||
}
|
||||
|
||||
function renderProfileModal() {
|
||||
const list = $('profileList');
|
||||
if (!list || !_profiles) return;
|
||||
list.innerHTML = _profiles.profiles.map(p => {
|
||||
const isActive = p.id === _profiles.activeProfileId;
|
||||
const modeLabel = p.dropStatsMode === 'shared' ? 'Partagé' : 'Privé';
|
||||
const modeClass = p.dropStatsMode === 'shared' ? 'shared' : '';
|
||||
return '<div class="profile-row' + (isActive ? ' active' : '') + '" data-profile-id="' + escHtml(p.id) + '">' +
|
||||
'<span class="profile-row-name">' + (isActive ? '● ' : '') + escHtml(p.name) + '</span>' +
|
||||
'<button class="profile-row-mode ' + modeClass + '" data-toggle-mode="' + escHtml(p.id) + '" title="Cliquer pour changer le mode de drop stats">' + modeLabel + '</button>' +
|
||||
(isActive ? '' : '<button class="btn-ghost small" data-switch-profile="' + escHtml(p.id) + '">Activer</button>') +
|
||||
(p.id !== 'default' && !isActive ? '<button class="btn-ghost small danger" data-delete-profile="' + escHtml(p.id) + '">✕</button>' : '') +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
$('profileBtn').addEventListener('click', () => {
|
||||
renderProfileModal();
|
||||
$('profileModal').hidden = false;
|
||||
$('profileCreateForm').hidden = true;
|
||||
});
|
||||
$('profileModalClose').addEventListener('click', () => { $('profileModal').hidden = true; });
|
||||
$('profileModal').addEventListener('click', e => { if (e.target === $('profileModal')) $('profileModal').hidden = true; });
|
||||
|
||||
$('profileNewBtn').addEventListener('click', () => {
|
||||
$('profileCreateForm').hidden = false;
|
||||
$('profileNewBtn').hidden = true;
|
||||
$('profileNewName').value = '';
|
||||
$('profileNewName').focus();
|
||||
});
|
||||
$('profileCreateCancel').addEventListener('click', () => {
|
||||
$('profileCreateForm').hidden = true;
|
||||
$('profileNewBtn').hidden = false;
|
||||
});
|
||||
$('profileCreateConfirm').addEventListener('click', async () => {
|
||||
const name = $('profileNewName').value.trim();
|
||||
if (!name) return;
|
||||
const mode = document.querySelector('input[name="newProfileMode"]:checked')?.value || 'private';
|
||||
_profiles = await window.api.createProfile(name, mode);
|
||||
_activeProfile = _profiles.profiles.find(p => p.id === _profiles.activeProfileId) || _profiles.profiles[0];
|
||||
$('profileCreateForm').hidden = true;
|
||||
$('profileNewBtn').hidden = false;
|
||||
renderProfileModal();
|
||||
});
|
||||
|
||||
$('profileList').addEventListener('click', async e => {
|
||||
const switchId = e.target.dataset.switchProfile;
|
||||
if (switchId) {
|
||||
await window.api.switchProfile(switchId);
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
const deleteId = e.target.dataset.deleteProfile;
|
||||
if (deleteId) {
|
||||
if (!confirm('Supprimer ce profil ? L\'historique et les stats de drop privées seront effacés définitivement.')) return;
|
||||
const res = await window.api.deleteProfile(deleteId);
|
||||
if (res.ok) {
|
||||
_profiles = await window.api.getProfiles();
|
||||
renderProfileModal();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const toggleId = e.target.dataset.toggleMode;
|
||||
if (toggleId) {
|
||||
const p = _profiles.profiles.find(pr => pr.id === toggleId);
|
||||
if (!p) return;
|
||||
const newMode = p.dropStatsMode === 'shared' ? 'private' : 'shared';
|
||||
await window.api.setDropStatsMode(toggleId, newMode);
|
||||
_profiles = await window.api.getProfiles();
|
||||
_activeProfile = _profiles.profiles.find(pr => pr.id === _profiles.activeProfileId) || _profiles.profiles[0];
|
||||
renderProfileModal();
|
||||
// si c'est le profil actif, reload pour recharger les drop stats
|
||||
if (toggleId === _profiles.activeProfileId) {
|
||||
if (confirm('Mode de drop stats changé. Recharger pour appliquer ?')) location.reload();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Renommage double-clic sur le nom
|
||||
$('profileList').addEventListener('dblclick', async e => {
|
||||
const row = e.target.closest('.profile-row');
|
||||
if (!row) return;
|
||||
const id = row.dataset.profileId;
|
||||
const nameEl = row.querySelector('.profile-row-name');
|
||||
if (!nameEl) return;
|
||||
const current = _profiles.profiles.find(p => p.id === id);
|
||||
if (!current) return;
|
||||
const newName = prompt('Renommer le profil :', current.name);
|
||||
if (!newName || !newName.trim()) return;
|
||||
await window.api.renameProfile(id, newName.trim());
|
||||
_profiles = await window.api.getProfiles();
|
||||
_activeProfile = _profiles.profiles.find(p => p.id === _profiles.activeProfileId) || _profiles.profiles[0];
|
||||
renderProfileBtn();
|
||||
renderProfileModal();
|
||||
});
|
||||
|
||||
// ---------- Tooltip pour les graphes ----------
|
||||
const chartTip = $('chartTip');
|
||||
function showChartTip(text, cx, cy){
|
||||
@@ -2217,7 +2383,7 @@
|
||||
// TIMERS
|
||||
// ======================================================================
|
||||
|
||||
let timerConfigs = JSON.parse(localStorage.getItem('timerConfigs') || '[]');
|
||||
let timerConfigs = JSON.parse(localStorage.getItem(lsKey('timerConfigs')) || '[]');
|
||||
// ---- Sound catalog ----
|
||||
const SOUND_CATALOG = [
|
||||
{ id:'levelup', label:'Level Up XP', desc:'Montée de niveau', hash:'19034765ba8ba5389b35804ab213537ab5cf706f' },
|
||||
@@ -2331,12 +2497,12 @@
|
||||
}
|
||||
|
||||
// migration : ajoute soundId et volume si absents
|
||||
let alertConfigs = JSON.parse(localStorage.getItem('alertConfigs') || '[]').map(c => ({
|
||||
let alertConfigs = JSON.parse(localStorage.getItem(lsKey('alertConfigs')) || '[]').map(c => ({
|
||||
soundId: 'levelup', volume: 10, ...c
|
||||
}));
|
||||
|
||||
function saveAlertConfigs(){
|
||||
localStorage.setItem('alertConfigs', JSON.stringify(alertConfigs));
|
||||
localStorage.setItem(lsKey('alertConfigs'), JSON.stringify(alertConfigs));
|
||||
}
|
||||
|
||||
// activeTimers : Map<configId, { endsAt: number, durationSec: number, mobName: string }>
|
||||
@@ -2344,7 +2510,7 @@
|
||||
let timerClockId = null;
|
||||
|
||||
function saveTimerConfigs(){
|
||||
localStorage.setItem('timerConfigs', JSON.stringify(timerConfigs));
|
||||
localStorage.setItem(lsKey('timerConfigs'), JSON.stringify(timerConfigs));
|
||||
updateTimerTileStatus();
|
||||
}
|
||||
|
||||
@@ -2761,6 +2927,13 @@
|
||||
// ======================================================================
|
||||
|
||||
(async () => {
|
||||
// Charger les profils en premier (nécessaire avant lsKey et les données)
|
||||
await loadProfiles();
|
||||
|
||||
// Re-lire timerConfigs et alertConfigs maintenant que lsKey est disponible
|
||||
timerConfigs = JSON.parse(localStorage.getItem(lsKey('timerConfigs')) || '[]');
|
||||
alertConfigs = JSON.parse(localStorage.getItem(lsKey('alertConfigs')) || '[]').map(c => ({ soundId: 'levelup', volume: 10, ...c }));
|
||||
|
||||
let appState = null, dropStatsPlain = null, communityDropStatsPlain = null;
|
||||
try{ appState = await window.api.loadAppState(); }catch(e){}
|
||||
try{ dropStatsPlain = await window.api.loadDropStats(); }catch(e){}
|
||||
|
||||
Reference in New Issue
Block a user