284 lines
11 KiB
JavaScript
284 lines
11 KiB
JavaScript
(() => {
|
|
const toast = document.querySelector('[data-toast]');
|
|
if (toast) {
|
|
const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
const region = toast.closest('.toast-region');
|
|
const duration = toast.classList.contains('toast-error') ? 6500 : 4800;
|
|
let remaining = duration;
|
|
let startedAt = 0;
|
|
let dismissTimer;
|
|
let timerRunning = false;
|
|
|
|
const dismiss = () => {
|
|
if (!toast.isConnected || toast.classList.contains('is-leaving')) return;
|
|
window.clearTimeout(dismissTimer);
|
|
timerRunning = false;
|
|
toast.classList.add('is-leaving');
|
|
if (reducedMotion) {
|
|
region?.remove();
|
|
return;
|
|
}
|
|
window.setTimeout(() => region?.remove(), 220);
|
|
};
|
|
|
|
const resume = () => {
|
|
if (timerRunning || toast.classList.contains('is-leaving')) return;
|
|
toast.classList.remove('is-paused');
|
|
startedAt = performance.now();
|
|
timerRunning = true;
|
|
dismissTimer = window.setTimeout(dismiss, remaining);
|
|
};
|
|
|
|
const pause = () => {
|
|
if (!timerRunning || toast.classList.contains('is-leaving')) return;
|
|
remaining = Math.max(0, remaining - (performance.now() - startedAt));
|
|
window.clearTimeout(dismissTimer);
|
|
timerRunning = false;
|
|
toast.classList.add('is-paused');
|
|
};
|
|
|
|
toast.querySelector('[data-toast-close]')?.addEventListener('click', dismiss);
|
|
toast.addEventListener('pointerenter', pause);
|
|
toast.addEventListener('pointerleave', () => {
|
|
if (!toast.contains(document.activeElement)) resume();
|
|
});
|
|
toast.addEventListener('focusin', pause);
|
|
toast.addEventListener('focusout', () => {
|
|
window.requestAnimationFrame(() => {
|
|
if (!toast.matches(':hover') && !toast.contains(document.activeElement)) resume();
|
|
});
|
|
});
|
|
resume();
|
|
}
|
|
|
|
const currentURL = new URL(window.location.href);
|
|
if (currentURL.searchParams.has('notice')) {
|
|
currentURL.searchParams.delete('notice');
|
|
currentURL.searchParams.delete('notice_kind');
|
|
const cleanLocation = `${currentURL.pathname}${currentURL.search}${currentURL.hash}`;
|
|
window.history.replaceState(window.history.state, '', cleanLocation);
|
|
}
|
|
|
|
const settingsForm = document.querySelector('[data-settings-form]');
|
|
if (settingsForm) {
|
|
const updaterToggle = settingsForm.querySelector('[data-updater-toggle]');
|
|
const ipv6Toggle = settingsForm.querySelector('[data-ipv6-toggle]');
|
|
const ipv6Settings = settingsForm.querySelector('[data-ipv6-settings]');
|
|
|
|
const syncToggleCard = (input) => {
|
|
if (!input) return;
|
|
const card = input.closest('.setting-switch-card');
|
|
card?.classList.toggle('is-enabled', input.checked);
|
|
const label = input.closest('.setting-switch')?.querySelector('b');
|
|
if (label) label.textContent = input.checked ? 'Aktiv' : 'Inaktiv';
|
|
};
|
|
|
|
const syncIPv6Settings = () => {
|
|
if (!ipv6Toggle || !ipv6Settings) return;
|
|
ipv6Settings.classList.toggle('is-disabled', !ipv6Toggle.checked);
|
|
ipv6Settings.setAttribute('aria-disabled', String(!ipv6Toggle.checked));
|
|
const textarea = ipv6Settings.querySelector('textarea');
|
|
if (textarea) textarea.required = ipv6Toggle.checked;
|
|
};
|
|
|
|
updaterToggle?.addEventListener('change', () => syncToggleCard(updaterToggle));
|
|
ipv6Toggle?.addEventListener('change', () => {
|
|
syncToggleCard(ipv6Toggle);
|
|
syncIPv6Settings();
|
|
});
|
|
syncToggleCard(updaterToggle);
|
|
syncToggleCard(ipv6Toggle);
|
|
syncIPv6Settings();
|
|
}
|
|
|
|
for (const form of document.querySelectorAll('[data-target-form]')) {
|
|
const active = form.querySelector('[data-target-active]');
|
|
const staticFields = form.querySelector('[data-static-ip-fields]');
|
|
const ipv4Toggle = form.querySelector('[name="update_ipv4"]');
|
|
const ipv6Toggle = form.querySelector('[name="update_ipv6"]');
|
|
const staticIPv4 = form.querySelector('[data-static-ipv4]');
|
|
const staticIPv6 = form.querySelector('[data-static-ipv6]');
|
|
const requirement = form.querySelector('[data-target-requirement]');
|
|
const submit = form.querySelector('button[type="submit"]');
|
|
const ipv6Enabled = form.dataset.ipv6Enabled !== 'false';
|
|
const ipv4Selected = () => ipv4Toggle ? ipv4Toggle.checked : form.dataset.updateIpv4 === 'true';
|
|
const ipv6Selected = () => ipv6Enabled && (ipv6Toggle ? ipv6Toggle.checked : form.dataset.updateIpv6 === 'true');
|
|
|
|
const syncStaticFields = () => {
|
|
if (!active || !staticFields) return;
|
|
const useIPv4 = ipv4Selected();
|
|
const useIPv6 = ipv6Selected();
|
|
const hasFamily = useIPv4 || useIPv6;
|
|
if (active.checked && !hasFamily) active.checked = false;
|
|
|
|
active.disabled = !hasFamily;
|
|
active.closest('.switch')?.classList.toggle('switch-disabled', !hasFamily);
|
|
|
|
const useStatic = !active.checked && hasFamily;
|
|
staticFields.hidden = !useStatic;
|
|
staticFields.setAttribute('aria-hidden', String(!useStatic));
|
|
if (staticIPv4) {
|
|
staticIPv4.disabled = !useStatic || !useIPv4;
|
|
staticIPv4.required = useStatic && useIPv4;
|
|
}
|
|
if (staticIPv6) {
|
|
staticIPv6.disabled = !useStatic || !useIPv6;
|
|
staticIPv6.required = useStatic && useIPv6;
|
|
}
|
|
|
|
const missingStaticIP = useStatic && (
|
|
(useIPv4 && !staticIPv4?.value.trim()) ||
|
|
(useIPv6 && !staticIPv6?.value.trim())
|
|
);
|
|
if (submit) submit.disabled = missingStaticIP;
|
|
if (requirement) requirement.hidden = !missingStaticIP;
|
|
};
|
|
|
|
active?.addEventListener('change', syncStaticFields);
|
|
for (const familyToggle of [ipv4Toggle, ipv6Toggle]) {
|
|
familyToggle?.addEventListener('change', () => {
|
|
if (!familyToggle.checked && active) active.checked = false;
|
|
syncStaticFields();
|
|
});
|
|
}
|
|
staticIPv4?.addEventListener('input', syncStaticFields);
|
|
staticIPv6?.addEventListener('input', syncStaticFields);
|
|
syncStaticFields();
|
|
}
|
|
|
|
for (const zone of document.querySelectorAll('[data-access-zone]')) {
|
|
const access = zone.querySelector('[data-zone-access]');
|
|
const manage = zone.querySelector('[data-zone-manage]');
|
|
const create = zone.querySelector('[data-zone-create]');
|
|
access?.addEventListener('change', () => {
|
|
if (!access.checked) {
|
|
if (manage) manage.checked = false;
|
|
if (create) create.checked = false;
|
|
}
|
|
});
|
|
manage?.addEventListener('change', () => {
|
|
if (manage.checked && access) access.checked = true;
|
|
if (!manage.checked && create) create.checked = false;
|
|
});
|
|
create?.addEventListener('change', () => {
|
|
if (create.checked) {
|
|
if (access) access.checked = true;
|
|
if (manage) manage.checked = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
for (const target of document.querySelectorAll('[data-access-target]')) {
|
|
const access = target.querySelector('[data-target-access]');
|
|
const manage = target.querySelector('[data-target-manage]');
|
|
access?.addEventListener('change', () => {
|
|
if (!access.checked && manage) manage.checked = false;
|
|
});
|
|
manage?.addEventListener('change', () => {
|
|
if (manage.checked && access) access.checked = true;
|
|
});
|
|
}
|
|
|
|
for (const form of document.querySelectorAll('[data-dirty-form]')) {
|
|
const submit = form.querySelector('[data-dirty-submit]');
|
|
if (!submit) continue;
|
|
const state = () => new URLSearchParams(new FormData(form)).toString();
|
|
const initialState = state();
|
|
const syncDirtyState = () => {
|
|
const dirty = state() !== initialState;
|
|
submit.disabled = !dirty;
|
|
submit.setAttribute('aria-disabled', String(!dirty));
|
|
};
|
|
form.addEventListener('input', syncDirtyState);
|
|
form.addEventListener('change', syncDirtyState);
|
|
form.addEventListener('submit', (event) => {
|
|
syncDirtyState();
|
|
if (submit.disabled) event.preventDefault();
|
|
});
|
|
syncDirtyState();
|
|
}
|
|
|
|
for (const details of document.querySelectorAll('details[data-scroll-page-end]')) {
|
|
details.addEventListener('toggle', () => {
|
|
if (!details.open) return;
|
|
window.requestAnimationFrame(() => {
|
|
window.scrollTo({
|
|
top: document.documentElement.scrollHeight,
|
|
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
const copyButtons = document.querySelectorAll('[data-copy-target]');
|
|
for (const button of copyButtons) {
|
|
button.addEventListener('click', async () => {
|
|
const target = document.getElementById(button.dataset.copyTarget);
|
|
if (!target) return;
|
|
try {
|
|
await navigator.clipboard.writeText(target.textContent);
|
|
const original = button.textContent;
|
|
button.textContent = 'Kopiert';
|
|
window.setTimeout(() => { button.textContent = original; }, 2000);
|
|
} catch {
|
|
button.textContent = 'Manuell kopieren';
|
|
}
|
|
});
|
|
}
|
|
|
|
const countdowns = [...document.querySelectorAll('[data-countdown]')];
|
|
if (countdowns.length === 0) return;
|
|
|
|
let nextRun = Number(countdowns[0].dataset.countdown || 0);
|
|
let running = countdowns[0].dataset.running === 'true';
|
|
let enabled = countdowns[0].dataset.enabled !== 'false';
|
|
let lastRefresh = 0;
|
|
|
|
const formatRemaining = (seconds) => {
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
const rest = seconds % 60;
|
|
if (hours > 0) return `${hours}:${String(minutes).padStart(2, '0')}:${String(rest).padStart(2, '0')}`;
|
|
return `${String(minutes).padStart(2, '0')}:${String(rest).padStart(2, '0')}`;
|
|
};
|
|
|
|
const refreshSchedule = async () => {
|
|
const now = Date.now();
|
|
if (now - lastRefresh < 2500) return;
|
|
lastRefresh = now;
|
|
try {
|
|
const response = await fetch('/api/v1/scheduler', {headers: {'Accept': 'application/json'}, cache: 'no-store'});
|
|
if (!response.ok) return;
|
|
const state = await response.json();
|
|
nextRun = Number(state.next_run_unix || 0);
|
|
running = Boolean(state.running);
|
|
enabled = state.enabled !== false;
|
|
} catch {
|
|
// The visible countdown keeps its last known state when the status request fails.
|
|
}
|
|
};
|
|
|
|
const tick = () => {
|
|
if (!enabled) {
|
|
for (const node of countdowns) {
|
|
node.textContent = 'Inaktiv';
|
|
node.setAttribute('aria-label', 'Automatischer Updater ist deaktiviert');
|
|
}
|
|
return;
|
|
}
|
|
const now = Math.floor(Date.now() / 1000);
|
|
const remaining = Math.max(0, nextRun - now);
|
|
const label = running ? 'Update läuft' : (nextRun > 0 ? formatRemaining(remaining) : '--:--');
|
|
for (const node of countdowns) {
|
|
node.textContent = label;
|
|
node.setAttribute('aria-label', running ? 'Update läuft' : `Nächste Aktualisierung in ${remaining} Sekunden`);
|
|
}
|
|
if (running || nextRun === 0 || remaining === 0) refreshSchedule();
|
|
};
|
|
|
|
tick();
|
|
refreshSchedule();
|
|
window.setInterval(tick, 1000);
|
|
window.setInterval(refreshSchedule, 30000);
|
|
})();
|