From 5010da4ddbb451f6275cfa6b8db94051108d91e5 Mon Sep 17 00:00:00 2001
From: xziino
Date: Fri, 22 May 2026 11:20:17 +0200
Subject: [PATCH 1/3] Fix: auth_start_href() durch direkte Links auf
auth/start.php ersetzen
Co-Authored-By: Claude Sonnet 4.6
---
auth/start.php | 2 +-
templates/login.php | 2 +-
templates/report-form.php | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/auth/start.php b/auth/start.php
index a3f96da..0dec0e6 100644
--- a/auth/start.php
+++ b/auth/start.php
@@ -8,7 +8,7 @@ $state = bin2hex(random_bytes(16));
$_SESSION['pkce_verifier'] = $verifier;
$_SESSION['oauth_state'] = $state;
-$_SESSION['oauth_return'] = safe_return_path($_GET['return'] ?? ($_SERVER['HTTP_REFERER'] ?? null));
+$_SESSION['oauth_return'] = null;
$params = http_build_query([
'response_type' => 'code',
diff --git a/templates/login.php b/templates/login.php
index 336f18c..ef804a3 100644
--- a/templates/login.php
+++ b/templates/login.php
@@ -21,7 +21,7 @@
-
+
= $tokenExpired ? 'Reconnect to FFLogs' : 'Connect to FFLogs' ?>
diff --git a/templates/report-form.php b/templates/report-form.php
index 9f0c275..09da893 100644
--- a/templates/report-form.php
+++ b/templates/report-form.php
@@ -23,7 +23,7 @@
- Reconnect
+ Reconnect
From be9d0500367939dd48d8ee2aa44681615043d33e Mon Sep 17 00:00:00 2001
From: xziino
Date: Fri, 22 May 2026 12:25:44 +0200
Subject: [PATCH 2/3] Planner: Ordner-Struktur, Rechtsklick-Remove,
Duplikat-Schutz
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Ordner CRUD (erstellen, umbenennen, lΓΆschen) in der Plan-Sidebar
- PlΓ€ne per π-Button in Ordner verschieben (Picker-Dropdown)
- Ordner auf-/zuklappbar mit Chevron-Animation
- Rechtsklick auf Ability-Badge entfernt die Zuweisung
- uniquePlanName(): Duplikat-Namen beim manuellen Erstellen blockieren
- Import-Modal: Name wird direkt mit eindeutigem Namen vorbefΓΌllt
Co-Authored-By: Claude Sonnet 4.6
---
css/planner.css | 93 +++++++++++++
js/planner.js | 277 ++++++++++++++++++++++++++++++++++----
templates/tab-planner.php | 9 ++
3 files changed, 355 insertions(+), 24 deletions(-)
diff --git a/css/planner.css b/css/planner.css
index 3a12bba..c06fbb9 100644
--- a/css/planner.css
+++ b/css/planner.css
@@ -416,3 +416,96 @@
.ability-chip.badge-assign-shield.ability-chip--active { background: rgba(74,158,255,.18); border-color: rgba(74,158,255,.6); color: var(--blue); }
.ability-chip--other-job { opacity: 0.45; }
+
+/* ββ Folder Sidebar ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+.folder-section { margin-bottom: 2px; }
+
+.folder-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ padding: 7px 10px;
+ border-radius: var(--r);
+ cursor: pointer;
+ transition: background 0.12s;
+ user-select: none;
+}
+.folder-row:hover { background: var(--bg2); }
+
+.folder-chevron {
+ font-size: 9px;
+ color: var(--t3);
+ transition: transform 0.15s;
+ display: inline-block;
+ width: 10px;
+ flex-shrink: 0;
+}
+.folder-chevron.open { transform: rotate(90deg); }
+
+.folder-name-text {
+ font-size: 13px;
+ color: var(--t1);
+ font-weight: 500;
+ flex: 1;
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.folder-name-input {
+ font-size: 13px !important;
+ padding: 2px 6px !important;
+ flex: 1;
+ width: auto !important;
+ min-width: 80px;
+}
+
+.folder-count {
+ font-size: 11px;
+ color: var(--t3);
+ background: var(--bg3);
+ border-radius: 10px;
+ padding: 1px 6px;
+ flex-shrink: 0;
+}
+
+.folder-actions {
+ display: flex;
+ gap: 4px;
+ flex-shrink: 0;
+ opacity: 0;
+ transition: opacity 0.12s;
+}
+.folder-row:hover .folder-actions { opacity: 1; }
+
+.folder-plans { padding-left: 10px; }
+.folder-plans.collapsed { display: none; }
+
+.folder-empty {
+ font-size: 12px;
+ color: var(--t3);
+ padding: 5px 10px;
+ font-style: italic;
+}
+
+/* ββ Folder Picker Dropdown ββββββββββββββββββββββββββββββββββββββββββββββββββββ */
+.folder-picker {
+ background: var(--bgcard);
+ border: 1px solid var(--borderem);
+ border-radius: var(--r);
+ box-shadow: 0 4px 16px rgba(0,0,0,0.5);
+ min-width: 160px;
+ overflow: hidden;
+}
+
+.folder-picker-option {
+ padding: 7px 14px;
+ font-size: 13px;
+ color: var(--t2);
+ cursor: pointer;
+ transition: background 0.1s;
+ white-space: nowrap;
+}
+.folder-picker-option:hover { background: var(--bg2); color: var(--t1); }
+.folder-picker-option.active { color: var(--gold); }
diff --git a/js/planner.js b/js/planner.js
index 0664b0b..4ac4237 100644
--- a/js/planner.js
+++ b/js/planner.js
@@ -1,14 +1,33 @@
// ββ Storage βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const PLANNER_KEY = 'ff14-planner-plans';
+const FOLDERS_KEY = 'ff14-planner-folders';
function loadPlans() {
try { return JSON.parse(localStorage.getItem(PLANNER_KEY) || '[]'); }
catch { return []; }
}
+function savePlans(plans) { localStorage.setItem(PLANNER_KEY, JSON.stringify(plans)); }
-function savePlans(plans) {
- localStorage.setItem(PLANNER_KEY, JSON.stringify(plans));
+function loadFolders() {
+ try { return JSON.parse(localStorage.getItem(FOLDERS_KEY) || '[]'); }
+ catch { return []; }
+}
+function saveFolders(f) { localStorage.setItem(FOLDERS_KEY, JSON.stringify(f)); }
+
+function createFolder(name) {
+ const folder = { id: crypto.randomUUID(), name: name.trim() || 'Neuer Ordner', createdAt: Date.now(), updatedAt: Date.now() };
+ const all = loadFolders(); all.push(folder); saveFolders(all); return folder;
+}
+function updateFolder(id, changes) {
+ const all = loadFolders(), idx = all.findIndex(f => f.id === id);
+ if (idx === -1) return;
+ all[idx] = { ...all[idx], ...changes, updatedAt: Date.now() };
+ saveFolders(all);
+}
+function deleteFolder(id) {
+ saveFolders(loadFolders().filter(f => f.id !== id));
+ savePlans(loadPlans().map(p => p.folderId === id ? { ...p, folderId: null, updatedAt: Date.now() } : p));
}
// ββ CRUD ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -20,6 +39,7 @@ function createPlan(name) {
createdAt: Date.now(),
updatedAt: Date.now(),
source: null,
+ folderId: null,
jobComposition: Array(8).fill(''),
mechanics: []
};
@@ -46,6 +66,14 @@ function deletePlan(id) {
savePlans(loadPlans().filter(p => p.id !== id));
}
+function uniquePlanName(baseName) {
+ const names = new Set(loadPlans().map(p => p.name));
+ if (!names.has(baseName)) return baseName;
+ let i = 2;
+ while (names.has(`${baseName} ${i}`)) i++;
+ return `${baseName} ${i}`;
+}
+
function copyPlan(id) {
const all = loadPlans();
const orig = all.find(p => p.id === id);
@@ -64,7 +92,8 @@ function copyPlan(id) {
// ββ State βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-let activePlanId = null;
+let activePlanId = null;
+let collapsedFolders = new Set();
// ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -91,30 +120,68 @@ function fmtNumber(n) {
// ββ Rendering: Plan List ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-function renderPlanList() {
- const el = document.getElementById('plan-list');
- if (!el) return;
-
- const plans = loadPlans();
-
- if (plans.length === 0) {
- el.innerHTML = 'Noch keine PlΓ€ne vorhanden
';
- return;
- }
-
- el.innerHTML = plans.map(p => `
+function planItemHtml(p) {
+ return `
${escHtml(p.name)}
${p.mechanics.length} Mechaniken · ${fmtDate(p.updatedAt)}
+
-
- `).join('');
+ `;
+}
+function renderPlanList() {
+ const el = document.getElementById('plan-list');
+ if (!el) return;
+
+ const plans = loadPlans();
+ const folders = loadFolders();
+
+ if (plans.length === 0 && folders.length === 0) {
+ el.innerHTML = 'Noch keine PlΓ€ne vorhanden
';
+ return;
+ }
+
+ const byFolder = new Map();
+ const ungrouped = [];
+ for (const p of plans) {
+ const fid = p.folderId ?? null;
+ if (fid && folders.some(f => f.id === fid)) {
+ if (!byFolder.has(fid)) byFolder.set(fid, []);
+ byFolder.get(fid).push(p);
+ } else {
+ ungrouped.push(p);
+ }
+ }
+
+ let html = folders.map(folder => {
+ const folderPlans = byFolder.get(folder.id) ?? [];
+ const collapsed = collapsedFolders.has(folder.id);
+ return `
+
+
+
βΆ
+
${escHtml(folder.name)}
+
${folderPlans.length}
+
+
+
+
+
+
+ ${folderPlans.map(p => planItemHtml(p)).join('') || '
Leer
'}
+
+
`;
+ }).join('') + ungrouped.map(p => planItemHtml(p)).join('');
+
+ el.innerHTML = html || 'Noch keine PlΓ€ne vorhanden
';
+
+ // Plan item: open
el.querySelectorAll('.plan-item').forEach(row => {
row.addEventListener('click', e => {
if (e.target.closest('.plan-item-actions')) return;
@@ -122,6 +189,15 @@ function renderPlanList() {
});
});
+ // Plan item: move to folder
+ el.querySelectorAll('.plan-btn-move').forEach(btn => {
+ btn.addEventListener('click', e => {
+ e.stopPropagation();
+ showFolderPicker(btn.dataset.id, btn);
+ });
+ });
+
+ // Plan item: copy
el.querySelectorAll('.plan-btn-copy').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
@@ -130,6 +206,7 @@ function renderPlanList() {
});
});
+ // Plan item: delete
el.querySelectorAll('.plan-btn-delete').forEach(btn => {
btn.addEventListener('click', e => {
e.stopPropagation();
@@ -137,10 +214,63 @@ function renderPlanList() {
if (!plan) return;
if (!confirm(`Plan "${plan.name}" lΓΆschen?`)) return;
deletePlan(btn.dataset.id);
- if (activePlanId === btn.dataset.id) {
- activePlanId = null;
- renderPlanDetail(null);
- }
+ if (activePlanId === btn.dataset.id) { activePlanId = null; renderPlanDetail(null); }
+ renderPlanList();
+ });
+ });
+
+ // Folder row: toggle collapse
+ el.querySelectorAll('.folder-row').forEach(row => {
+ row.addEventListener('click', e => {
+ if (e.target.closest('.folder-actions')) return;
+ const fid = row.dataset.folderId;
+ if (collapsedFolders.has(fid)) collapsedFolders.delete(fid);
+ else collapsedFolders.add(fid);
+ renderPlanList();
+ });
+ });
+
+ // Folder: rename
+ el.querySelectorAll('.folder-rename-btn').forEach(btn => {
+ btn.addEventListener('click', e => {
+ e.stopPropagation();
+ const fid = btn.dataset.id;
+ const folder = loadFolders().find(f => f.id === fid);
+ if (!folder) return;
+ const span = el.querySelector(`.folder-row[data-folder-id="${fid}"] .folder-name-text`);
+ if (!span) return;
+ const input = document.createElement('input');
+ input.className = 'folder-name-input';
+ input.value = folder.name;
+ span.replaceWith(input);
+ input.focus(); input.select();
+ let saved = false;
+ const save = () => {
+ if (saved) return; saved = true;
+ const name = input.value.trim();
+ if (name) updateFolder(fid, { name });
+ renderPlanList();
+ };
+ input.addEventListener('blur', save);
+ input.addEventListener('keydown', ev => {
+ if (ev.key === 'Enter') { ev.preventDefault(); save(); }
+ if (ev.key === 'Escape') { saved = true; renderPlanList(); }
+ });
+ });
+ });
+
+ // Folder: delete
+ el.querySelectorAll('.folder-delete-btn').forEach(btn => {
+ btn.addEventListener('click', e => {
+ e.stopPropagation();
+ const folder = loadFolders().find(f => f.id === btn.dataset.id);
+ if (!folder) return;
+ const count = (byFolder.get(btn.dataset.id) ?? []).length;
+ const msg = count > 0
+ ? `Ordner "${folder.name}" lΓΆschen? Die ${count} enthaltenen PlΓ€ne werden nicht gelΓΆscht.`
+ : `Ordner "${folder.name}" lΓΆschen?`;
+ if (!confirm(msg)) return;
+ deleteFolder(btn.dataset.id);
renderPlanList();
});
});
@@ -339,6 +469,14 @@ function initMechanicClicks(planId) {
if (!card) return;
showAbilityModal(planId, card.dataset.mechanicId);
});
+ list.addEventListener('contextmenu', e => {
+ const badge = e.target.closest('.badge-assign');
+ if (!badge) return;
+ e.preventDefault();
+ e.stopPropagation();
+ const removeBtn = badge.querySelector('.badge-remove');
+ if (removeBtn) removeAssignment(planId, removeBtn.dataset.mechanicId, removeBtn.dataset.ability);
+ });
}
// ββ Rename ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -401,15 +539,105 @@ function initNewPlanForm() {
cancel.addEventListener('click', () => { form.style.display = 'none'; });
+ const clearErr = () => {
+ input.style.borderColor = '';
+ document.getElementById('planner-new-name-err')?.remove();
+ };
+
const doCreate = () => {
const name = input.value.trim();
if (!name) { input.focus(); return; }
+ if (loadPlans().some(p => p.name === name)) {
+ input.style.borderColor = 'var(--red)';
+ let err = document.getElementById('planner-new-name-err');
+ if (!err) {
+ err = document.createElement('div');
+ err.id = 'planner-new-name-err';
+ err.style.cssText = 'font-size:11px;color:var(--red);margin-top:3px';
+ input.insertAdjacentElement('afterend', err);
+ }
+ err.textContent = 'Name bereits vergeben';
+ input.focus();
+ return;
+ }
+ clearErr();
const plan = createPlan(name);
form.style.display = 'none';
renderPlanList();
openPlan(plan.id);
};
+ input.addEventListener('input', clearErr);
+ save.addEventListener('click', doCreate);
+ input.addEventListener('keydown', e => {
+ if (e.key === 'Enter') doCreate();
+ if (e.key === 'Escape') { clearErr(); form.style.display = 'none'; }
+ });
+}
+
+// ββ Folder Picker βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+let folderPickerCleanup = null;
+
+function closeFolderPicker() {
+ document.getElementById('folder-picker')?.remove();
+ if (folderPickerCleanup) { folderPickerCleanup(); folderPickerCleanup = null; }
+}
+
+function showFolderPicker(planId, anchorEl) {
+ closeFolderPicker();
+ const plan = getPlan(planId);
+ const folders = loadFolders();
+ if (!plan) return;
+ if (folders.length === 0) return;
+
+ const rect = anchorEl.getBoundingClientRect();
+ const picker = document.createElement('div');
+ picker.id = 'folder-picker';
+ picker.className = 'folder-picker';
+ picker.style.cssText = `position:fixed;top:${rect.bottom + 4}px;left:${rect.left}px;z-index:9999`;
+
+ picker.innerHTML = [{ id: null, name: 'Kein Ordner' }, ...folders].map(f => `
+
+ ${f.id === null ? 'β ' : 'π '}${escHtml(f.name)}
+
`).join('');
+
+ document.body.appendChild(picker);
+
+ picker.querySelectorAll('.folder-picker-option').forEach(opt => {
+ opt.addEventListener('click', e => {
+ e.stopPropagation();
+ updatePlan(planId, { folderId: opt.dataset.fid || null });
+ closeFolderPicker();
+ renderPlanList();
+ });
+ });
+
+ const onOutside = e => { if (!picker.contains(e.target)) closeFolderPicker(); };
+ setTimeout(() => document.addEventListener('click', onOutside), 0);
+ folderPickerCleanup = () => document.removeEventListener('click', onOutside);
+}
+
+// ββ New Folder Form βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+function initNewFolderForm() {
+ const btn = document.getElementById('planner-new-folder-btn');
+ const form = document.getElementById('planner-new-folder-form');
+ const input = document.getElementById('planner-new-folder-name');
+ const save = document.getElementById('planner-new-folder-save');
+ const cancel = document.getElementById('planner-new-folder-cancel');
+ if (!btn) return;
+
+ btn.addEventListener('click', () => { form.style.display = ''; input.value = ''; input.focus(); });
+ cancel.addEventListener('click', () => { form.style.display = 'none'; });
+
+ const doCreate = () => {
+ const name = input.value.trim();
+ if (!name) { input.focus(); return; }
+ createFolder(name);
+ form.style.display = 'none';
+ renderPlanList();
+ };
save.addEventListener('click', doCreate);
input.addEventListener('keydown', e => {
if (e.key === 'Enter') doCreate();
@@ -680,7 +908,7 @@ function doImport(data, withMitigations, whereMode, mergeId, newName) {
const mechanics = aoeEventsToMechanics(aoeEvents, fightStart, phases, players, withMitigations);
if (whereMode === 'new') {
- const plan = createPlan(newName || fightName || 'Importierter Plan');
+ const plan = createPlan(uniquePlanName(newName || fightName || 'Importierter Plan'));
return updatePlan(plan.id, {
mechanics,
source: { reportCode, fightName },
@@ -821,9 +1049,9 @@ let pendingImportData = null;
function showImportModal(data) {
pendingImportData = data;
- // Pre-fill name
+ // Pre-fill name β auto-unique so the user sees immediately what name will be used
const nameInput = document.getElementById('import-plan-name');
- if (nameInput) { nameInput.value = data.fightName || ''; }
+ if (nameInput) { nameInput.value = uniquePlanName(data.fightName || 'Importierter Plan'); }
// Populate merge dropdown
const planSelect = document.getElementById('import-plan-select');
@@ -928,6 +1156,7 @@ window.plannerTab = {
document.addEventListener('DOMContentLoaded', () => {
initNewPlanForm();
+ initNewFolderForm();
initImportModal();
initAbilityModal();
renderPlanList();
diff --git a/templates/tab-planner.php b/templates/tab-planner.php
index 98109ca..c657927 100644
--- a/templates/tab-planner.php
+++ b/templates/tab-planner.php
@@ -4,9 +4,18 @@