Add support badges and audio processing

This commit is contained in:
Skrockle
2026-06-08 19:10:23 +02:00
parent da12dc07a7
commit 7bba36c505
51 changed files with 1424 additions and 46 deletions
+1
View File
@@ -121,5 +121,6 @@ Both the official KoalaSync website and the **v2.0 Browser Extension** feature f
<div align="center">
<sub>Built with ❤️ by <a href="https://github.com/Shik3i">Shik3i</a>. KoalaSync is Open Source under the <a href="LICENSE">MIT License</a>.</sub>
<sub><a href="https://ko-fi.com/koaladev"><img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi"></a></sub>
<sub><a href="https://gitgem.org/github/Shik3i/KoalaSync"><img src="https://gitgem.org/api/badge/github/Shik3i/KoalaSync.svg" alt="GitGem Badge" /></a></sub>
</div>
+224
View File
@@ -0,0 +1,224 @@
:root {
--bg: #0f172a;
--card: #1e293b;
--panel: #172033;
--accent: #6366f1;
--accent-hover: #818cf8;
--text: #f8fafc;
--text-muted: #94a3b8;
--border: #334155;
--radius: 8px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
}
.page-shell {
width: min(760px, calc(100vw - 32px));
margin: 0 auto;
padding: 32px 0;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 18px;
}
.eyebrow {
margin: 0 0 4px;
color: var(--text-muted);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1,
h2,
p {
margin: 0;
}
h1 {
color: var(--accent-hover);
font-size: 28px;
font-weight: 800;
}
h2 {
font-size: 16px;
}
.panel {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px;
margin-bottom: 14px;
}
.muted-panel {
color: var(--text-muted);
}
.section-heading,
.master-toggle,
.inline-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.master-toggle,
.inline-toggle {
color: var(--text-muted);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
.preset-group {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(148px, 1fr));
gap: 10px;
margin: 18px 0;
}
.preset-card {
min-height: 44px;
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
cursor: pointer;
}
.preset-card:has(input:checked) {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(99, 102, 241, 0.35);
}
.preset-card input {
accent-color: var(--accent);
}
.custom-grid {
display: grid;
gap: 12px;
padding: 14px;
background: rgba(15, 23, 42, 0.58);
border: 1px solid rgba(148, 163, 184, 0.16);
border-radius: var(--radius);
}
.control-row {
display: grid;
grid-template-columns: 110px minmax(160px, 1fr) 78px 30px;
align-items: center;
gap: 10px;
}
.control-row label {
color: var(--text-muted);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
input[type="range"] {
width: 100%;
accent-color: var(--accent);
}
input[type="number"] {
width: 100%;
padding: 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text);
font: inherit;
}
.unit {
color: var(--text-muted);
font-size: 12px;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 38px;
height: 22px;
flex: 0 0 auto;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.slider {
position: absolute;
cursor: pointer;
inset: 0;
background-color: #334155;
transition: .3s;
border-radius: 22px;
}
.slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 3px;
bottom: 3px;
background-color: #94a3b8;
transition: .3s;
border-radius: 50%;
}
input:checked + .slider {
background-color: var(--accent);
}
input:checked + .slider:before {
transform: translateX(16px);
background-color: white;
}
@media (max-width: 620px) {
.page-header,
.section-heading {
align-items: flex-start;
flex-direction: column;
}
.control-row {
grid-template-columns: 1fr 74px 28px;
}
.control-row label {
grid-column: 1 / -1;
}
}
+103
View File
@@ -0,0 +1,103 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>KoalaSync Audio Settings</title>
<link rel="stylesheet" href="audio-options.css">
</head>
<body>
<main class="page-shell">
<header class="page-header">
<div>
<p class="eyebrow">KoalaSync</p>
<h1 data-i18n="AUDIO_PAGE_TITLE">Audio Settings</h1>
</div>
<label class="master-toggle">
<span data-i18n="AUDIO_MASTER_TOGGLE">Audio Processing</span>
<span class="toggle-switch">
<input type="checkbox" id="audioEnabled">
<span class="slider"></span>
</span>
</label>
</header>
<section class="panel">
<div class="section-heading">
<h2 data-i18n="AUDIO_COMPRESSOR">Compressor</h2>
<label class="inline-toggle">
<span data-i18n="AUDIO_COMPRESSOR_ENABLE">Enabled</span>
<span class="toggle-switch">
<input type="checkbox" id="compressorEnabled">
<span class="slider"></span>
</span>
</label>
</div>
<div class="preset-group" role="radiogroup" aria-label="Compressor preset">
<label class="preset-card">
<input type="radio" name="preset" value="recommended">
<span data-i18n="AUDIO_PRESET_RECOMMENDED">Recommended</span>
</label>
<label class="preset-card">
<input type="radio" name="preset" value="dynamicRange">
<span data-i18n="AUDIO_PRESET_DYNAMIC_RANGE">Dynamic Range</span>
</label>
<label class="preset-card">
<input type="radio" name="preset" value="vocalEnhancement">
<span data-i18n="AUDIO_PRESET_VOCAL_ENHANCEMENT">Vocal Enhancement</span>
</label>
<label class="preset-card">
<input type="radio" name="preset" value="smooth">
<span data-i18n="AUDIO_PRESET_SMOOTH">Smooth</span>
</label>
<label class="preset-card">
<input type="radio" name="preset" value="custom">
<span data-i18n="AUDIO_PRESET_CUSTOM">Custom</span>
</label>
</div>
<div class="custom-grid" id="customControls">
<div class="control-row" data-param="threshold">
<label data-i18n="AUDIO_PARAM_THRESHOLD">Threshold</label>
<input type="range" min="-60" max="0" step="1">
<input type="number" min="-60" max="0" step="1">
<span class="unit">dB</span>
</div>
<div class="control-row" data-param="knee">
<label data-i18n="AUDIO_PARAM_KNEE">Knee</label>
<input type="range" min="0" max="40" step="1">
<input type="number" min="0" max="40" step="1">
<span class="unit">dB</span>
</div>
<div class="control-row" data-param="ratio">
<label data-i18n="AUDIO_PARAM_RATIO">Ratio</label>
<input type="range" min="1" max="20" step="0.5">
<input type="number" min="1" max="20" step="0.5">
<span class="unit">:1</span>
</div>
<div class="control-row" data-param="attack">
<label data-i18n="AUDIO_PARAM_ATTACK">Attack</label>
<input type="range" min="0" max="1" step="0.001">
<input type="number" min="0" max="1000" step="1" data-ms-input="true">
<span class="unit">ms</span>
</div>
<div class="control-row" data-param="release">
<label data-i18n="AUDIO_PARAM_RELEASE">Release</label>
<input type="range" min="0" max="1" step="0.005">
<input type="number" min="0" max="1000" step="5" data-ms-input="true">
<span class="unit">ms</span>
</div>
</div>
</section>
<section class="panel muted-panel">
<div class="section-heading">
<h2 data-i18n="AUDIO_EQUALIZER">Equalizer</h2>
</div>
<p data-i18n="AUDIO_COMING_SOON">Coming soon</p>
</section>
</main>
<script src="audio-options.js" type="module"></script>
</body>
</html>
+183
View File
@@ -0,0 +1,183 @@
import { loadLocale, translateDOM, getSystemLanguage } from './i18n.js';
const PRESETS = {
recommended: { threshold: -24, ratio: 8, attack: 0.010, release: 0.300, knee: 15 },
dynamicRange: { threshold: -18, ratio: 4, attack: 0.020, release: 0.200, knee: 10 },
vocalEnhancement: { threshold: -12, ratio: 3, attack: 0.015, release: 0.150, knee: 5 },
smooth: { threshold: -30, ratio: 1.5, attack: 0.030, release: 0.250, knee: 20 },
custom: { threshold: -24, ratio: 12, attack: 0.003, release: 0.250, knee: 30 }
};
const DEFAULT_AUDIO_SETTINGS = {
enabled: false,
compressor: {
enabled: false,
preset: 'recommended',
customParams: { ...PRESETS.custom }
}
};
const PARAM_LIMITS = {
threshold: { min: -60, max: 0 },
knee: { min: 0, max: 40 },
ratio: { min: 1, max: 20 },
attack: { min: 0, max: 1 },
release: { min: 0, max: 1 }
};
const elements = {
audioEnabled: document.getElementById('audioEnabled'),
compressorEnabled: document.getElementById('compressorEnabled'),
presetInputs: Array.from(document.querySelectorAll('input[name="preset"]')),
controlRows: Array.from(document.querySelectorAll('.control-row'))
};
let saveTimer = null;
let isRendering = false;
function cloneDefaultSettings() {
return JSON.parse(JSON.stringify(DEFAULT_AUDIO_SETTINGS));
}
let currentSettings = cloneDefaultSettings();
function mergeAudioSettings(settings = {}) {
const safeSettings = settings && typeof settings === 'object' ? settings : {};
const defaults = cloneDefaultSettings();
return {
...defaults,
...safeSettings,
compressor: {
...defaults.compressor,
...(safeSettings.compressor || {}),
customParams: {
...defaults.compressor.customParams,
...(safeSettings.compressor?.customParams || {})
}
}
};
}
function debounceSave() {
if (saveTimer) clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
chrome.storage.sync.set({ audioSettings: currentSettings });
}, 40);
}
function getParamValue(param, value, isMsInput = false) {
const parsed = Number(value);
const candidate = Number.isFinite(parsed)
? (isMsInput ? parsed / 1000 : parsed)
: currentSettings.compressor.customParams[param];
const limits = PARAM_LIMITS[param];
if (!limits) return candidate;
return Math.min(limits.max, Math.max(limits.min, candidate));
}
function formatNumber(value, param, isMsInput = false) {
if (isMsInput) return Math.round(value * 1000);
if (param === 'ratio') return Number(value).toFixed(1).replace(/\.0$/, '');
return value;
}
function render() {
isRendering = true;
elements.audioEnabled.checked = currentSettings.enabled === true;
elements.compressorEnabled.checked = currentSettings.compressor.enabled === true;
const selectedPreset = currentSettings.compressor.preset || 'recommended';
elements.presetInputs.forEach(input => {
input.checked = input.value === selectedPreset;
});
const params = selectedPreset === 'custom'
? currentSettings.compressor.customParams
: PRESETS[selectedPreset] || PRESETS.recommended;
elements.controlRows.forEach(row => {
const param = row.dataset.param;
const range = row.querySelector('input[type="range"]');
const number = row.querySelector('input[type="number"]');
const value = params[param];
range.value = value;
number.value = formatNumber(value, param, number.dataset.msInput === 'true');
});
isRendering = false;
}
function setPreset(preset) {
currentSettings.compressor.preset = preset;
if (preset === 'custom') {
currentSettings.compressor.customParams = {
...PRESETS.custom,
...currentSettings.compressor.customParams
};
}
render();
debounceSave();
}
function setCustomParam(param, value) {
currentSettings.compressor.preset = 'custom';
currentSettings.compressor.customParams[param] = getParamValue(param, value);
render();
debounceSave();
}
async function init() {
const syncData = await chrome.storage.sync.get(['audioSettings', 'locale']);
const lang = syncData.locale || getSystemLanguage();
await loadLocale(lang);
translateDOM();
currentSettings = mergeAudioSettings(syncData.audioSettings);
render();
}
elements.audioEnabled.addEventListener('change', () => {
currentSettings.enabled = elements.audioEnabled.checked;
if (currentSettings.enabled && !currentSettings.compressor.enabled) {
currentSettings.compressor.enabled = true;
}
render();
debounceSave();
});
elements.compressorEnabled.addEventListener('change', () => {
currentSettings.compressor.enabled = elements.compressorEnabled.checked;
if (currentSettings.compressor.enabled) currentSettings.enabled = true;
render();
debounceSave();
});
elements.presetInputs.forEach(input => {
input.addEventListener('change', () => {
if (input.checked) setPreset(input.value);
});
});
elements.controlRows.forEach(row => {
const param = row.dataset.param;
const range = row.querySelector('input[type="range"]');
const number = row.querySelector('input[type="number"]');
range.addEventListener('input', () => {
if (isRendering) return;
setCustomParam(param, getParamValue(param, range.value));
});
number.addEventListener('input', () => {
if (isRendering) return;
setCustomParam(param, getParamValue(param, number.value, number.dataset.msInput === 'true'));
});
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync' || !changes.audioSettings) return;
currentSettings = mergeAudioSettings(changes.audioSettings.newValue);
render();
});
init().catch(err => {
console.error('[AudioOptions] Failed to initialize:', err);
});
+41 -5
View File
@@ -1292,6 +1292,20 @@ function leaveOldRoomIfSwitching(newRoomId) {
}
}
function resetAudioProcessingInTab(tabId) {
if (!tabId) return;
chrome.tabs.sendMessage(tabId, { action: 'RESET_AUDIO_PROCESSING' }).catch(() => {});
}
async function applyAudioSettingsToTab(tabId) {
if (!tabId) return;
const data = await chrome.storage.sync.get(['audioSettings']);
chrome.tabs.sendMessage(tabId, {
action: 'APPLY_AUDIO_SETTINGS',
settings: data.audioSettings
}).catch(() => {});
}
// --- Extension Message Listeners ---
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
handleAsyncMessage(message, sender, sendResponse);
@@ -1354,6 +1368,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
roomPassword: currentRoom ? currentRoom.password : null
});
} else if (message.type === 'LEAVE_ROOM') {
resetAudioProcessingInTab(currentTabId);
emit(EVENTS.LEAVE_ROOM, { peerId });
currentRoom = null;
currentTabId = null;
@@ -1599,6 +1614,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
sendResponse({ status: 'ok' });
});
} else if (message.type === 'SET_TARGET_TAB') {
const previousTabId = currentTabId;
currentTabId = message.tabId;
currentTabTitle = message.tabTitle;
lastContentHeartbeatAt = null;
@@ -1607,14 +1623,21 @@ async function handleAsyncMessage(message, sender, sendResponse) {
}
chrome.storage.session.set({ currentTabId, currentTabTitle, roomIdleSince, lastContentHeartbeatAt });
updateBadgeStatus();
if (previousTabId && previousTabId !== currentTabId) {
resetAudioProcessingInTab(previousTabId);
}
if (currentTabId) {
const selectedTabId = currentTabId;
chrome.scripting.executeScript({
target: { tabId: currentTabId },
target: { tabId: selectedTabId },
files: ['content.js']
}).catch(err => {
addLog(`Failed to inject into tab: ${err.message}`, 'warn');
});
})
.then(() => applyAudioSettingsToTab(selectedTabId))
.catch(err => {
addLog(`Failed to inject into tab: ${err.message}`, 'warn');
});
}
sendResponse({ status: 'ok' });
@@ -1724,6 +1747,17 @@ async function handleAsyncMessage(message, sender, sendResponse) {
}
}
chrome.storage.onChanged.addListener(async (changes, area) => {
if (area !== 'sync' || !changes.audioSettings) return;
await ensureState();
if (!currentTabId) return;
chrome.tabs.sendMessage(currentTabId, {
action: 'APPLY_AUDIO_SETTINGS',
settings: changes.audioSettings.newValue
}).catch(() => {});
});
// Tab removal listener
chrome.tabs.onRemoved.addListener(async (tabId) => {
await ensureState();
@@ -1775,7 +1809,9 @@ chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, _tab) => {
chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
}).catch(() => {});
})
.then(() => applyAudioSettingsToTab(tabId))
.catch(() => {});
}
});
+173 -2
View File
@@ -62,10 +62,15 @@
let _pendingLobbyTitle = null; // Title we're waiting to match (from remote lobby)
let lobbyPollTimer = null;
let _autoSyncEnabled = true; // Cached setting, updated via storage.onChanged
let _audioSettings = null;
let _audioProcessingAllowed = true;
// Cache the autoSyncNextEpisode setting
chrome.storage.sync.get(['autoSyncNextEpisode'], (data) => {
chrome.storage.sync.get(['autoSyncNextEpisode', 'audioSettings'], (data) => {
_autoSyncEnabled = data.autoSyncNextEpisode !== false; // default: enabled
_audioSettings = mergeAudioSettings(data.audioSettings);
const video = findVideo();
if (video && _audioProcessingAllowed) applyAudioSettings(video, _audioSettings);
});
chrome.storage.onChanged.addListener((changes, area) => {
if (area === 'sync' && changes.autoSyncNextEpisode) {
@@ -114,6 +119,147 @@
return best;
}
// --- Audio Processing Module ---
const AUDIO_PRESETS = {
recommended: { threshold: -24, ratio: 8, attack: 0.010, release: 0.300, knee: 15 },
dynamicRange: { threshold: -18, ratio: 4, attack: 0.020, release: 0.200, knee: 10 },
vocalEnhancement: { threshold: -12, ratio: 3, attack: 0.015, release: 0.150, knee: 5 },
smooth: { threshold: -30, ratio: 1.5, attack: 0.030, release: 0.250, knee: 20 },
custom: { threshold: -24, ratio: 12, attack: 0.003, release: 0.250, knee: 30 }
};
const DEFAULT_AUDIO_SETTINGS = {
enabled: false,
compressor: {
enabled: false,
preset: 'recommended',
customParams: { ...AUDIO_PRESETS.custom }
}
};
let audioCtx = null;
let audioChains = new WeakMap();
let currentAudioVideo = null;
function mergeAudioSettings(settings = {}) {
const safeSettings = settings && typeof settings === 'object' ? settings : {};
return {
...DEFAULT_AUDIO_SETTINGS,
...safeSettings,
compressor: {
...DEFAULT_AUDIO_SETTINGS.compressor,
...(safeSettings.compressor || {}),
customParams: {
...DEFAULT_AUDIO_SETTINGS.compressor.customParams,
...(safeSettings.compressor?.customParams || {})
}
}
};
}
function initAudioContext() {
if (!audioCtx) {
try {
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
if (!AudioContextClass) return null;
audioCtx = new AudioContextClass({ latencyHint: 'interactive' });
} catch (e) {
reportLog(`Audio Processing unavailable: ${e.message}`, 'warn');
return null;
}
}
if (audioCtx.state === 'suspended') {
audioCtx.resume().catch(() => {});
}
return audioCtx;
}
function closeAudioContext() {
if (audioCtx) {
audioCtx.close().catch(() => {});
audioCtx = null;
}
audioChains = new WeakMap();
currentAudioVideo = null;
}
function setupAudioChain(videoEl) {
if (audioChains.has(videoEl)) return audioChains.get(videoEl);
const ctx = initAudioContext();
if (!ctx) return null;
try {
const src = ctx.createMediaElementSource(videoEl);
const compressor = ctx.createDynamicsCompressor();
const dryGain = ctx.createGain();
const compGain = ctx.createGain();
src.connect(dryGain);
dryGain.connect(ctx.destination);
src.connect(compressor);
compressor.connect(compGain);
compGain.connect(ctx.destination);
dryGain.gain.value = 1;
compGain.gain.value = 0;
const chain = { compressor, dryGain, compGain, active: false };
audioChains.set(videoEl, chain);
currentAudioVideo = videoEl;
return chain;
} catch (e) {
reportLog(`Audio Processing setup failed: ${e.message}`, 'warn');
return null;
}
}
function rampGain(node, value, t) {
const current = node.gain.value;
node.gain.cancelScheduledValues(t);
node.gain.setValueAtTime(current, t);
node.gain.linearRampToValueAtTime(value, t + 0.04);
}
function applyAudioBypass(videoEl) {
const chain = audioChains.get(videoEl);
if (!chain || !chain.active) return;
const t = chain.dryGain.context.currentTime;
rampGain(chain.dryGain, 1, t);
rampGain(chain.compGain, 0, t);
chain.active = false;
}
function bypassCurrentAudioProcessing() {
if (currentAudioVideo) applyAudioBypass(currentAudioVideo);
}
function applyAudioSettings(videoEl, settings) {
const mergedSettings = mergeAudioSettings(settings);
if (!mergedSettings.enabled || !mergedSettings.compressor?.enabled) {
applyAudioBypass(videoEl);
return;
}
const chain = setupAudioChain(videoEl);
if (!chain) return;
const cSettings = mergedSettings.compressor;
const params = cSettings.preset === 'custom'
? cSettings.customParams
: AUDIO_PRESETS[cSettings.preset] || AUDIO_PRESETS.recommended;
chain.compressor.threshold.value = params.threshold ?? -24;
chain.compressor.knee.value = params.knee ?? 15;
chain.compressor.ratio.value = params.ratio ?? 8;
chain.compressor.attack.value = params.attack ?? 0.010;
chain.compressor.release.value = params.release ?? 0.300;
if (!chain.active) {
const t = chain.dryGain.context.currentTime;
rampGain(chain.dryGain, 0, t);
rampGain(chain.compGain, 1, t);
chain.active = true;
}
}
// --- Episode Auto-Sync: Detection ---
function getMediaTitle() {
return (navigator.mediaSession && navigator.mediaSession.metadata)
@@ -351,6 +497,22 @@
return true;
}
if (message.action === 'APPLY_AUDIO_SETTINGS') {
_audioProcessingAllowed = true;
_audioSettings = mergeAudioSettings(message.settings);
const video = findVideo();
if (video) applyAudioSettings(video, _audioSettings);
sendResponse({ ok: true });
return true;
}
if (message.action === 'RESET_AUDIO_PROCESSING') {
_audioProcessingAllowed = false;
bypassCurrentAudioProcessing();
sendResponse({ ok: true });
return true;
}
if (message.type === 'SERVER_COMMAND') {
const { action, payload } = message;
let actionCompleted = false;
@@ -655,7 +817,10 @@
});
// Reset on page hide/show (bfcache, tab discard)
window.addEventListener('pagehide', () => { pageVisible = false; });
window.addEventListener('pagehide', () => {
pageVisible = false;
closeAudioContext();
});
window.addEventListener('pageshow', (event) => {
// event.persisted is true ONLY when restored from bfcache, not on initial load
if (event.persisted && !pageVisible) {
@@ -722,6 +887,9 @@
function setupListeners() {
const video = findVideo();
if (video) {
if (currentAudioVideo && currentAudioVideo !== video) {
bypassCurrentAudioProcessing();
}
const existing = video._koalaHandlers;
if (existing) {
video.removeEventListener('play', existing.play);
@@ -741,6 +909,8 @@
if (!lastKnownMediaTitle) {
lastKnownMediaTitle = getMediaTitle();
}
if (_audioSettings && _audioProcessingAllowed) applyAudioSettings(video, _audioSettings);
}
}
@@ -755,6 +925,7 @@
if (!video && lastVideoSrc !== undefined) {
reportLog('Video element removed from page', 'warn');
lastVideoSrc = undefined;
closeAudioContext();
return;
}
+24 -1
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "Alle anderen Teilnehmer haben den Raum verlassen",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Zeitüberschreitung — nicht alle Teilnehmer haben die Episode geladen",
"LOBBY_CANCEL_USER": "Vom Benutzer abgebrochen",
"NOTIF_ERROR_TITLE": "KoalaSync-Fehler"
"NOTIF_ERROR_TITLE": "KoalaSync-Fehler",
"FOOTER_SUPPORT": "♥ Unterstützen",
"FOOTER_REVIEW": "★ Bewerten",
"LABEL_AUDIO_PROCESSING": "Audio-Verarbeitung",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Wendet Audioeffekte wie Kompression auf die Videowiedergabe an",
"AUDIO_OPEN_SETTINGS": "Audio-Einstellungen öffnen",
"NEW_FEATURE_AUDIO": "Neu: Audio-Verarbeitung — probiere den Kompressor aus!",
"AUDIO_PAGE_TITLE": "Audio-Einstellungen",
"AUDIO_MASTER_TOGGLE": "Audio-Verarbeitung",
"AUDIO_COMPRESSOR": "Kompressor",
"AUDIO_COMPRESSOR_ENABLE": "Aktiviert",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Empfohlen",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamikumfang",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Sprache verbessern",
"AUDIO_PRESET_SMOOTH": "Sanft",
"AUDIO_PRESET_CUSTOM": "Benutzerdefiniert",
"AUDIO_PARAM_THRESHOLD": "Schwellwert",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Demnächst"
}
+24 -1
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "All other peers left",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — not all peers loaded the episode",
"LOBBY_CANCEL_USER": "Cancelled by user",
"NOTIF_ERROR_TITLE": "KoalaSync Error"
"NOTIF_ERROR_TITLE": "KoalaSync Error",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+24 -1
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "Todos los demás participantes se han ido",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tiempo de espera agotado: no todos los participantes cargaron el episodio",
"LOBBY_CANCEL_USER": "Cancelado por el usuario",
"NOTIF_ERROR_TITLE": "Error de KoalaSync"
"NOTIF_ERROR_TITLE": "Error de KoalaSync",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+24 -1
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "Tous les autres membres sont partis",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Délai dépassé — tous les membres n'ont pas chargé l'épisode",
"LOBBY_CANCEL_USER": "Annulé par l'utilisateur",
"NOTIF_ERROR_TITLE": "Erreur KoalaSync"
"NOTIF_ERROR_TITLE": "Erreur KoalaSync",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+25 -2
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "Tutti gli altri partecipanti hanno lasciato la stanza",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — non tutti i partecipanti hanno caricato l'episodio",
"LOBBY_CANCEL_USER": "Annullato dall'utente",
"NOTIF_ERROR_TITLE": "Errore KoalaSync"
}
"NOTIF_ERROR_TITLE": "Errore KoalaSync",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+25 -2
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "他のすべてのメンバーが退室しました",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "タイムアウト — 一部のメンバーがエピソードを読み込めませんでした",
"LOBBY_CANCEL_USER": "ユーザーによってキャンセルされました",
"NOTIF_ERROR_TITLE": "KoalaSyncエラー"
}
"NOTIF_ERROR_TITLE": "KoalaSyncエラー",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+25 -2
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "다른 모든 피어가 나갔습니다",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "시간 초과 — 모든 피어가 에피소드를 로드하지 못했습니다",
"LOBBY_CANCEL_USER": "사용자에 의해 취소됨",
"NOTIF_ERROR_TITLE": "KoalaSync 오류"
}
"NOTIF_ERROR_TITLE": "KoalaSync 오류",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+25 -2
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "Alle andere deelnemers zijn vertrokken",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Time-out — niet alle deelnemers hebben de aflevering geladen",
"LOBBY_CANCEL_USER": "Geannuleerd door gebruiker",
"NOTIF_ERROR_TITLE": "KoalaSync-fout"
}
"NOTIF_ERROR_TITLE": "KoalaSync-fout",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+25 -2
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "Wszyscy inni uczestnicy wyszli",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Limit czasu — nie wszyscy uczestnicy załadowali odcinek",
"LOBBY_CANCEL_USER": "Anulowane przez użytkownika",
"NOTIF_ERROR_TITLE": "Błąd KoalaSync"
}
"NOTIF_ERROR_TITLE": "Błąd KoalaSync",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+24 -1
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "Todos os outros participantes saíram",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Tempo limite esgotado — nem todos os participantes carregaram o episódio",
"LOBBY_CANCEL_USER": "Cancelado pelo usuário",
"NOTIF_ERROR_TITLE": "Erro do KoalaSync"
"NOTIF_ERROR_TITLE": "Erro do KoalaSync",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+25 -2
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "Todos os outros participantes saíram",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Timeout — nem todos os participantes carregaram o episódio",
"LOBBY_CANCEL_USER": "Cancelado pelo utilizador",
"NOTIF_ERROR_TITLE": "Erro do KoalaSync"
}
"NOTIF_ERROR_TITLE": "Erro do KoalaSync",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+24 -1
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "Все остальные участники вышли",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Время ожидания истекло — не все участники загрузили серию",
"LOBBY_CANCEL_USER": "Отменено пользователем",
"NOTIF_ERROR_TITLE": "Ошибка KoalaSync"
"NOTIF_ERROR_TITLE": "Ошибка KoalaSync",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+25 -2
View File
@@ -179,5 +179,28 @@
"LOBBY_CANCEL_PEERS_LEFT": "Diğer tüm bağlantılar ayrıldı",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD": "Zaman aşımı — tüm bağlantılar bölümü yüklemedi",
"LOBBY_CANCEL_USER": "Kullanıcı tarafından iptal edildi",
"NOTIF_ERROR_TITLE": "KoalaSync Hatası"
}
"NOTIF_ERROR_TITLE": "KoalaSync Hatası",
"FOOTER_SUPPORT": "♥ Support",
"FOOTER_REVIEW": "★ Rate us",
"LABEL_AUDIO_PROCESSING": "Audio Processing",
"LABEL_AUDIO_PROCESSING_TOOLTIP": "Apply audio effects like compression to video playback",
"AUDIO_OPEN_SETTINGS": "Open Audio Settings",
"NEW_FEATURE_AUDIO": "New: Audio Processing — try the compressor!",
"AUDIO_PAGE_TITLE": "Audio Settings",
"AUDIO_MASTER_TOGGLE": "Audio Processing",
"AUDIO_COMPRESSOR": "Compressor",
"AUDIO_COMPRESSOR_ENABLE": "Enabled",
"AUDIO_PRESET": "Preset",
"AUDIO_PRESET_RECOMMENDED": "Recommended",
"AUDIO_PRESET_DYNAMIC_RANGE": "Dynamic Range",
"AUDIO_PRESET_VOCAL_ENHANCEMENT": "Vocal Enhancement",
"AUDIO_PRESET_SMOOTH": "Smooth",
"AUDIO_PRESET_CUSTOM": "Custom",
"AUDIO_PARAM_THRESHOLD": "Threshold",
"AUDIO_PARAM_KNEE": "Knee",
"AUDIO_PARAM_RATIO": "Ratio",
"AUDIO_PARAM_ATTACK": "Attack",
"AUDIO_PARAM_RELEASE": "Release",
"AUDIO_EQUALIZER": "Equalizer",
"AUDIO_COMING_SOON": "Coming soon"
}
+60 -2
View File
@@ -331,6 +331,39 @@
transform: translateX(16px);
background-color: white;
}
.popup-footer {
margin-top: 20px;
text-align: center;
border-top: 1px solid rgba(255,255,255,0.05);
padding-top: 10px;
color: var(--text-muted);
font-size: 10px;
opacity: 0.6;
}
.popup-footer a {
color: var(--text-muted);
text-decoration: none;
transition: color 0.2s, opacity 0.2s;
}
.popup-footer a:hover {
color: var(--accent);
opacity: 1;
}
.feature-hint {
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--accent);
margin-left: 4px;
animation: featurePulse 2s ease-in-out infinite;
cursor: help;
vertical-align: middle;
}
@keyframes featurePulse {
0%, 100% { opacity: 0.4; transform: scale(1); }
50% { opacity: 1; transform: scale(1.3); }
}
</style>
</head>
<body>
@@ -535,11 +568,30 @@
</select>
</div>
<div class="form-group" style="display: flex; align-items: center; justify-content: space-between; background: var(--card); padding: 10px; border-radius: 8px; margin-bottom: 12px; border: 1px solid #334155;">
<div style="display: flex; flex-direction: column; gap: 2px;">
<label style="margin-bottom: 0; cursor: help;" data-i18n="LABEL_AUDIO_PROCESSING" data-i18n-title="LABEL_AUDIO_PROCESSING_TOOLTIP">Audio Processing</label>
<a id="audioSettingsLink" href="#" style="font-size: 10px; color: var(--accent); text-decoration: none;" data-i18n="AUDIO_OPEN_SETTINGS">Open Audio Settings</a>
</div>
<label class="toggle-switch">
<input type="checkbox" id="audioProcessingToggle">
<span class="slider"></span>
</label>
</div>
<div style="margin-top: 15px; padding: 8px; border-top: 1px solid var(--card);">
<label title="Tools for fixing connection issues" data-i18n="LABEL_TROUBLESHOOTING" data-i18n-title="LABEL_TROUBLESHOOTING_TOOLTIP">Troubleshooting</label>
<button id="regenId" class="secondary" style="width: 100%; font-size: 11px;" title="Regenerate your internal ID and reconnect" data-i18n="BTN_REGEN_ID" data-i18n-title="BTN_REGEN_ID_TOOLTIP">Regenerate Peer ID</button>
<p style="font-size: 9px; color: var(--text-muted); margin-top: 5px; text-align: center;" data-i18n="REGEN_ID_DESC">Use this if you see "Duplicate Identity" errors.</p>
</div>
<div class="popup-footer">
<a id="settingsSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">♥ Support</a>
<span> · </span>
<a id="settingsReviewLink" href="#" target="_blank" data-i18n="FOOTER_REVIEW">★ Rate us</a>
<span> · </span>
<span id="settingsVersion">v0.0.0</span>
</div>
</div>
<!-- Dev Tab -->
@@ -570,9 +622,15 @@
</div>
<div id="logList"></div>
<div style="margin-top: 20px; text-align: center; border-top: 1px solid rgba(255,255,255,0.05); padding-top: 10px;">
<div class="popup-footer">
<a href="https://github.com/Shik3i/KoalaSync" target="_blank" style="color: var(--text-muted); text-decoration: none; font-size: 10px; opacity: 0.6; display: block;" data-i18n="LABEL_GITHUB">GitHub Repository</a>
<div id="appVersion" style="color: var(--text-muted); font-size: 9px; opacity: 0.4; margin-top: 4px;">v0.0.0</div>
<div style="margin-top: 4px;">
<a id="devSupportLink" href="#" target="_blank" data-i18n="FOOTER_SUPPORT">♥ Support</a>
<span> · </span>
<a id="devReviewLink" href="#" target="_blank" data-i18n="FOOTER_REVIEW">★ Rate us</a>
<span> · </span>
<span id="appVersion">v0.0.0</span>
</div>
</div>
</div>
+143 -4
View File
@@ -1,4 +1,4 @@
import { EVENTS, OFFICIAL_LANDING_PAGE_URL } from './shared/constants.js';
import { EVENTS, OFFICIAL_LANDING_PAGE_URL, SUPPORT_URL, getReviewUrl } from './shared/constants.js';
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
import { getAvatarForName, generateUsername, USERNAME_ADJECTIVES, USERNAME_NOUNS } from './shared/names.js';
import { loadLocale, translateDOM, getMessage, getSystemLanguage } from './i18n.js';
@@ -50,9 +50,16 @@ const elements = {
lobbyPeerStatus: document.getElementById('lobbyPeerStatus'),
browserNotifications: document.getElementById('browserNotifications'),
autoCopyInvite: document.getElementById('autoCopyInvite'),
syncTabCopyInvite: document.getElementById('syncTabCopyInvite'),
cancelLobbyBtn: document.getElementById('cancelLobbyBtn'),
langSelector: document.getElementById('langSelector')
langSelector: document.getElementById('langSelector'),
audioProcessingToggle: document.getElementById('audioProcessingToggle'),
audioSettingsLink: document.getElementById('audioSettingsLink'),
settingsSupportLink: document.getElementById('settingsSupportLink'),
settingsReviewLink: document.getElementById('settingsReviewLink'),
settingsVersion: document.getElementById('settingsVersion'),
devSupportLink: document.getElementById('devSupportLink'),
devReviewLink: document.getElementById('devReviewLink'),
syncTabCopyInvite: document.getElementById('syncTabCopyInvite')
};
let localPeerId = null;
@@ -71,8 +78,100 @@ let pendingConnectionErrorMsg = null;
let roomListRefreshTimer = null;
let roomListRefreshInterval = null;
const ROOM_LIST_REFRESH_COOLDOWN_MS = 11000;
const DEFAULT_AUDIO_SETTINGS = {
enabled: false,
compressor: {
enabled: false,
preset: 'recommended',
customParams: {
threshold: -24,
knee: 30,
ratio: 12,
attack: 0.003,
release: 0.250
}
}
};
const FEATURE_HINTS = [
{
key: 'audio_processing',
selector: '#audioSettingsLink',
i18nTooltip: 'NEW_FEATURE_AUDIO'
}
];
function mergeAudioSettings(settings = {}) {
const safeSettings = settings && typeof settings === 'object' ? settings : {};
return {
...DEFAULT_AUDIO_SETTINGS,
...safeSettings,
compressor: {
...DEFAULT_AUDIO_SETTINGS.compressor,
...(safeSettings.compressor || {}),
customParams: {
...DEFAULT_AUDIO_SETTINGS.compressor.customParams,
...(safeSettings.compressor?.customParams || {})
}
}
};
}
// --- Helpers ---
function configureFooterLinks() {
const reviewUrl = getReviewUrl();
[elements.settingsSupportLink, elements.devSupportLink].forEach(link => {
if (link) link.href = SUPPORT_URL;
});
[elements.settingsReviewLink, elements.devReviewLink].forEach(link => {
if (link) link.href = reviewUrl;
});
}
function updateAudioSettingsLinkVisibility() {
if (elements.audioSettingsLink && elements.audioProcessingToggle) {
elements.audioSettingsLink.style.display = elements.audioProcessingToggle.checked ? 'inline-flex' : 'none';
}
}
function openAudioSettingsPage() {
chrome.tabs.create({ url: chrome.runtime.getURL('audio-options.html') });
}
async function updateFeatureHints() {
const data = await chrome.storage.sync.get(['dismissedHints']);
const dismissed = Array.isArray(data.dismissedHints) ? data.dismissedHints : [];
FEATURE_HINTS.forEach(feature => {
const target = document.querySelector(feature.selector);
if (!target) return;
const existing = target.parentElement?.querySelector(`.feature-hint[data-feature="${feature.key}"]`);
if (dismissed.includes(feature.key)) {
if (existing) existing.remove();
return;
}
if (!existing) {
const hint = document.createElement('span');
hint.className = 'feature-hint';
hint.dataset.feature = feature.key;
hint.title = getMessage(feature.i18nTooltip);
target.insertAdjacentElement('afterend', hint);
} else {
existing.title = getMessage(feature.i18nTooltip);
}
});
}
async function dismissFeatureHint(featureKey) {
const data = await chrome.storage.sync.get(['dismissedHints']);
const dismissed = Array.isArray(data.dismissedHints) ? data.dismissedHints : [];
if (!dismissed.includes(featureKey)) {
await chrome.storage.sync.set({ dismissedHints: [...dismissed, featureKey] });
}
await updateFeatureHints();
}
function clearConnectionErrorTimer() {
if (connectionErrorTimer) {
clearTimeout(connectionErrorTimer);
@@ -111,7 +210,7 @@ async function init() {
// Load per-device settings (local) + synced preferences (sync)
const [localData, syncData] = await Promise.all([
chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']),
chrome.storage.sync.get(['filterNoise', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale'])
chrome.storage.sync.get(['filterNoise', 'autoSyncNextEpisode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'dismissedHints'])
]);
// Migrate from sync → local for existing users
@@ -165,6 +264,11 @@ async function init() {
if (elements.forceSyncMode) elements.forceSyncMode.value = syncData.forceSyncMode || 'jump-to-others';
if (elements.browserNotifications) elements.browserNotifications.checked = syncData.browserNotifications === true;
if (elements.autoCopyInvite) elements.autoCopyInvite.checked = syncData.autoCopyInvite !== false;
if (elements.audioProcessingToggle) {
const audioSettings = mergeAudioSettings(syncData.audioSettings);
elements.audioProcessingToggle.checked = audioSettings.enabled === true;
updateAudioSettingsLinkVisibility();
}
// Set Version Info
const versionTxt = `v${chrome.runtime.getManifest().version}`;
@@ -172,6 +276,9 @@ async function init() {
if (versionEl) versionEl.textContent = versionTxt;
const popupVerEl = document.getElementById('popupVersion');
if (popupVerEl) popupVerEl.textContent = versionTxt;
if (elements.settingsVersion) elements.settingsVersion.textContent = versionTxt;
configureFooterLinks();
await updateFeatureHints();
if (localData.useCustomServer) {
setServerMode(true);
@@ -959,6 +1066,35 @@ if (elements.autoCopyInvite) {
});
}
if (elements.audioProcessingToggle) {
elements.audioProcessingToggle.addEventListener('change', async () => {
const data = await chrome.storage.sync.get(['audioSettings']);
const audioSettings = mergeAudioSettings(data.audioSettings);
audioSettings.enabled = elements.audioProcessingToggle.checked;
if (audioSettings.enabled && !audioSettings.compressor.enabled) {
audioSettings.compressor.enabled = true;
}
await chrome.storage.sync.set({ audioSettings });
updateAudioSettingsLinkVisibility();
if (audioSettings.enabled) openAudioSettingsPage();
});
}
if (elements.audioSettingsLink) {
elements.audioSettingsLink.addEventListener('click', async (event) => {
event.preventDefault();
await dismissFeatureHint('audio_processing');
openAudioSettingsPage();
});
}
chrome.storage.onChanged.addListener((changes, area) => {
if (area !== 'sync' || !changes.audioSettings || !elements.audioProcessingToggle) return;
const audioSettings = mergeAudioSettings(changes.audioSettings.newValue);
elements.audioProcessingToggle.checked = audioSettings.enabled === true;
updateAudioSettingsLinkVisibility();
});
elements.forceSyncMode.addEventListener('change', () => {
chrome.storage.sync.set({ forceSyncMode: elements.forceSyncMode.value });
});
@@ -977,6 +1113,9 @@ if (elements.langSelector) {
await chrome.storage.sync.set({ locale: selectedLang });
await loadLocale(selectedLang);
translateDOM();
configureFooterLinks();
updateAudioSettingsLinkVisibility();
await updateFeatureHints();
// Re-apply connection and room UI state since translateDOM may overwrite dynamic elements
chrome.runtime.sendMessage({ type: 'GET_STATUS' }, async (res) => {
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env node
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const sourcePath = path.join(repoRoot, 'extension/audio-options.js');
const source = fs.readFileSync(sourcePath, 'utf8')
.replace("import { loadLocale, translateDOM, getSystemLanguage } from './i18n.js';", '')
.replace(/init\(\)\.catch[\s\S]*?;\n?$/, '');
function makeInput(overrides = {}) {
return {
checked: false,
value: '',
dataset: {},
addEventListener() {},
...overrides
};
}
const rows = [
['threshold', '-60', '0', '1', false],
['knee', '0', '40', '1', false],
['ratio', '1', '20', '0.5', false],
['attack', '0', '1', '0.001', true],
['release', '0', '1', '0.005', true]
].map(([param, min, max, step, ms]) => {
const range = makeInput({ value: '0', min, max, step });
const number = makeInput({ value: '0', min: ms ? '0' : min, max: ms ? '1000' : max, step, dataset: ms ? { msInput: 'true' } : {} });
return {
dataset: { param },
querySelector(selector) {
return selector === 'input[type="range"]' ? range : number;
}
};
});
const sandbox = {
console,
loadLocale: async () => {},
translateDOM: () => {},
getSystemLanguage: () => 'en',
chrome: {
storage: {
sync: {
get: async () => ({}),
set: () => {},
},
onChanged: {
addListener: () => {}
}
}
},
document: {
getElementById: () => makeInput(),
querySelectorAll: (selector) => selector === '.control-row' ? rows : [makeInput({ value: 'recommended' })]
},
setTimeout,
clearTimeout
};
vm.createContext(sandbox);
vm.runInContext(`${source}
globalThis.__audioSettingsTest = {
mergeAudioSettings,
getParamValue,
setCustomParam,
get currentSettings() { return currentSettings; }
};`, sandbox, { filename: sourcePath });
const helpers = sandbox.__audioSettingsTest;
assert.doesNotThrow(() => helpers.mergeAudioSettings(null), 'mergeAudioSettings tolerates null storage values');
assert.doesNotThrow(() => helpers.mergeAudioSettings('bad'), 'mergeAudioSettings tolerates non-object storage values');
assert.equal(helpers.getParamValue('threshold', '-999'), -60, 'threshold clamps to minimum');
assert.equal(helpers.getParamValue('threshold', '999'), 0, 'threshold clamps to maximum');
assert.equal(helpers.getParamValue('knee', '-1'), 0, 'knee clamps to minimum');
assert.equal(helpers.getParamValue('knee', '100'), 40, 'knee clamps to maximum');
assert.equal(helpers.getParamValue('ratio', '0'), 1, 'ratio clamps to minimum');
assert.equal(helpers.getParamValue('ratio', '999'), 20, 'ratio clamps to maximum');
assert.equal(helpers.getParamValue('attack', '-1', true), 0, 'attack ms input clamps to minimum seconds');
assert.equal(helpers.getParamValue('attack', '5000', true), 1, 'attack ms input clamps to maximum seconds');
assert.equal(helpers.getParamValue('release', '-1', true), 0, 'release ms input clamps to minimum seconds');
assert.equal(helpers.getParamValue('release', '5000', true), 1, 'release ms input clamps to maximum seconds');
helpers.setCustomParam('threshold', 999);
assert.equal(helpers.currentSettings.compressor.customParams.threshold, 0, 'setCustomParam stores clamped values');
console.log('audio settings tests passed');
+1
View File
@@ -12,6 +12,7 @@ const checks = [
env: { ADMIN_METRICS_TOKEN: 'verify-admin-token-with-more-than-32-chars' }
}],
['content video finder', 'node', ['scripts/test-content-video-finder.js']],
['audio settings', 'node', ['scripts/test-audio-settings.mjs']],
['popup refresh cooldown', 'node', ['scripts/test-popup-refresh-cooldown.mjs']],
['server syntax index', 'node', ['-c', 'server/index.js']],
['server syntax ops', 'node', ['-c', 'server/ops.js']],
+14
View File
@@ -12,6 +12,20 @@ export const APP_VERSION = "1.9.0";
export const OFFICIAL_SERVER_URL = 'wss://syncserver.koalastuff.net';
export const OFFICIAL_LANDING_PAGE_URL = 'https://sync.koalastuff.net';
export const OFFICIAL_SERVER_TOKEN = '62170b705234c4f4807a9b22420bb93cf1a2aacfa4c5d3b47804482babb8eb50';
export const SUPPORT_URL = 'https://support.koalastuff.net';
export const KOFI_URL = 'https://ko-fi.com/koaladev';
export const GITHUB_URL = 'https://github.com/Shik3i/KoalaSync';
export function isFirefox() {
const manifest = chrome.runtime.getManifest();
return !!manifest.browser_specific_settings?.gecko?.id;
}
export function getReviewUrl() {
return isFirefox()
? 'https://addons.mozilla.org/firefox/addon/koalasync/reviews/'
: 'https://chromewebstore.google.com/detail/koalasync/obbnmkmlaaddodakcbdljknjpagklifc/reviews';
}
export const EVENTS = {
// Connection & Room
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "Protokoll (Letzte 50)",
"MOCK_32": "LÖSCHEN",
"FOOTER_LEGAL_LINK": "de/impressum",
"FOOTER_PRIVACY_LINK": "de/datenschutz"
"FOOTER_PRIVACY_LINK": "de/datenschutz",
"FOOTER_SUPPORT": "Auf Ko-Fi unterstützen"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "Logs (Last 50)",
"MOCK_32": "CLEAR",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Support on Ko-Fi"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "BORRAR",
"MOCK_32": "CLEAR",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Apoyar en Ko-Fi"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "EFFACER",
"MOCK_32": "CLEAR",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Soutenir sur Ko-Fi"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "Log (Ultimi 50)",
"MOCK_32": "PULISCI",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Sostieni su Ko-Fi"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "ログ(直近50件)",
"MOCK_32": "消去",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Ko-Fiで支援"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "로그 (최근 50개)",
"MOCK_32": "지우기",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Ko-Fi에서 후원"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "Logs (Laatste 50)",
"MOCK_32": "WISSEN",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Steun op Ko-Fi"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "Logi (ostatnie 50)",
"MOCK_32": "WYCZYŚĆ",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Wesprzyj na Ko-Fi"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "LIMPAR",
"MOCK_32": "CLEAR",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Apoiar no Ko-Fi"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "Registos (Últimos 50)",
"MOCK_32": "LIMPAR",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Apoiar no Ko-Fi"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "ОЧИСТИТЬ",
"MOCK_32": "CLEAR",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Поддержать на Ko-Fi"
}
+2 -1
View File
@@ -163,5 +163,6 @@
"MOCK_31": "Günlükler (Son 50)",
"MOCK_32": "TEMİZLE",
"FOOTER_LEGAL_LINK": "imprint",
"FOOTER_PRIVACY_LINK": "privacy"
"FOOTER_PRIVACY_LINK": "privacy",
"FOOTER_SUPPORT": "Ko-Fi üzerinden destekle"
}
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="{{ASSET_PATH}}{{FOOTER_LEGAL_LINK}}" style="color: var(--text-muted); text-decoration: none;"><span>{{FOOTER_LEGAL}}</span></a>
<a href="{{ASSET_PATH}}{{FOOTER_PRIVACY_LINK}}" style="color: var(--text-muted); text-decoration: none;"><span>{{FOOTER_PRIVACY}}</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="{{FOOTER_SUPPORT}}" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="14" height="14" aria-hidden="true" style="display: block;"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38.265-.061.527-.132.786-.213.585-.184 1.27-.39 1.774-.753a.057.057 0 0 0 .023-.043v-1.809a.052.052 0 0 0-.02-.041.053.053 0 0 0-.046-.01 20.282 20.282 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.593 5.593 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422.038-.008.077-.015.11-.024 2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545zm-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102c0-1.31.337-2.35 1.011-3.12.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164.675.77 1.012 1.81 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../de/impressum" style="color: var(--text-muted); text-decoration: none;"><span>Impressum</span></a>
<a href="../de/datenschutz" style="color: var(--text-muted); text-decoration: none;"><span>Datenschutz</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Auf Ko-Fi unterstützen" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>Legal Notice</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>Privacy Policy</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Apoyar en Ko-Fi" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>Legal Notice</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>Privacy Policy</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Soutenir sur Ko-Fi" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="imprint" style="color: var(--text-muted); text-decoration: none;"><span>Legal Notice</span></a>
<a href="privacy" style="color: var(--text-muted); text-decoration: none;"><span>Privacy Policy</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Support on Ko-Fi" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>Note Legali</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>Informativa sulla Privacy</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Sostieni su Ko-Fi" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>法的通知</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>プライバシーポリシー</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Ko-Fiで支援" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>법적 고지</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>개인정보 처리방침</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Ko-Fi에서 후원" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>Juridische kennisgeving</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>Privacybeleid</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Steun op Ko-Fi" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>Nota prawna</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>Polityka prywatności</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Wesprzyj na Ko-Fi" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>Legal Notice</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>Privacy Policy</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Apoiar no Ko-Fi" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>Aviso Legal</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>Política de Privacidade</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Apoiar no Ko-Fi" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>Legal Notice</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>Privacy Policy</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Поддержать на Ko-Fi" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon
+3
View File
@@ -1208,6 +1208,9 @@
<div style="margin-top: 1.5rem; font-size: 0.8rem; display: flex; justify-content: center; align-items: center; gap: 1.5rem; flex-wrap: wrap;">
<a href="../imprint" style="color: var(--text-muted); text-decoration: none;"><span>Yasal Uyarı</span></a>
<a href="../privacy" style="color: var(--text-muted); text-decoration: none;"><span>Gizlilik Politikası</span></a>
<a href="https://ko-fi.com/koaladev" target="_blank" title="Ko-Fi üzerinden destekle" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<img src="https://img.shields.io/badge/Ko--Fi-Support-FF5E5B?logo=ko-fi&logoColor=white" alt="Ko-Fi" style="height: 16px;">
</a>
<a href="https://mastodon.social/@koalastuff" rel="me" target="_blank" style="color: var(--text-muted); text-decoration: none; display: inline-flex; align-items: center; gap: 4px;">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" aria-hidden="true" style="display:block" viewBox="0 0 24 24"><path d="M23.268 5.313c-.35-2.578-2.617-4.61-5.304-5.004C17.51.242 15.792 0 11.813 0h-.03c-3.98 0-4.835.242-5.288.309C3.882.692 1.496 2.518.917 5.127.64 6.412.61 7.837.661 9.143c.074 1.874.088 3.745.26 5.611.118 1.24.325 2.47.62 3.68.55 2.237 2.777 4.098 4.96 4.857 2.336.792 4.849.923 7.256.38q.398-.092.786-.213c.585-.184 1.27-.39 1.774-.753a.06.06 0 0 0 .023-.043v-1.809a.05.05 0 0 0-.02-.041.05.05 0 0 0-.046-.01 20.3 20.3 0 0 1-4.709.545c-2.73 0-3.463-1.284-3.674-1.818a5.6 5.6 0 0 1-.319-1.433.053.053 0 0 1 .066-.054c1.517.363 3.072.546 4.632.546.376 0 .75 0 1.125-.01 1.57-.044 3.224-.124 4.768-.422q.059-.011.11-.024c2.435-.464 4.753-1.92 4.989-5.604.008-.145.03-1.52.03-1.67.002-.512.167-3.63-.024-5.545m-3.748 9.195h-2.561V8.29c0-1.309-.55-1.976-1.67-1.976-1.23 0-1.846.79-1.846 2.35v3.403h-2.546V8.663c0-1.56-.617-2.35-1.848-2.35-1.112 0-1.668.668-1.67 1.977v6.218H4.822V8.102q0-1.965 1.011-3.12c.696-.77 1.608-1.164 2.74-1.164 1.311 0 2.302.5 2.962 1.498l.638 1.06.638-1.06c.66-.999 1.65-1.498 2.96-1.498 1.13 0 2.043.395 2.74 1.164q1.012 1.155 1.012 3.12z"/></svg>
Mastodon