diff --git a/api/abilities.php b/api/abilities.php index eccba47..5e29d8b 100644 --- a/api/abilities.php +++ b/api/abilities.php @@ -13,19 +13,32 @@ $reportCode = preg_replace('/[^a-zA-Z0-9]/', '', $_POST['report_code'] ?? ''); $fightId = (int)($_POST['fight_id'] ?? 0); $startTime = (float)($_POST['start_time'] ?? 0); $endTime = (float)($_POST['end_time'] ?? 0); +$language = strtolower(trim($_POST['language'] ?? 'en')); +$language = in_array($language, ['en', 'de', 'fr', 'jp'], true) ? $language : 'en'; +$translate = 'true'; if (!$reportCode || !$fightId || !$endTime) { http_response_code(400); echo json_encode(['error' => 'Missing params']); exit; } $token = $_SESSION['access_token']; +function localized_graphql_uri(string $language): string { + $host = [ + 'de' => 'de.fflogs.com', + 'fr' => 'fr.fflogs.com', + 'jp' => 'ja.fflogs.com', + ][$language] ?? 'www.fflogs.com'; + return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI); +} + function ab_gql(string $query): array { - global $token; - $ch = curl_init(GRAPHQL_URI); + global $token, $language; + $acceptLanguage = $language === 'jp' ? 'ja' : $language; + $ch = curl_init(localized_graphql_uri($language)); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(['query' => $query]), CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token], + CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token, 'Accept-Language: ' . $acceptLanguage], CURLOPT_SSL_VERIFYPEER => !DEV_MODE, ]); $body = curl_exec($ch); @@ -39,7 +52,7 @@ $mdResult = ab_gql(<< 'de.fflogs.com', + 'fr' => 'fr.fflogs.com', + 'jp' => 'ja.fflogs.com', + ][$language] ?? 'www.fflogs.com'; + return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI); +} + function fflogs_gql(string $query): array { - global $token; - $ch = curl_init(GRAPHQL_URI); + global $token, $language; + $acceptLanguage = $language === 'jp' ? 'ja' : $language; + $ch = curl_init(localized_graphql_uri($language)); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(['query' => $query]), @@ -43,6 +56,7 @@ function fflogs_gql(string $query): array { CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $token, + 'Accept-Language: ' . $acceptLanguage, ], CURLOPT_SSL_VERIFYPEER => !DEV_MODE, ]); @@ -126,6 +140,7 @@ function resolveMitigations(string $buffStr, array $mitigIdMap): array { if (isset($seen[$name])) continue; $seen[$name] = true; $result[] = [ + 'key' => $mitigIdMap[$id]['key'] ?? $name, 'name' => $name, 'dr' => $mitigIdMap[$id]['dr'], 'buffType' => $mitigIdMap[$id]['buffType'], @@ -146,7 +161,7 @@ function shieldsActiveAt(array $shieldTimeline, int $targetId, float $ts, array if ($iv['apply'] <= $ts && ($iv['remove'] === null || $iv['remove'] >= $ts - 200)) { if (isset($mitigIdMap[$statusId])) { $m = $mitigIdMap[$statusId]; - $result[] = ['name' => $m['name'], 'dr' => $m['dr'], 'buffType' => $m['buffType']]; + $result[] = ['key' => $m['key'] ?? $m['name'], 'name' => $m['name'], 'dr' => $m['dr'], 'buffType' => $m['buffType']]; } break; } @@ -161,7 +176,7 @@ $pdResult = fflogs_gql(<< true]); exit; } if (isset($pdResult['_curl_error'])) { http_response_code(502); echo json_encode(['error' => $pdResult['_curl_error']]); exit; } -// abilityGameID → display name +// abilityGameID/statusID → display name $abilityNames = []; foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ?? [] as $ab) { $abilityNames[(int)$ab['gameID']] = $ab['name']; @@ -187,12 +202,13 @@ foreach ($pdResult['data']['reportData']['report']['masterData']['abilities'] ?? $mitigIdMap = []; foreach ($abilityNames as $gameId => $name) { if (isset(MITIGATION_ABILITIES[$name])) { - $mitigIdMap[$gameId] = array_merge(['name' => $name], MITIGATION_ABILITIES[$name]); + $mitigIdMap[$gameId] = array_merge(['key' => $name, 'name' => $name], MITIGATION_ABILITIES[$name]); } } foreach (MITIGATION_ABILITIES as $name => $meta) { if (isset($meta['statusId']) && !isset($mitigIdMap[$meta['statusId']])) { - $mitigIdMap[$meta['statusId']] = array_merge(['name' => $name], $meta); + $displayName = $abilityNames[(int)$meta['statusId']] ?? $name; + $mitigIdMap[$meta['statusId']] = array_merge(['key' => $name, 'name' => $displayName], $meta); } } diff --git a/api/debug-events.php b/api/debug-events.php index 4ab54ab..dc3554b 100644 --- a/api/debug-events.php +++ b/api/debug-events.php @@ -17,6 +17,9 @@ $playerName = trim($_POST['player_name'] ?? ''); $eventType = trim($_POST['event_type'] ?? ''); $abilityId = (int)($_POST['ability_id'] ?? 0); $limit = max(1, min(500, (int)($_POST['limit'] ?? 20))); +$language = strtolower(trim($_POST['language'] ?? 'en')); +$language = in_array($language, ['en', 'de', 'fr', 'jp'], true) ? $language : 'en'; +$translate = 'true'; $startOffset = (float)($_POST['start_offset'] ?? 0) * 1000; // s → ms $endOffset = isset($_POST['end_offset']) && $_POST['end_offset'] !== '' ? (float)$_POST['end_offset'] * 1000 @@ -33,14 +36,24 @@ $queryEnd = min($queryEnd, $endTime); $token = $_SESSION['access_token']; +function localized_graphql_uri(string $language): string { + $host = [ + 'de' => 'de.fflogs.com', + 'fr' => 'fr.fflogs.com', + 'jp' => 'ja.fflogs.com', + ][$language] ?? 'www.fflogs.com'; + return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI); +} + function dbg_gql(string $query): array { - global $token; - $ch = curl_init(GRAPHQL_URI); + global $token, $language; + $acceptLanguage = $language === 'jp' ? 'ja' : $language; + $ch = curl_init(localized_graphql_uri($language)); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode(['query' => $query]), CURLOPT_RETURNTRANSFER => true, - CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token], + CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token, 'Accept-Language: ' . $acceptLanguage], CURLOPT_SSL_VERIFYPEER => !DEV_MODE, ]); $body = curl_exec($ch); @@ -85,6 +98,7 @@ $result = dbg_gql(<< ['reportCode' => $reportCode], ]); -$ch = curl_init(GRAPHQL_URI); +function localized_graphql_uri(string $language): string { + $host = [ + 'de' => 'de.fflogs.com', + 'fr' => 'fr.fflogs.com', + 'jp' => 'ja.fflogs.com', + ][$language] ?? 'www.fflogs.com'; + return preg_replace('#https://[^/]+#', 'https://' . $host, GRAPHQL_URI); +} + +$acceptLanguage = $language === 'jp' ? 'ja' : $language; +$ch = curl_init(localized_graphql_uri($language)); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_POSTFIELDS => $payload, @@ -72,6 +84,7 @@ curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => [ 'Content-Type: application/json', 'Authorization: Bearer ' . $_SESSION['access_token'], + 'Accept-Language: ' . $acceptLanguage, ], CURLOPT_SSL_VERIFYPEER => !DEV_MODE, ]); diff --git a/js/analysis.js b/js/analysis.js index 5dfe911..17a1f0f 100644 --- a/js/analysis.js +++ b/js/analysis.js @@ -219,6 +219,9 @@ if (!refId) { refEvents = []; refFightStart = 0; + refPlayers = []; + window.App.setUrlState?.({ compareReportCode: '', compareFightId: '' }); + renderRefPlayers(); renderTimeline(lastEvents, lastFightStart); return; } @@ -239,25 +242,30 @@ fight_id: refId, start_time: fight.startTime, end_time: fight.endTime, + language: window.App.language, }), }); const json = await res.json(); if (!json.error && !json.reauth) { refEvents = json.aoe_events ?? []; refFightStart = json.fight_start ?? fight.startTime; + refPlayers = []; + window.App.setUrlState?.({ + compareReportCode: '', + compareFightId: refId, + language: window.App.language, + }); } } catch { } refFightSelect.disabled = false; + renderRefPlayers(); renderTimeline(lastEvents, lastFightStart); }); let allSameReportFights = []; function populateRefFightSelect() { - const currentName = (window.App.fights ?? []).find(f => f.id === window.App.fightId)?.name; - const visible = allSameReportFights.filter(f => - f.id !== window.App.fightId && (!currentName || f.name === currentName) - ); + const visible = allSameReportFights.filter(f => f.id !== window.App.fightId); refFightSelect.innerHTML = ''; visible.forEach(f => { const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?'); @@ -292,8 +300,7 @@ refExtPanel.style.display = hidden ? '' : 'none'; }); - refReportLoad.addEventListener('click', async () => { - const code = refReportInput.value.trim(); + async function loadExternalReport(code, preferredFightId = 0) { if (!code) return; refReportLoad.disabled = true; @@ -303,7 +310,7 @@ const res = await fetch('api/fight.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ report_code: code }), + body: new URLSearchParams({ report_code: code, language: window.App.language }), }); const json = await res.json(); if (json.reauth) { window.location.href = 'auth/start.php'; return; } @@ -312,8 +319,7 @@ extFights = fights; extReportCode = code; - const currentName = (window.App.fights ?? []).find(f => f.id === window.App.fightId)?.name; - const visibleExt = currentName ? fights.filter(f => f.name === currentName) : fights; + const visibleExt = fights; refExtFightSelect.innerHTML = ''; visibleExt.forEach(f => { const hp = f.kill ? 'Kill' : (f.fightPercentage != null ? f.fightPercentage.toFixed(2) + '%' : '?'); @@ -323,18 +329,27 @@ refExtFightSelect.appendChild(opt); }); refExtFightSelect.style.display = visibleExt.length ? '' : 'none'; + refExtPanel.style.display = ''; + if (preferredFightId) { + refExtFightSelect.value = String(preferredFightId); + await loadExternalCompare(preferredFightId); + } } catch { } refReportLoad.disabled = false; refReportLoad.textContent = 'Laden'; + } + + refReportLoad.addEventListener('click', async () => { + await loadExternalReport(refReportInput.value.trim()); }); - refExtFightSelect.addEventListener('change', async () => { - const refId = parseInt(refExtFightSelect.value, 10); + async function loadExternalCompare(refId) { if (!refId) { refEvents = []; refFightStart = 0; refPlayers = []; + window.App.setUrlState?.({ compareReportCode: '', compareFightId: '' }); renderRefPlayers(); renderTimeline(lastEvents, lastFightStart); return; @@ -356,6 +371,7 @@ fight_id: refId, start_time: fight.startTime, end_time: fight.endTime, + language: window.App.language, }), }); const json = await res.json(); @@ -363,11 +379,20 @@ refEvents = json.aoe_events ?? []; refFightStart = json.fight_start ?? fight.startTime; refPlayers = json.players ?? []; + window.App.setUrlState?.({ + compareReportCode: extReportCode, + compareFightId: refId, + language: window.App.language, + }); } } catch { } refExtFightSelect.disabled = false; renderRefPlayers(); renderTimeline(lastEvents, lastFightStart); + } + + refExtFightSelect.addEventListener('change', async () => { + await loadExternalCompare(parseInt(refExtFightSelect.value, 10)); }); // ── Timeline rendering ──────────────────────────────────────────────────── @@ -405,24 +430,25 @@ if (!visibleTargets.length) return ''; // Collect boss debuffs (Reprisal/Feint/Addle) once at event level - const seenDebuffNames = new Set(); + const seenDebuffKeys = new Set(); const eventDebuffs = []; for (const t of visibleTargets) { for (const m of (t.mitigations ?? [])) { - if (m.buffType === 'debuff' && !seenDebuffNames.has(m.name)) { - seenDebuffNames.add(m.name); + const key = m.key ?? m.name; + if (m.buffType === 'debuff' && !seenDebuffKeys.has(key)) { + seenDebuffKeys.add(key); eventDebuffs.push(m); } } } const eventMissingDebuffs = refEv - ? (refEv.targets[0]?.mitigations ?? []).filter(m => m.buffType === 'debuff' && !seenDebuffNames.has(m.name)) + ? (refEv.targets[0]?.mitigations ?? []).filter(m => m.buffType === 'debuff' && !seenDebuffKeys.has(m.key ?? m.name)) : []; const debuffIconsHtml = [ ...eventDebuffs.map(m => ({ ...m, missing: false })), ...eventMissingDebuffs.map(m => ({ ...m, missing: true })), ].map(m => { - const iconSrc = MITIG_ICONS[m.name]; + const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name]; if (!iconSrc) return ''; const dr = m.dr > 0 ? ` −${m.dr}%` : ''; return m.missing @@ -447,15 +473,15 @@ `; })() : ''; - const currentMitigNames = new Set((t.mitigations ?? []).map(m => m.name)); + const currentMitigKeys = new Set((t.mitigations ?? []).map(m => m.key ?? m.name)); const refTarget = refEv?.targets?.find(rt => rt.name === t.name); const missingMitigs = refTarget - ? (refTarget.mitigations ?? []).filter(m => m.buffType === 'buff' && !currentMitigNames.has(m.name)) + ? (refTarget.mitigations ?? []).filter(m => m.buffType === 'buff' && !currentMitigKeys.has(m.key ?? m.name)) : []; // DR buff icons (shown below player box) const mitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => { - const iconSrc = MITIG_ICONS[m.name]; + const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name]; if (!iconSrc) return ''; const dr = m.dr > 0 ? ` −${m.dr}%` : ''; return `${m.name}`; @@ -464,7 +490,7 @@ // Shield tooltip on absorbed value const activeShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield'); const missingShields = refTarget - ? (refTarget.mitigations ?? []).filter(m => m.buffType === 'shield' && !currentMitigNames.has(m.name)) + ? (refTarget.mitigations ?? []).filter(m => m.buffType === 'shield' && !currentMitigKeys.has(m.key ?? m.name)) : []; const shieldLines = [ ...activeShields.map(s => s.name), @@ -504,11 +530,11 @@ const currentByName = {}; ev.targets.forEach(t => { currentByName[t.name] = t; }); - const seenRefDebuffNames = new Set(); + const seenRefDebuffKeys = new Set(); const refDebuffIconsHtml = refVisible.flatMap(t => (t.mitigations ?? [])) - .filter(m => m.buffType === 'debuff' && !seenRefDebuffNames.has(m.name) && seenRefDebuffNames.add(m.name)) + .filter(m => m.buffType === 'debuff' && !seenRefDebuffKeys.has(m.key ?? m.name) && seenRefDebuffKeys.add(m.key ?? m.name)) .map(m => { - const iconSrc = MITIG_ICONS[m.name]; + const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name]; if (!iconSrc) return ''; const dr = m.dr > 0 ? ` −${m.dr}%` : ''; return `${m.name}`; @@ -523,13 +549,13 @@ ? `${diff > 0 ? '+' : '-'}${fmtDmg(Math.abs(diff))}` : ''; - const currMitigNames = new Set((curr?.mitigations ?? []).map(m => m.name)); + const currMitigKeys = new Set((curr?.mitigations ?? []).map(m => m.key ?? m.name)); const refMitigIcons = (t.mitigations ?? []).filter(m => m.buffType === 'buff').map(m => { - const iconSrc = MITIG_ICONS[m.name]; + const iconSrc = MITIG_ICONS[m.key] ?? MITIG_ICONS[m.name]; if (!iconSrc) return ''; const dr = m.dr > 0 ? ` −${m.dr}%` : ''; - const missing = !currMitigNames.has(m.name); + const missing = !currMitigKeys.has(m.key ?? m.name); const cls = missing ? ' aoe-buff-ref-unique' : ''; const titleSufx = missing ? ' (fehlt im aktuellen Pull)' : ''; return `${m.name}`; @@ -537,7 +563,7 @@ const refShields = (t.mitigations ?? []).filter(m => m.buffType === 'shield'); const refShieldTitle = refShields.length - ? refShields.map(s => currMitigNames.has(s.name) ? s.name : `${s.name} [fehlt im aktuellen Pull]`).join('\n') + ? refShields.map(s => currMitigKeys.has(s.key ?? s.name) ? s.name : `${s.name} [fehlt im aktuellen Pull]`).join('\n') : null; return ` @@ -609,7 +635,7 @@ const res = await fetch('api/analysis.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ report_code: reportCode, fight_id: fightId, start_time: fightStart, end_time: fightEnd }), + body: new URLSearchParams({ report_code: reportCode, fight_id: fightId, start_time: fightStart, end_time: fightEnd, language: window.App.language }), }); json = await res.json(); } catch (err) { @@ -634,6 +660,18 @@ onFightSelected: load, onTabOpen: load, onFightsLoaded: onFightsLoaded, + async selectSharedCompare(fightId, reportCode = '') { + if (!fightId) return; + if (reportCode && reportCode !== window.App?.reportCode) { + refReportInput.value = reportCode; + await loadExternalReport(reportCode, fightId); + return; + } + if ([...refFightSelect.options].some(opt => parseInt(opt.value, 10) === fightId)) { + refFightSelect.value = String(fightId); + refFightSelect.dispatchEvent(new Event('change')); + } + }, reset() { lastFightId = null; refEvents = []; diff --git a/js/app.js b/js/app.js index 5dde654..3389b1a 100644 --- a/js/app.js +++ b/js/app.js @@ -1,5 +1,5 @@ document.addEventListener('DOMContentLoaded', () => { - window.App = { reportCode: null, fightId: null, fightStart: 0, fightEnd: 0, phases: [], fights: [] }; + window.App = { reportCode: null, fightId: null, fightStart: 0, fightEnd: 0, language: 'en', phases: [], fights: [] }; const form = document.getElementById('report-form'); const output = document.getElementById('output'); @@ -7,6 +7,7 @@ document.addEventListener('DOMContentLoaded', () => { const initialHint = document.getElementById('initial-hint'); const fightSelectCard = document.getElementById('fight-select-card'); const fightSelect = document.getElementById('fight-select'); + const languageSelect = document.getElementById('language-select'); const explorerCard = document.getElementById('event-explorer-card'); const exLoadBtn = document.getElementById('ex-load-btn'); const exAbilitySelect = document.getElementById('ex-ability'); @@ -14,6 +15,68 @@ document.addEventListener('DOMContentLoaded', () => { let allFights = []; + function getUrlState() { + const params = new URLSearchParams(window.location.search); + const pick = (...names) => { + for (const name of names) { + const value = params.get(name); + if (value !== null && value !== '') return value; + } + return ''; + }; + return { + reportCode: pick('report_code', 'reportCode', 'report'), + fightId: parseInt(pick('fightid', 'fight_id', 'fightId'), 10) || 0, + compareReportCode: pick('compare_report_code', 'compareReportCode', 'compare_report', 'ref_report'), + compareFightId: parseInt(pick('comparefightid', 'compare_fight_id', 'compareFightId', 'ref_fight_id'), 10) || 0, + language: pick('language', 'lang'), + translate: pick('translate'), + }; + } + + function normalizeLanguage(value, fallback = 'en') { + const lang = String(value || '').toLowerCase(); + return ['en', 'de', 'fr', 'jp'].includes(lang) ? lang : fallback; + } + + function setUrlState(updates) { + const url = new URL(window.location.href); + const setOrDelete = (name, value) => { + if (value === null || value === undefined || value === '') url.searchParams.delete(name); + else url.searchParams.set(name, value); + }; + + if ('reportCode' in updates) setOrDelete('report_code', updates.reportCode); + if ('fightId' in updates) setOrDelete('fightid', updates.fightId); + if ('compareReportCode' in updates) setOrDelete('compare_report_code', updates.compareReportCode); + if ('compareFightId' in updates) setOrDelete('comparefightid', updates.compareFightId); + if ('language' in updates) { + setOrDelete('language', normalizeLanguage(updates.language)); + url.searchParams.delete('translate'); + } + + window.history.replaceState(null, '', url); + } + window.App.setUrlState = setUrlState; + + const initialUrlState = getUrlState(); + const storedLanguage = localStorage.getItem('ff14-mitigator-language'); + const legacyTranslateLanguage = initialUrlState.translate === '1' ? 'de' : 'en'; + languageSelect.value = normalizeLanguage( + initialUrlState.language || storedLanguage, + initialUrlState.translate !== '' ? legacyTranslateLanguage : 'en' + ); + window.App.language = languageSelect.value; + + languageSelect.addEventListener('change', () => { + window.App.language = normalizeLanguage(languageSelect.value); + localStorage.setItem('ff14-mitigator-language', window.App.language); + setUrlState({ language: window.App.language }); + if (window.App.reportCode) { + loadReport(window.App.reportCode, window.App.fightId); + } + }); + const codeInput = form.elements['report_code']; codeInput.addEventListener('input', () => { const match = codeInput.value.match(/fflogs\.com\/reports\/([A-Za-z0-9]+)/); @@ -38,12 +101,15 @@ document.addEventListener('DOMContentLoaded', () => { outputCard.style.display = 'block'; } - fightSelect.addEventListener('change', () => { - if (!fightSelect.value) return; - const id = parseInt(fightSelect.value, 10); - const fight = allFights.find(f => f.id === id); - if (!fight) return; + function openAnalysisTab() { + document.querySelector('.tabs .tab[data-tab="analysis"]')?.click(); + } + function selectFight(id, updateUrl = true) { + const fight = allFights.find(f => f.id === id); + if (!fight) return false; + + fightSelect.value = String(id); window.App.fightId = id; window.App.fightStart = fight.startTime; window.App.fightEnd = fight.endTime; @@ -51,8 +117,21 @@ document.addEventListener('DOMContentLoaded', () => { displayFight(fight); explorerCard.style.display = 'block'; + if (updateUrl) { + setUrlState({ + reportCode: window.App.reportCode, + fightId: id, + language: window.App.language, + }); + } window.analysisTab?.onFightSelected?.(); loadAbilities(id, fight.startTime, fight.endTime); + return true; + } + + fightSelect.addEventListener('change', () => { + if (!fightSelect.value) return; + selectFight(parseInt(fightSelect.value, 10)); }); function buildPhases(fight) { @@ -78,6 +157,7 @@ document.addEventListener('DOMContentLoaded', () => { fight_id: fightId, start_time: startTime, end_time: endTime, + language: window.App.language, }), }); const json = await res.json(); @@ -113,6 +193,7 @@ document.addEventListener('DOMContentLoaded', () => { fight_id: window.App.fightId, start_time: window.App.fightStart, end_time: window.App.fightEnd, + language: window.App.language, data_type: document.getElementById('ex-data-type').value, ability_id: exAbilitySelect.value, event_type: document.getElementById('ex-event-type').value.trim(), @@ -136,9 +217,7 @@ document.addEventListener('DOMContentLoaded', () => { } }); - form.addEventListener('submit', async (e) => { - e.preventDefault(); - + async function loadReport(reportCode, preferredFightId = 0) { initialHint.style.display = 'none'; outputCard.style.display = 'block'; output.textContent = '// fetching...'; @@ -147,21 +226,29 @@ document.addEventListener('DOMContentLoaded', () => { fightSelect.innerHTML = ''; allFights = []; - const reportCode = form.elements['report_code'].value.trim(); window.App.reportCode = reportCode; window.App.fightId = null; window.App.fightStart = 0; window.App.fightEnd = 0; + window.App.language = normalizeLanguage(languageSelect.value); window.App.phases = []; window.App.fights = []; window.analysisTab?.reset?.(); + localStorage.setItem('ff14-mitigator-language', window.App.language); + setUrlState({ + reportCode, + fightId: '', + compareReportCode: '', + compareFightId: '', + language: window.App.language, + }); let response, json; try { response = await fetch('api/fight.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, - body: new URLSearchParams({ report_code: reportCode }), + body: new URLSearchParams({ report_code: reportCode, language: window.App.language }), }); json = await response.json(); } catch (err) { @@ -204,7 +291,26 @@ document.addEventListener('DOMContentLoaded', () => { window.App.fights = allFights; fightSelectCard.style.display = 'block'; - output.textContent = '// Fight auswählen ↑'; window.analysisTab?.onFightsLoaded?.(allFights); + + if (preferredFightId && selectFight(preferredFightId, true)) return; + output.textContent = '// Fight auswählen ↑'; + } + + form.addEventListener('submit', async (e) => { + e.preventDefault(); + await loadReport(form.elements['report_code'].value.trim()); }); + + if (initialUrlState.reportCode) { + form.elements['report_code'].value = initialUrlState.reportCode; + loadReport(initialUrlState.reportCode, initialUrlState.fightId).then(() => { + if (initialUrlState.fightId) { + openAnalysisTab(); + } + if (initialUrlState.compareFightId) { + window.analysisTab?.selectSharedCompare?.(initialUrlState.compareFightId, initialUrlState.compareReportCode); + } + }); + } }); diff --git a/templates/report-form.php b/templates/report-form.php index abb662c..09da893 100644 --- a/templates/report-form.php +++ b/templates/report-form.php @@ -13,6 +13,15 @@ required > +
+ + +
Reconnect