diff --git a/css/layout.css b/css/layout.css index 866a347..f5c485d 100644 --- a/css/layout.css +++ b/css/layout.css @@ -98,6 +98,43 @@ color: var(--t2); } +.report-load-row { + flex-wrap: nowrap; +} + +.report-code-field { + flex: 1 1 360px; + min-width: 240px; +} + +.report-language-field { + flex: 0 0 120px; + min-width: 100px; +} + +.report-fight-field { + flex: 1 1 360px; + min-width: 260px; +} + +.report-fight-label { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; +} + +.report-fight-label a { + color: var(--gold); + font-size: 12px; +} + +@media (max-width: 1100px) { + .report-load-row { + flex-wrap: wrap; + } +} + /* ── Topbar user badge ───────────────────────────────────────────────────────── */ .topbar-user { font-size: 13px; diff --git a/css/planner.css b/css/planner.css index f55d9ac..bc40f7c 100644 --- a/css/planner.css +++ b/css/planner.css @@ -1762,11 +1762,6 @@ /* ── View Toggle ─────────────────────────────────────────────────────────── */ -.view-toggle-btns { - display: flex; - gap: 4px; -} - .planner-card-actions { margin-left: auto; display: flex; @@ -1775,6 +1770,31 @@ flex-wrap: wrap; } +.planner-class-filter-wrap { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--t3); + font-size: 11px; + white-space: nowrap; +} + +.planner-class-filter { + width: auto; + min-width: 88px; + padding: 4px 9px; + border: 1px solid var(--border); + border-radius: var(--r); + background: var(--bg2); + color: var(--t1); + font-size: 12px; + cursor: pointer; +} + +.planner-class-filter:hover { + border-color: var(--t3); +} + .cactbot-export-modal-box { max-width: 720px; max-height: min(86vh, 780px); @@ -1971,7 +1991,7 @@ font-weight: 600; } -/* ── Meine Spells ────────────────────────────────────────────────────────── */ +/* ── Simple View Rows ────────────────────────────────────────────────────── */ .myspells-controls { display: flex; @@ -2040,6 +2060,18 @@ white-space: nowrap; } +.myspells-ability .badge-remove { + opacity: .45; +} + +.myspells-ability:hover .badge-remove { + opacity: .8; +} + +.myspells-ability .badge-remove:hover { + opacity: 1; +} + .myspells-ability.myspells-type--debuff { color: var(--orange); border-color: rgba(255,140,0,0.3); } .myspells-ability.myspells-type--shield { color: var(--blue); border-color: rgba(88,166,255,0.3); } .myspells-ability.myspells-type--personal { color: #dbc7ff; border-color: rgba(177,112,255,0.4); } diff --git a/js/planner.js b/js/planner.js index 63bb773..276cfa5 100644 --- a/js/planner.js +++ b/js/planner.js @@ -3,6 +3,8 @@ const PLANNER_KEY = 'ff14-planner-plans'; const PLANNER_ACTIVE_KEY = 'ff14-planner-active-plan'; const FOLDERS_KEY = 'ff14-planner-folders'; +const PLANNER_SIMPLE_VIEW_KEY = 'ff14-planner-simple-view'; +const PLANNER_CLASS_FILTER_KEY = 'ff14-planner-class-filter'; function loadPlans() { try { return JSON.parse(localStorage.getItem(PLANNER_KEY) || '[]'); } @@ -255,6 +257,33 @@ function visiblePlanMechanics(plan) { .sort((a, b) => a.timestamp - b.timestamp); } +function plannerClassFilterJobs(plan) { + return [...new Set((plan?.jobComposition ?? []).filter(Boolean))]; +} + +function plannerViewOptions(plan = null) { + const savedJob = localStorage.getItem(PLANNER_CLASS_FILTER_KEY) ?? ''; + const jobs = plan ? plannerClassFilterJobs(plan) : []; + const job = plan && savedJob && !jobs.includes(savedJob) ? '' : savedJob; + if (savedJob && savedJob !== job) localStorage.setItem(PLANNER_CLASS_FILTER_KEY, job); + return { + simple: localStorage.getItem(PLANNER_SIMPLE_VIEW_KEY) === '1', + job, + }; +} + +function renderClassFilterOptions(plan, selectedJob = '') { + const jobs = plannerClassFilterJobs(plan); + return [ + ``, + ...jobs.map(job => ``), + ].join(''); +} + +function assignmentMatchesClassFilter(assignment, job) { + return !job || assignment.job === job; +} + // ── Rendering: Plan List ────────────────────────────────────────────────────── function planItemHtml(p) { @@ -431,6 +460,7 @@ function renderPlanDetail(plan) { content.style.display = ''; const visibleMechanics = visiblePlanMechanics(plan); + const viewOptions = plannerViewOptions(plan); content.innerHTML = `
@@ -485,28 +515,19 @@ function renderPlanDetail(plan) {
Mechaniken
-
- - -
+ +
${renderMechanicListHtml(plan)}
-
`; @@ -523,68 +544,12 @@ function renderPlanDetail(plan) { initTimelineOptions(plan.id); initTimeline(plan.id); initMechanicClicks(plan.id); - initMySpells(plan.id); + initPlannerViewControls(plan.id); initCactbotExport(plan.id); renderInfoPanel(plan); ensureActionMetaLoaded().then(() => refreshMechanicList(plan.id)); } -// ── Meine Spells ───────────────────────────────────────────────────────────── - -function renderMySpellsHtml(plan, job) { - if (!job) return '
Job auswählen um Assignments zu sehen.
'; - - const mechanics = visiblePlanMechanics(plan); - const rows = []; - for (const mechanic of mechanics) { - const mine = (mechanic.assignments ?? []).filter(a => a.job === job); - if (!mine.length) continue; - rows.push({ mechanic, assignments: mine }); - } - - if (!rows.length) { - return `
Keine Assignments für ${escHtml(job)} in diesem Plan.
`; - } - - return rows.map(({ mechanic, assignments }) => { - const time = escHtml(fmtTimestamp(mechanic.timestamp)); - const mechName = escHtml(mechanic.name); - const abilities = assignments.map(a => { - const iconSrc = abilityIcon(a.ability); - const icon = iconSrc - ? `` - : ''; - const name = escHtml(assignmentAbilityName(a, plan)); - const isPersonal = TIMELINE_PERSONAL_ABILITIES.has(a.ability); - const typeClass = a.buffType === 'debuff' ? 'myspells-type--debuff' - : a.buffType === 'shield' ? 'myspells-type--shield' - : isPersonal ? 'myspells-type--personal' - : 'myspells-type--buff'; - return `${icon}${name}`; - }).join(''); - return ` -
- ${time} - ${mechName} -
${abilities}
-
`; - }).join(''); -} - -function mySpellsPlainText(plan, job) { - if (!job) return ''; - const mechanics = visiblePlanMechanics(plan); - const lines = [`Meine Spells — ${job} (${plan.name})`, '─'.repeat(36)]; - for (const mechanic of mechanics) { - const mine = (mechanic.assignments ?? []).filter(a => a.job === job); - if (!mine.length) continue; - const time = fmtTimestamp(mechanic.timestamp); - const names = mine.map(a => assignmentAbilityName(a, plan)).join(', '); - lines.push(`${time.padEnd(6)} ${mechanic.name.padEnd(22)} → ${names}`); - } - return lines.join('\n'); -} - function cactbotEscape(text) { return String(text ?? '') .replace(/\\/g, '\\\\') @@ -900,55 +865,30 @@ function initCactbotExport(planId) { }); } -function initMySpells(planId) { - const viewBtns = document.querySelectorAll('.view-toggle-btn'); - const mechList = document.getElementById('mechanic-list'); - const myPanel = document.getElementById('myspells-panel'); - const jobSelect = document.getElementById('myspells-job-select'); - const myList = document.getElementById('myspells-list'); - const copyBtn = document.getElementById('myspells-copy-btn'); - if (!mechList || !myPanel || !jobSelect || !myList) return; +function initPlannerViewControls(planId) { + const simpleBtn = document.getElementById('planner-simple-view-btn'); + const classFilter = document.getElementById('planner-class-filter'); - viewBtns.forEach(btn => { - btn.addEventListener('click', () => { - viewBtns.forEach(b => b.classList.remove('active')); - btn.classList.add('active'); - const view = btn.dataset.view; - mechList.style.display = view === 'mechanics' ? '' : 'none'; - myPanel.style.display = view === 'myspells' ? '' : 'none'; - if (view === 'myspells') { - const plan = loadPlans().find(p => p.id === planId); - if (plan) myList.innerHTML = renderMySpellsHtml(plan, jobSelect.value || ''); - } - }); + simpleBtn?.addEventListener('click', () => { + const enabled = !simpleBtn.classList.contains('active'); + simpleBtn.classList.toggle('active', enabled); + localStorage.setItem(PLANNER_SIMPLE_VIEW_KEY, enabled ? '1' : '0'); + refreshMechanicList(planId, false); }); - // Gespeicherten Job wiederherstellen - const savedJob = localStorage.getItem('ff14-planner-myspells-job') ?? ''; - if (savedJob && [...jobSelect.options].some(o => o.value === savedJob)) { - jobSelect.value = savedJob; - const plan = loadPlans().find(p => p.id === planId); - if (plan) myList.innerHTML = renderMySpellsHtml(plan, savedJob); - } - - jobSelect.addEventListener('change', () => { - localStorage.setItem('ff14-planner-myspells-job', jobSelect.value); - const plan = loadPlans().find(p => p.id === planId); - if (plan) myList.innerHTML = renderMySpellsHtml(plan, jobSelect.value); + classFilter?.addEventListener('change', () => { + localStorage.setItem(PLANNER_CLASS_FILTER_KEY, classFilter.value); + refreshMechanicList(planId, false); }); +} - copyBtn?.addEventListener('click', () => { - const plan = loadPlans().find(p => p.id === planId); - if (!plan) return; - const text = mySpellsPlainText(plan, jobSelect.value); - navigator.clipboard.writeText(text).then(() => { - copyBtn.textContent = '✓ Kopiert'; - setTimeout(() => { copyBtn.innerHTML = '⎘ Kopieren'; }, 1800); - }).catch(() => { - copyBtn.textContent = '✗ Fehler'; - setTimeout(() => { copyBtn.innerHTML = '⎘ Kopieren'; }, 1800); - }); - }); +function refreshPlannerClassFilter(planId) { + const classFilter = document.getElementById('planner-class-filter'); + const plan = getPlan(planId); + if (!classFilter || !plan) return; + const selected = plannerViewOptions(plan).job; + classFilter.innerHTML = renderClassFilterOptions(plan, selected); + classFilter.value = selected; } function avgNonTankMaxHp(plan) { @@ -1037,6 +977,46 @@ function plannedAssignmentsForMechanic(plan, targetMechanic) { return result; } +function renderSimpleMechanicListHtml(plan, mechanics, selectedJob) { + const rows = []; + for (const mechanic of mechanics) { + const assignments = sortedAssignments(plannedAssignmentsForMechanic(plan, mechanic)) + .filter(a => assignmentMatchesClassFilter(a, selectedJob)); + if (!assignments.length) continue; + rows.push({ mechanic, assignments }); + } + + if (!rows.length) { + const scope = selectedJob ? ` für ${escHtml(selectedJob)}` : ''; + return `
Keine Assignments${scope} in diesem Plan.
`; + } + + return rows.map(({ mechanic, assignments }) => { + const abilities = assignments.map(a => { + const iconSrc = abilityIcon(a.ability); + const icon = iconSrc ? `` : ''; + const name = escHtml(assignmentAbilityName(a, plan)); + const label = selectedJob || !a.job ? name : `${escHtml(a.job)} · ${name}`; + const isPersonal = TIMELINE_PERSONAL_ABILITIES.has(a.ability); + const typeClass = a.buffType === 'debuff' ? 'myspells-type--debuff' + : a.buffType === 'shield' ? 'myspells-type--shield' + : isPersonal ? 'myspells-type--personal' + : 'myspells-type--buff'; + return ` + ${icon}${label} + + `; + }).join(''); + + return ` +
+ ${escHtml(fmtTimestamp(mechanic.timestamp))} + ${escHtml(mechanic.name)} +
${abilities}
+
`; + }).join(''); +} + function renderMechanicListHtml(plan) { const mechanics = visiblePlanMechanics(plan); if (mechanics.length === 0) { @@ -1054,14 +1034,21 @@ function renderMechanicListHtml(plan) { const activeJobSet = new Set(plan.jobComposition.filter(j => j)); const nonTankAvgHp = avgNonTankMaxHp(plan); const tankAvgHp = avgTankMaxHp(plan); + const viewOptions = plannerViewOptions(plan); - return mechanics.map(m => { + if (viewOptions.simple) { + return renderSimpleMechanicListHtml(plan, mechanics, viewOptions.job); + } + + const rows = mechanics.map(m => { // Tankbuster: gespeicherter maxHP des getroffenen Tanks hat Vorrang (präziser als Roster-Durchschnitt) const avgHp = m.isHeavyTankbuster ? ((m.tankMaxHp ?? 0) > 0 ? m.tankMaxHp : tankAvgHp) : nonTankAvgHp; const planned = plannedAssignmentsForMechanic(plan, m); - const sorted = sortedAssignments(planned); + const visiblePlanned = planned.filter(a => assignmentMatchesClassFilter(a, viewOptions.job)); + const sorted = sortedAssignments(visiblePlanned); + if (viewOptions.job && !sorted.length) return ''; const assignHtml = sorted.length === 0 ? 'Keine Zuweisung' : sorted.map(a => { @@ -1120,7 +1107,12 @@ function renderMechanicListHtml(plan) { `; - }).join(''); + }).filter(Boolean); + + if (!rows.length) { + return `
Keine Assignments für ${escHtml(viewOptions.job)} in diesem Plan.
`; + } + return rows.join(''); } // ── Job Slots ───────────────────────────────────────────────────────────────── @@ -1163,6 +1155,7 @@ function initJobSlots(planId) { slot.className = 'job-slot' + (role ? ` job-slot--${role}` : ''); } refreshMechanicList(planId); + refreshPlannerClassFilter(planId); }); } diff --git a/templates/fight-select.php b/templates/fight-select.php deleted file mode 100644 index 37db7ae..0000000 --- a/templates/fight-select.php +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/templates/report-form.php b/templates/report-form.php index 9f0c275..5e2893b 100644 --- a/templates/report-form.php +++ b/templates/report-form.php @@ -1,8 +1,8 @@
Report laden
-
-
+
+
-
- +
+
- Reconnect +
diff --git a/templates/tab-analysis.php b/templates/tab-analysis.php index 840a23b..a62c77c 100644 --- a/templates/tab-analysis.php +++ b/templates/tab-analysis.php @@ -1,3 +1,5 @@ + +