mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-21 00:16:36 +00:00
Enhance forest scene with mid bamboo layer, butterflies, click confetti, and demo scene polish
This commit is contained in:
+169
-8
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>KoalaSync</title>
|
||||
<script src="theme-init.js"></script>
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: 'Twemoji Country Flags';
|
||||
@@ -26,6 +27,7 @@
|
||||
--accent-terracotta: oklch(0.62 0.14 45);
|
||||
--danger: oklch(0.55 0.15 25);
|
||||
--text-on-green: oklch(0.14 0.02 90);
|
||||
--text-on-warm: oklch(0.16 0.025 45);
|
||||
|
||||
--bg: var(--surface-deep);
|
||||
--card: var(--surface);
|
||||
@@ -37,6 +39,25 @@
|
||||
--error: var(--danger);
|
||||
--radius: 12px;
|
||||
--star: oklch(0.68 0.14 45);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
html.theme-light {
|
||||
--bg-base: oklch(0.965 0.012 105);
|
||||
--surface: oklch(0.995 0.006 100);
|
||||
--surface-deep: oklch(0.935 0.018 110);
|
||||
--surface-alt: oklch(0.91 0.022 115);
|
||||
--border-soft: oklch(0.28 0.035 140 / 0.10);
|
||||
--border-strong: oklch(0.73 0.035 125);
|
||||
--text-primary: oklch(0.21 0.03 140);
|
||||
--text-secondary: oklch(0.43 0.03 135);
|
||||
--accent-green: oklch(0.56 0.13 150);
|
||||
--accent-green-hover: oklch(0.49 0.13 150);
|
||||
--accent-terracotta: oklch(0.64 0.14 45);
|
||||
--danger: oklch(0.57 0.17 25);
|
||||
--text-on-green: oklch(0.16 0.03 140);
|
||||
--text-on-warm: oklch(0.18 0.03 45);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -47,6 +68,16 @@
|
||||
color: var(--text);
|
||||
font-family: 'Twemoji Country Flags', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
background-image:
|
||||
radial-gradient(circle at 12% -8%, oklch(0.68 0.13 150 / 0.12), transparent 42%),
|
||||
radial-gradient(circle at 105% 12%, oklch(0.62 0.14 45 / 0.08), transparent 34%);
|
||||
transition: background-color 0.25s ease, color 0.25s ease;
|
||||
}
|
||||
|
||||
html.theme-light body {
|
||||
background-image:
|
||||
radial-gradient(circle at 8% -5%, oklch(0.72 0.11 145 / 0.24), transparent 40%),
|
||||
radial-gradient(circle at 105% 5%, oklch(0.82 0.12 65 / 0.2), transparent 38%);
|
||||
}
|
||||
|
||||
.header-row {
|
||||
@@ -81,7 +112,7 @@
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
text-decoration: none;
|
||||
opacity: 0.5;
|
||||
opacity: 0.72;
|
||||
transition: opacity 0.2s, color 0.2s;
|
||||
}
|
||||
.popup-version:hover {
|
||||
@@ -113,7 +144,8 @@
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s;
|
||||
min-height: 36px;
|
||||
transition: background-color 0.2s, color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
@@ -154,7 +186,7 @@
|
||||
box-sizing: border-box;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border-strong);
|
||||
color: white;
|
||||
color: var(--text);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
@@ -189,7 +221,7 @@
|
||||
font-weight: 700;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
transition: background-color 0.2s, box-shadow 0.2s, transform 0.2s;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@@ -203,13 +235,44 @@
|
||||
padding: 10px;
|
||||
background: var(--border-strong);
|
||||
border: none;
|
||||
color: white;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
button.secondary:hover {
|
||||
background: oklch(0.38 0.035 125);
|
||||
}
|
||||
|
||||
details > summary:focus-visible {
|
||||
outline: 2px solid var(--accent-hover) !important;
|
||||
outline-offset: 3px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button:focus-visible,
|
||||
a:focus-visible,
|
||||
input:focus-visible,
|
||||
select:focus-visible {
|
||||
outline: 2px solid var(--accent-hover);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.48;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.copy-success {
|
||||
background: var(--success) !important;
|
||||
border-color: var(--success) !important;
|
||||
color: var(--text-on-green) !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Info Cards */
|
||||
.info-card {
|
||||
background: var(--card);
|
||||
@@ -289,7 +352,7 @@
|
||||
.toast-success { background: var(--success); color: var(--text-on-green); }
|
||||
.toast-error { background: var(--error); color: white; }
|
||||
.toast-info { background: var(--accent); color: var(--text-on-green); }
|
||||
.toast-warning { background: var(--accent-terracotta); color: white; }
|
||||
.toast-warning { background: var(--accent-terracotta); color: var(--text-on-warm); }
|
||||
|
||||
@keyframes toastSlideIn {
|
||||
from { opacity: 0; transform: translateY(-20px); }
|
||||
@@ -300,6 +363,59 @@
|
||||
to { opacity: 0; transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
/* Eucalyptus confetti: tiny leaves bursting from the copy-invite
|
||||
button (spawned by popup.js, mirrors the website's effect). The
|
||||
outer span owns the flight path — fast burst that brakes at its
|
||||
peak, then sinks — while the inner leaf rocks around its stem. */
|
||||
.click-leaf {
|
||||
position: fixed;
|
||||
z-index: 3000;
|
||||
margin: -5px 0 0 -4px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
animation: leafBurst var(--leaf-dur, 1s) forwards;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.click-leaf i {
|
||||
display: block;
|
||||
width: 7px;
|
||||
height: 10px;
|
||||
border-radius: 0 70% 0 70%;
|
||||
background: linear-gradient(135deg, oklch(0.68 0.13 150 / 0.95), oklch(0.5 0.1 145 / 0.85));
|
||||
transform: rotate(var(--leaf-rot, 0deg));
|
||||
animation: leafRock var(--flutter-dur, 0.55s) ease-in-out infinite alternate;
|
||||
}
|
||||
.click-leaf-sage i {
|
||||
background: linear-gradient(135deg, oklch(0.75 0.09 140 / 0.9), oklch(0.58 0.08 145 / 0.8));
|
||||
}
|
||||
.click-leaf-amber i {
|
||||
background: linear-gradient(135deg, oklch(0.68 0.14 55 / 0.95), oklch(0.55 0.13 40 / 0.85));
|
||||
}
|
||||
@keyframes leafBurst {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(0, 0) scale(var(--leaf-scale, 1));
|
||||
animation-timing-function: cubic-bezier(0.16, 0.84, 0.44, 1);
|
||||
}
|
||||
10% { opacity: 1; }
|
||||
45% {
|
||||
opacity: 1;
|
||||
transform: translate(var(--leaf-x, 0), var(--leaf-y, -14px)) scale(var(--leaf-scale, 1));
|
||||
animation-timing-function: cubic-bezier(0.55, 0.06, 0.68, 0.19);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(calc(var(--leaf-x, 0) * 1.4), calc(var(--leaf-y, -14px) + 30px)) scale(calc(var(--leaf-scale, 1) * 0.92));
|
||||
}
|
||||
}
|
||||
@keyframes leafRock {
|
||||
from { transform: rotate(calc(var(--leaf-rot, 0deg) - 24deg)); }
|
||||
to { transform: rotate(calc(var(--leaf-rot, 0deg) + 24deg)); }
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.click-leaf { display: none; }
|
||||
}
|
||||
|
||||
@keyframes fadeSlideIn {
|
||||
from { opacity: 0; transform: translateX(5px); }
|
||||
to { opacity: 1; transform: translateX(0); }
|
||||
@@ -353,7 +469,7 @@
|
||||
padding-top: 10px;
|
||||
color: var(--text-muted);
|
||||
font-size: 10px;
|
||||
opacity: 0.6;
|
||||
opacity: 0.78;
|
||||
}
|
||||
.popup-footer a {
|
||||
color: var(--text-muted);
|
||||
@@ -364,6 +480,42 @@
|
||||
color: var(--accent);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Light theme polish: preserve the same depth hierarchy without
|
||||
carrying dark-only inline colors into form controls and cards. */
|
||||
html.theme-light .tabs,
|
||||
html.theme-light .info-card,
|
||||
html.theme-light details.form-group,
|
||||
html.theme-light .form-group {
|
||||
box-shadow: 0 8px 24px oklch(0.22 0.035 140 / 0.07);
|
||||
}
|
||||
html.theme-light input,
|
||||
html.theme-light select,
|
||||
html.theme-light option {
|
||||
color: var(--text) !important;
|
||||
background-color: var(--surface) !important;
|
||||
}
|
||||
html.theme-light button.secondary {
|
||||
background: var(--surface-alt);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border-strong);
|
||||
}
|
||||
html.theme-light button.secondary:hover {
|
||||
background: oklch(0.86 0.035 125);
|
||||
}
|
||||
html.theme-light .mock-card,
|
||||
html.theme-light .peer-item {
|
||||
border-color: var(--border-strong);
|
||||
}
|
||||
html.theme-light .slider:before {
|
||||
background-color: oklch(0.62 0.025 125);
|
||||
}
|
||||
html.theme-light input:checked + .slider:before {
|
||||
background-color: white;
|
||||
}
|
||||
html.theme-light .popup-footer {
|
||||
border-top-color: var(--border-soft);
|
||||
}
|
||||
.feature-hint {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
@@ -466,7 +618,7 @@
|
||||
<label title="Share this link with friends so they can join" data-i18n="LABEL_INVITE_LINK" data-i18n-title="LABEL_INVITE_LINK_TOOLTIP">Invite Link</label>
|
||||
<div class="invite-box">
|
||||
<input type="text" id="inviteLink" readonly>
|
||||
<button id="copyInvite" class="secondary">📋</button>
|
||||
<button id="copyInvite" class="secondary" title="Copy invite link" aria-label="Copy invite link">📋</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -561,6 +713,15 @@
|
||||
|
||||
<!-- Settings Tab -->
|
||||
<div id="tab-settings" class="tab-content">
|
||||
<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 var(--border-strong);">
|
||||
<label id="themeLabel" style="margin-bottom: 0;" title="Follow the system theme or choose a fixed appearance">Appearance</label>
|
||||
<select id="themeSelector" aria-label="Appearance" style="width: 165px; background: var(--bg); border: 1px solid var(--border-strong); color: var(--text); padding: 6px 10px; border-radius: 8px; font-size: 13px; cursor: pointer; outline: none; font-family: inherit;">
|
||||
<option id="themeOptionSystem" value="system">💻 System</option>
|
||||
<option id="themeOptionDark" value="dark">🌙 Dark</option>
|
||||
<option id="themeOptionLight" value="light">☀️ Light</option>
|
||||
</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 var(--border-strong);">
|
||||
<label style="margin-bottom: 0;" title="Username helps others identify you." data-i18n="LABEL_USERNAME" data-i18n-title="LABEL_USERNAME_TOOLTIP">Your Username</label>
|
||||
<input type="text" id="username" data-i18n-placeholder="PLACEHOLDER_USERNAME" placeholder="Anonymous Koala" maxlength="20" style="width: 150px;">
|
||||
|
||||
+83
-12
@@ -64,6 +64,7 @@ const elements = {
|
||||
autoCopyInvite: document.getElementById('autoCopyInvite'),
|
||||
cancelLobbyBtn: document.getElementById('cancelLobbyBtn'),
|
||||
langSelector: document.getElementById('langSelector'),
|
||||
themeSelector: document.getElementById('themeSelector'),
|
||||
|
||||
audioSettingsLink: document.getElementById('audioSettingsLink'),
|
||||
settingsSupportLink: document.getElementById('settingsSupportLink'),
|
||||
@@ -132,6 +133,29 @@ function openAudioSettingsPage() {
|
||||
chrome.tabs.create({ url: chrome.runtime.getURL('audio-options.html') });
|
||||
}
|
||||
|
||||
function localizeThemeSelector(locale) {
|
||||
const labels = {
|
||||
de: ['Darstellung', 'System', 'Dunkel', 'Hell'], fr: ['Apparence', 'Système', 'Sombre', 'Clair'],
|
||||
es: ['Apariencia', 'Sistema', 'Oscuro', 'Claro'], it: ['Aspetto', 'Sistema', 'Scuro', 'Chiaro'],
|
||||
nl: ['Weergave', 'Systeem', 'Donker', 'Licht'], pl: ['Wygląd', 'System', 'Ciemny', 'Jasny'],
|
||||
pt: ['Aparência', 'Sistema', 'Escuro', 'Claro'], 'pt-BR': ['Aparência', 'Sistema', 'Escuro', 'Claro'],
|
||||
tr: ['Görünüm', 'Sistem', 'Koyu', 'Açık'], ru: ['Оформление', 'Системная', 'Тёмная', 'Светлая'],
|
||||
ja: ['外観', 'システム', 'ダーク', 'ライト'], ko: ['테마', '시스템', '다크', '라이트'],
|
||||
zh: ['外观', '跟随系统', '深色', '浅色'], uk: ['Вигляд', 'Системна', 'Темна', 'Світла'],
|
||||
en: ['Appearance', 'System', 'Dark', 'Light']
|
||||
};
|
||||
const [label, system, dark, light] = labels[locale] || labels.en;
|
||||
const labelEl = document.getElementById('themeLabel');
|
||||
const systemEl = document.getElementById('themeOptionSystem');
|
||||
const darkEl = document.getElementById('themeOptionDark');
|
||||
const lightEl = document.getElementById('themeOptionLight');
|
||||
if (labelEl) labelEl.textContent = label;
|
||||
if (systemEl) systemEl.textContent = `💻 ${system}`;
|
||||
if (darkEl) darkEl.textContent = `🌙 ${dark}`;
|
||||
if (lightEl) lightEl.textContent = `☀️ ${light}`;
|
||||
if (elements.themeSelector) elements.themeSelector.setAttribute('aria-label', label);
|
||||
}
|
||||
|
||||
async function updateFeatureHints() {
|
||||
const data = await chrome.storage.sync.get(['dismissedHints']);
|
||||
const dismissed = Array.isArray(data.dismissedHints) ? data.dismissedHints : [];
|
||||
@@ -204,7 +228,7 @@ function setRoomRefreshCooldown() {
|
||||
async function init() {
|
||||
// Local-only by design — settings and room credentials never come from
|
||||
// storage.sync (only onboardingComplete + dismissedHints live there).
|
||||
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab']);
|
||||
const localData = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username', 'filterNoise', 'autoSyncNextEpisode', 'sendTabTitle', 'mediaTitlePrivacyMode', 'titlePrivacyMode', 'forceSyncMode', 'browserNotifications', 'autoCopyInvite', 'locale', 'audioSettings', 'activeTab', 'themeMode']);
|
||||
|
||||
let activeLang = localData.locale;
|
||||
if (!activeLang) {
|
||||
@@ -214,8 +238,10 @@ async function init() {
|
||||
|
||||
await loadLocale(activeLang);
|
||||
translateDOM();
|
||||
localizeThemeSelector(activeLang);
|
||||
|
||||
if (elements.langSelector) elements.langSelector.value = activeLang;
|
||||
if (elements.themeSelector) elements.themeSelector.value = ['dark', 'light'].includes(localData.themeMode) ? localData.themeMode : 'system';
|
||||
|
||||
let username = localData.username;
|
||||
if (!username) {
|
||||
@@ -538,7 +564,7 @@ function updateLastActionUI(state, peers) {
|
||||
peerItem.style.cssText = `display:flex; flex-direction:column; align-items:center; opacity: ${isAcked ? 1 : 0.6};`;
|
||||
|
||||
const dot = document.createElement('div');
|
||||
dot.style.cssText = `width:18px; height:18px; border-radius:50%; background:${color}; color:white; display:flex; align-items:center; justify-content:center; font-size:9px; font-weight:bold; margin-bottom:1px;`;
|
||||
dot.style.cssText = `width:18px; height:18px; border-radius:50%; background:${color}; color:${isAcked ? 'var(--text-on-green)' : 'var(--text-primary)'}; display:flex; align-items:center; justify-content:center; font-size:9px; font-weight:bold; margin-bottom:1px;`;
|
||||
dot.textContent = icon;
|
||||
|
||||
const nameSpan = document.createElement('span');
|
||||
@@ -630,7 +656,10 @@ function renderEmpty(container, type) {
|
||||
copyBtn.title = getMessage('BTN_COPY_INVITE_TOOLTIP');
|
||||
copyBtn.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(elements.inviteLink.value)
|
||||
.then(() => showToast(getMessage('TOAST_INVITE_COPIED'), 'success', 2000))
|
||||
.then(() => {
|
||||
showToast(getMessage('TOAST_INVITE_COPIED'), 'success', 2000);
|
||||
spawnLeafBurst(copyBtn);
|
||||
})
|
||||
.catch(() => showToast(getMessage('TOAST_COPY_FAILED'), 'error'));
|
||||
});
|
||||
wrapper.appendChild(copyBtn);
|
||||
@@ -725,7 +754,7 @@ function updatePeerList(peers) {
|
||||
// Host Control Mode: show "Solo" badge for peers watching on their own.
|
||||
if (typeof p === 'object' && p.desynced) {
|
||||
const solo = document.createElement('span');
|
||||
solo.style.cssText = 'font-size:10px; color:#fff; background:#c96736; padding:2px 6px; border-radius:6px; font-weight:600;';
|
||||
solo.style.cssText = 'font-size:10px; color:var(--text-on-warm); background:var(--accent-terracotta); padding:2px 6px; border-radius:6px; font-weight:700;';
|
||||
const soloText = getMessage('BADGE_DESYNCED') || 'Solo';
|
||||
solo.textContent = soloText;
|
||||
solo.title = getMessage('TOOLTIP_PEER_DESYNCED') || soloText;
|
||||
@@ -742,7 +771,7 @@ function updatePeerList(peers) {
|
||||
const isController = !isOwner && hcmControllers.includes(pId);
|
||||
if (isOwner || isController) {
|
||||
const roleBadge = document.createElement('span');
|
||||
roleBadge.style.cssText = 'font-size:10px; color:#fff; background:var(--accent); padding:2px 6px; border-radius:6px; font-weight:600;';
|
||||
roleBadge.style.cssText = 'font-size:10px; color:var(--text-on-green); background:var(--accent); padding:2px 6px; border-radius:6px; font-weight:700;';
|
||||
roleBadge.textContent = isOwner ? (getMessage('BADGE_HOST') || 'Host') : (getMessage('BADGE_CONTROLLER') || 'Controller');
|
||||
rightGroup.appendChild(roleBadge);
|
||||
}
|
||||
@@ -755,7 +784,7 @@ function updatePeerList(peers) {
|
||||
btn.title = revoke ? (getMessage('BTN_REVOKE_CONTROL') || 'Revoke') : (getMessage('BTN_GIVE_CONTROL') || 'Give control');
|
||||
btn.addEventListener('mouseenter', () => {
|
||||
btn.style.background = revoke ? 'var(--text-muted)' : 'var(--accent)';
|
||||
btn.style.color = '#fff';
|
||||
btn.style.color = 'var(--text-on-green)';
|
||||
});
|
||||
btn.addEventListener('mouseleave', () => {
|
||||
btn.style.background = 'transparent';
|
||||
@@ -1273,6 +1302,14 @@ if (elements.autoCopyInvite) {
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.themeSelector) {
|
||||
elements.themeSelector.addEventListener('change', () => {
|
||||
const themeMode = elements.themeSelector.value;
|
||||
window.koalaTheme?.setMode(themeMode);
|
||||
chrome.storage.local.set({ themeMode });
|
||||
});
|
||||
}
|
||||
|
||||
if (elements.audioSettingsLink) {
|
||||
elements.audioSettingsLink.addEventListener('click', async (event) => {
|
||||
event.preventDefault();
|
||||
@@ -1305,6 +1342,7 @@ if (elements.langSelector) {
|
||||
await chrome.storage.local.set({ locale: selectedLang });
|
||||
await loadLocale(selectedLang);
|
||||
translateDOM();
|
||||
localizeThemeSelector(selectedLang);
|
||||
configureFooterLinks();
|
||||
await updateFeatureHints();
|
||||
|
||||
@@ -1389,6 +1427,39 @@ function showToast(message, type = 'info', duration = 3000) {
|
||||
setTimeout(() => toast.remove(), duration);
|
||||
}
|
||||
|
||||
// Eucalyptus confetti: a few tiny leaves burst from the given element
|
||||
// (matches the website's copy-invite moment). Subtle by design — few,
|
||||
// tiny, short-lived — and skipped entirely under reduced motion.
|
||||
function spawnLeafBurst(sourceEl) {
|
||||
if (!sourceEl || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
|
||||
const rect = sourceEl.getBoundingClientRect();
|
||||
if (!rect.width) return;
|
||||
const originY = rect.top + rect.height / 2;
|
||||
const count = 4 + Math.floor(Math.random() * 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const leaf = document.createElement('span');
|
||||
// Three tones of the palette: deep green, sage, a dash of amber
|
||||
const tone = Math.random();
|
||||
leaf.className = 'click-leaf' + (tone < 0.25 ? ' click-leaf-amber' : tone < 0.55 ? ' click-leaf-sage' : '');
|
||||
// Fan out upward to a burst peak spread across the button's width;
|
||||
// the keyframes brake there and let each leaf sink while it rocks.
|
||||
const angle = (-90 + (Math.random() - 0.5) * 150) * Math.PI / 180;
|
||||
const dist = 18 + Math.random() * 24;
|
||||
const dur = 0.85 + Math.random() * 0.4;
|
||||
leaf.style.left = (rect.left + 4 + Math.random() * Math.max(1, rect.width - 8)) + 'px';
|
||||
leaf.style.top = (originY - rect.height * 0.2) + 'px';
|
||||
leaf.style.setProperty('--leaf-x', (Math.cos(angle) * dist).toFixed(1) + 'px');
|
||||
leaf.style.setProperty('--leaf-y', (Math.sin(angle) * dist).toFixed(1) + 'px');
|
||||
leaf.style.setProperty('--leaf-rot', ((Math.random() - 0.5) * 160).toFixed(0) + 'deg');
|
||||
leaf.style.setProperty('--leaf-scale', (0.55 + Math.random() * 0.55).toFixed(2));
|
||||
leaf.style.setProperty('--leaf-dur', dur.toFixed(2) + 's');
|
||||
leaf.style.setProperty('--flutter-dur', (0.35 + Math.random() * 0.3).toFixed(2) + 's');
|
||||
leaf.appendChild(document.createElement('i'));
|
||||
document.body.appendChild(leaf);
|
||||
setTimeout(() => leaf.remove(), dur * 1000 + 150);
|
||||
}
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
if (!elements.roomError) return;
|
||||
const currentToken = ++errorToken;
|
||||
@@ -1775,13 +1846,12 @@ elements.copyInvite.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(elements.inviteLink.value).then(() => {
|
||||
const original = elements.copyInvite.textContent;
|
||||
elements.copyInvite.textContent = '✓';
|
||||
elements.copyInvite.style.background = 'var(--success)';
|
||||
elements.copyInvite.style.color = 'white';
|
||||
elements.copyInvite.classList.add('copy-success');
|
||||
showToast(getMessage('TOAST_INVITE_COPIED'), 'success', 2000);
|
||||
spawnLeafBurst(elements.copyInvite);
|
||||
setTimeout(() => {
|
||||
elements.copyInvite.textContent = original;
|
||||
elements.copyInvite.style.background = '';
|
||||
elements.copyInvite.style.color = '';
|
||||
elements.copyInvite.classList.remove('copy-success');
|
||||
}, 2000);
|
||||
}).catch(() => {
|
||||
showToast(getMessage('TOAST_COPY_FAILED'), 'error');
|
||||
@@ -1793,11 +1863,12 @@ if (elements.syncTabCopyInvite) {
|
||||
navigator.clipboard.writeText(elements.inviteLink.value).then(() => {
|
||||
const original = elements.syncTabCopyInvite.textContent;
|
||||
elements.syncTabCopyInvite.textContent = '✓';
|
||||
elements.syncTabCopyInvite.style.color = 'var(--success)';
|
||||
elements.syncTabCopyInvite.classList.add('copy-success');
|
||||
showToast(getMessage('TOAST_INVITE_COPIED'), 'success', 2000);
|
||||
spawnLeafBurst(elements.syncTabCopyInvite);
|
||||
setTimeout(() => {
|
||||
elements.syncTabCopyInvite.textContent = original;
|
||||
elements.syncTabCopyInvite.style.color = '';
|
||||
elements.syncTabCopyInvite.classList.remove('copy-success');
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
(() => {
|
||||
const systemTheme = window.matchMedia('(prefers-color-scheme: light)');
|
||||
let mode = 'system';
|
||||
|
||||
const normalize = value => ['system', 'dark', 'light'].includes(value) ? value : 'system';
|
||||
const apply = value => {
|
||||
mode = normalize(value);
|
||||
const light = mode === 'light' || (mode === 'system' && systemTheme.matches);
|
||||
document.documentElement.classList.toggle('theme-light', light);
|
||||
document.documentElement.dataset.theme = light ? 'light' : 'dark';
|
||||
document.documentElement.style.colorScheme = light ? 'light' : 'dark';
|
||||
};
|
||||
|
||||
apply('system');
|
||||
window.koalaTheme = {
|
||||
getMode: () => mode,
|
||||
setMode: apply
|
||||
};
|
||||
|
||||
systemTheme.addEventListener('change', () => {
|
||||
if (mode === 'system') apply('system');
|
||||
});
|
||||
|
||||
if (!globalThis.chrome?.storage?.local) return;
|
||||
chrome.storage.local.get(['themeMode'], data => apply(data.themeMode));
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area === 'local' && changes.themeMode) apply(changes.themeMode.newValue);
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user