mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-07-26 12:08:15 +00:00
Add support badges and audio processing
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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');
|
||||
@@ -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']],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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で支援"
|
||||
}
|
||||
|
||||
@@ -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에서 후원"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user