Compare commits
3 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b016aca58 | ||
|
|
5bd3d31dd6 | ||
|
|
87ddd3bb93 |
@@ -1,101 +1,102 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>VibeTorrent</title>
|
||||
|
||||
<!-- PWA & Mobile Capable -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="VibeTorrent" />
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="icon" type="image/png" href="icon-192.png" />
|
||||
<link rel="apple-touch-icon" href="icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>VibeTorrent</title>
|
||||
|
||||
<!-- Trunk Assets -->
|
||||
<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="0" />
|
||||
<link data-trunk rel="css" href="public/tailwind.css" />
|
||||
<link data-trunk rel="copy-file" href="manifest.json" />
|
||||
<link data-trunk rel="copy-file" href="icon-192.png" />
|
||||
<link data-trunk rel="copy-file" href="icon-512.png" />
|
||||
<link data-trunk rel="copy-file" href="sw.js" />
|
||||
<script>
|
||||
(function () {
|
||||
var localTheme = localStorage.getItem("vibetorrent_theme");
|
||||
var t = localTheme || "dark";
|
||||
if (t === "Amoled") t = "black";
|
||||
if (t === "Light") t = "light";
|
||||
if (t === "Dark" || t === "Midnight") t = "dark";
|
||||
<!-- PWA & Mobile Capable -->
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="VibeTorrent" />
|
||||
<meta name="theme-color" content="#111827" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="icon" type="image/png" href="icon-192.png" />
|
||||
<link rel="apple-touch-icon" href="icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
|
||||
|
||||
var theme = t.toLowerCase();
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
if (!localTheme) {
|
||||
localStorage.setItem("vibetorrent_theme", "dark");
|
||||
}
|
||||
<!-- Trunk Assets -->
|
||||
<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="0" />
|
||||
<link data-trunk rel="css" href="public/tailwind.css" />
|
||||
<link data-trunk rel="copy-file" href="manifest.json" />
|
||||
<link data-trunk rel="copy-file" href="icon-192.png" />
|
||||
<link data-trunk rel="copy-file" href="icon-512.png" />
|
||||
<link data-trunk rel="copy-file" href="sw.js" />
|
||||
<script>
|
||||
(function () {
|
||||
var localTheme = localStorage.getItem("vibetorrent_theme");
|
||||
var t = localTheme || "dark";
|
||||
if (t === "Amoled") t = "black";
|
||||
if (t === "Light") t = "light";
|
||||
if (t === "Dark" || t === "Midnight") t = "dark";
|
||||
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) {
|
||||
var colorMap = {
|
||||
light: "#ffffff",
|
||||
cupcake: "#faf7f5",
|
||||
bumblebee: "#ffffff",
|
||||
emerald: "#ffffff",
|
||||
corporate: "#ffffff",
|
||||
synthwave: "#2d1b69",
|
||||
retro: "#ece3ca",
|
||||
cyberpunk: "#ffee00",
|
||||
valentine: "#f0d6e8",
|
||||
halloween: "#212121",
|
||||
garden: "#e9e7e7",
|
||||
forest: "#171212",
|
||||
aqua: "#345da7",
|
||||
lofi: "#ffffff",
|
||||
pastel: "#ffffff",
|
||||
fantasy: "#ffffff",
|
||||
wireframe: "#ffffff",
|
||||
black: "#000000",
|
||||
luxury: "#09090b",
|
||||
dracula: "#282a36",
|
||||
cmyk: "#ffffff",
|
||||
autumn: "#8C0327",
|
||||
business: "#202020",
|
||||
acid: "#fafafa",
|
||||
lemonade: "#F1F8E8",
|
||||
night: "#0f1729",
|
||||
coffee: "#20161f",
|
||||
winter: "#ffffff",
|
||||
dark: "#1d232a",
|
||||
};
|
||||
var color = colorMap[theme] || "#1d232a";
|
||||
meta.setAttribute("content", color);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
var theme = t.toLowerCase();
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
// Shadcn dark mode CSS değişkenleri .dark class ile çalışıyor
|
||||
var darkThemes = ["dark", "black", "night", "coffee", "luxury", "business", "dracula", "halloween", "forest", "synthwave", "dim", "nord", "sunset", "cyberpunk", "abyss"];
|
||||
if (darkThemes.indexOf(theme) !== -1) {
|
||||
document.documentElement.classList.add("dark");
|
||||
} else {
|
||||
document.documentElement.classList.remove("dark");
|
||||
}
|
||||
if (!localTheme) {
|
||||
localStorage.setItem("vibetorrent_theme", "dark");
|
||||
}
|
||||
|
||||
<body style="cursor: pointer;">
|
||||
<div
|
||||
id="app-loading"
|
||||
style="
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) {
|
||||
var colorMap = {
|
||||
light: "#ffffff",
|
||||
cupcake: "#faf7f5",
|
||||
bumblebee: "#ffffff",
|
||||
emerald: "#ffffff",
|
||||
corporate: "#ffffff",
|
||||
synthwave: "#2d1b69",
|
||||
retro: "#ece3ca",
|
||||
cyberpunk: "#ffee00",
|
||||
valentine: "#f0d6e8",
|
||||
halloween: "#212121",
|
||||
garden: "#e9e7e7",
|
||||
forest: "#171212",
|
||||
aqua: "#345da7",
|
||||
lofi: "#ffffff",
|
||||
pastel: "#ffffff",
|
||||
fantasy: "#ffffff",
|
||||
wireframe: "#ffffff",
|
||||
black: "#000000",
|
||||
luxury: "#09090b",
|
||||
dracula: "#282a36",
|
||||
cmyk: "#ffffff",
|
||||
autumn: "#8C0327",
|
||||
business: "#202020",
|
||||
acid: "#fafafa",
|
||||
lemonade: "#F1F8E8",
|
||||
night: "#0f1729",
|
||||
coffee: "#20161f",
|
||||
winter: "#ffffff",
|
||||
dark: "#1d232a",
|
||||
};
|
||||
var color = colorMap[theme] || "#1d232a";
|
||||
meta.setAttribute("content", color);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body style="cursor: pointer;">
|
||||
<div id="app-loading" style="
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
font-family: sans-serif;
|
||||
"
|
||||
>
|
||||
<div
|
||||
id="app-loading-spinner"
|
||||
style="
|
||||
">
|
||||
<div id="app-loading-spinner" style="
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid currentColor;
|
||||
@@ -103,21 +104,15 @@
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
opacity: 0.5;
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
id="app-loading-error"
|
||||
style="display: none; text-align: center; margin-top: 20px; padding: 0 20px"
|
||||
>
|
||||
<p style="color: #ef4444; font-weight: bold; margin-bottom: 8px">
|
||||
Uygulama yüklenemedi
|
||||
</p>
|
||||
<p style="font-size: 14px; opacity: 0.7">
|
||||
Bağlantınız yavaş olabilir veya bir sistem hatası oluşmuş olabilir.
|
||||
</p>
|
||||
<button
|
||||
onclick="location.reload()"
|
||||
style="
|
||||
"></div>
|
||||
<div id="app-loading-error" style="display: none; text-align: center; margin-top: 20px; padding: 0 20px">
|
||||
<p style="color: #ef4444; font-weight: bold; margin-bottom: 8px">
|
||||
Uygulama yüklenemedi
|
||||
</p>
|
||||
<p style="font-size: 14px; opacity: 0.7">
|
||||
Bağlantınız yavaş olabilir veya bir sistem hatası oluşmuş olabilir.
|
||||
</p>
|
||||
<button onclick="location.reload()" style="
|
||||
margin-top: 16px;
|
||||
padding: 8px 16px;
|
||||
background: #3b82f6;
|
||||
@@ -126,104 +121,105 @@
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
"
|
||||
>
|
||||
Sayfayı Yenile
|
||||
</button>
|
||||
</div>
|
||||
">
|
||||
Sayfayı Yenile
|
||||
</button>
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
</div>
|
||||
<style>
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
body.app-loaded #app-loading {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* iOS Safari Click Fixes */
|
||||
body {
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
summary {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// App loading timeout handler
|
||||
(function () {
|
||||
var timeout = setTimeout(function () {
|
||||
if (!document.body.classList.contains("app-loaded")) {
|
||||
var spinner = document.getElementById("app-loading-spinner");
|
||||
var error = document.getElementById("app-loading-error");
|
||||
if (spinner) spinner.style.display = "none";
|
||||
if (error) error.style.display = "block";
|
||||
}
|
||||
}
|
||||
}, 15000); // 15 seconds timeout
|
||||
|
||||
body.app-loaded #app-loading {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* iOS Safari Click Fixes */
|
||||
body {
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
summary {
|
||||
list-style: none;
|
||||
}
|
||||
summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// App loading timeout handler
|
||||
(function () {
|
||||
var timeout = setTimeout(function () {
|
||||
if (!document.body.classList.contains("app-loaded")) {
|
||||
var spinner = document.getElementById("app-loading-spinner");
|
||||
var error = document.getElementById("app-loading-error");
|
||||
if (spinner) spinner.style.display = "none";
|
||||
if (error) error.style.display = "block";
|
||||
}
|
||||
}, 15000); // 15 seconds timeout
|
||||
|
||||
// Clean up timeout if app loads
|
||||
var observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
if (
|
||||
mutation.attributeName === "class" &&
|
||||
document.body.classList.contains("app-loaded")
|
||||
) {
|
||||
clearTimeout(timeout);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe(document.body, { attributes: true });
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Service Worker Registration & PWA Setup -->
|
||||
<script>
|
||||
// Global Dropdown Closer for iOS/Mobile
|
||||
document.addEventListener('click', function(event) {
|
||||
const details = document.querySelectorAll('details[open]');
|
||||
details.forEach(detail => {
|
||||
// Eğer tıklanan yer bu details'in içinde değilse kapat
|
||||
if (!detail.contains(event.target)) {
|
||||
detail.removeAttribute('open');
|
||||
// Clean up timeout if app loads
|
||||
var observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
if (
|
||||
mutation.attributeName === "class" &&
|
||||
document.body.classList.contains("app-loaded")
|
||||
) {
|
||||
clearTimeout(timeout);
|
||||
observer.disconnect();
|
||||
}
|
||||
});
|
||||
}, true); // Use capture phase for better mobile support
|
||||
});
|
||||
observer.observe(document.body, { attributes: true });
|
||||
})();
|
||||
</script>
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js")
|
||||
.then((registration) => {
|
||||
console.log("✅ Service Worker registered:", registration);
|
||||
|
||||
// Request notification permission after a delay (better UX)
|
||||
setTimeout(() => {
|
||||
if ("Notification" in window && Notification.permission === "default") {
|
||||
// Only request if user hasn't decided yet
|
||||
const shouldRequest = localStorage.getItem("vibetorrent_notification_prompt_shown");
|
||||
if (!shouldRequest) {
|
||||
Notification.requestPermission().then((permission) => {
|
||||
console.log("Notification permission:", permission);
|
||||
localStorage.setItem("vibetorrent_notification_prompt_shown", "true");
|
||||
});
|
||||
}
|
||||
<!-- Service Worker Registration & PWA Setup -->
|
||||
<script>
|
||||
// Global Dropdown Closer for iOS/Mobile
|
||||
document.addEventListener('click', function (event) {
|
||||
const details = document.querySelectorAll('details[open]');
|
||||
details.forEach(detail => {
|
||||
// Eğer tıklanan yer bu details'in içinde değilse kapat
|
||||
if (!detail.contains(event.target)) {
|
||||
detail.removeAttribute('open');
|
||||
}
|
||||
});
|
||||
}, true); // Use capture phase for better mobile support
|
||||
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/sw.js")
|
||||
.then((registration) => {
|
||||
console.log("✅ Service Worker registered:", registration);
|
||||
|
||||
// Request notification permission after a delay (better UX)
|
||||
setTimeout(() => {
|
||||
if ("Notification" in window && Notification.permission === "default") {
|
||||
// Only request if user hasn't decided yet
|
||||
const shouldRequest = localStorage.getItem("vibetorrent_notification_prompt_shown");
|
||||
if (!shouldRequest) {
|
||||
Notification.requestPermission().then((permission) => {
|
||||
console.log("Notification permission:", permission);
|
||||
localStorage.setItem("vibetorrent_notification_prompt_shown", "true");
|
||||
});
|
||||
}
|
||||
}, 3000); // Wait 3 seconds before asking
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("⚠️ Service Worker registration failed:", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
}, 3000); // Wait 3 seconds before asking
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("⚠️ Service Worker registration failed:", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -3166,216 +3166,160 @@ button:not(#\#):not(#\#), input:where([type="button"], [type="reset"], [type="su
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:border-blue-800:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
border-color: rgb(25, 60, 184);
|
||||
border-color: var(--color-blue-800);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:border-blue-800 {
|
||||
border-color: rgb(25, 60, 184);
|
||||
border-color: var(--color-blue-800);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:border-destructive:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
border-color: hsl(0, 84%, 60%);
|
||||
border-color: var(--color-destructive);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:border-destructive {
|
||||
border-color: hsl(0, 84%, 60%);
|
||||
border-color: var(--color-destructive);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:border-gray-800:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
border-color: #1f2937;
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:border-gray-800 {
|
||||
border-color: #1f2937;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:border-green-800:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
border-color: rgb(1, 102, 48);
|
||||
border-color: var(--color-green-800);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:border-green-800 {
|
||||
border-color: rgb(1, 102, 48);
|
||||
border-color: var(--color-green-800);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:border-red-800:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
border-color: rgb(159, 7, 18);
|
||||
border-color: var(--color-red-800);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:border-red-800 {
|
||||
border-color: rgb(159, 7, 18);
|
||||
border-color: var(--color-red-800);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:border-yellow-800:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
border-color: rgb(135, 76, 0);
|
||||
border-color: color(display-p3 0.50318 0.30478 0.07554);
|
||||
border-color: var(--color-yellow-800);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:border-yellow-800 {
|
||||
border-color: rgb(135, 76, 0);
|
||||
border-color: color(display-p3 0.50318 0.30478 0.07554);
|
||||
border-color: var(--color-yellow-800);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-blue-900:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: rgb(28, 57, 142);
|
||||
background-color: var(--color-blue-900);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-blue-900 {
|
||||
background-color: rgb(28, 57, 142);
|
||||
background-color: var(--color-blue-900);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-blue-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: rgba(28, 57, 142, 0.3);
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-blue-900\/30 {
|
||||
background-color: rgba(28, 57, 142, 0.3);
|
||||
}
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-blue-900\/30 {
|
||||
background-color: color-mix(in oklab, rgb(28, 57, 142) 30%, transparent);
|
||||
}
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
.dark\:bg-blue-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: color-mix(in oklab, rgb(28, 57, 142) 30%, transparent);
|
||||
}
|
||||
|
||||
@supports (color: color-mix(in lch, red, blue)) {
|
||||
.dark\:bg-blue-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: color-mix(in oklab, var(--color-blue-900) 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-gray-800:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-gray-900:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: #111827;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-green-900:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: rgb(13, 84, 43);
|
||||
background-color: var(--color-green-900);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-green-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: rgba(13, 84, 43, 0.3);
|
||||
}
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
.dark\:bg-green-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: color-mix(in oklab, rgb(13, 84, 43) 30%, transparent);
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-blue-900\/30 {
|
||||
background-color: color-mix(in oklab, var(--color-blue-900) 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-gray-800 {
|
||||
background-color: #1f2937;
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-gray-900 {
|
||||
background-color: #111827;
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-green-900 {
|
||||
background-color: rgb(13, 84, 43);
|
||||
background-color: var(--color-green-900);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-green-900\/30 {
|
||||
background-color: rgba(13, 84, 43, 0.3);
|
||||
}
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-green-900\/30 {
|
||||
background-color: color-mix(in oklab, rgb(13, 84, 43) 30%, transparent);
|
||||
}
|
||||
|
||||
@supports (color: color-mix(in lch, red, blue)) {
|
||||
.dark\:bg-green-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: color-mix(in oklab, var(--color-green-900) 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-red-900:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: rgb(130, 24, 26);
|
||||
background-color: var(--color-red-900);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-red-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: rgba(130, 24, 26, 0.3);
|
||||
}
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
.dark\:bg-red-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: color-mix(in oklab, rgb(130, 24, 26) 30%, transparent);
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-green-900\/30 {
|
||||
background-color: color-mix(in oklab, var(--color-green-900) 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-red-900 {
|
||||
background-color: rgb(130, 24, 26);
|
||||
background-color: var(--color-red-900);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-red-900\/30 {
|
||||
background-color: rgba(130, 24, 26, 0.3);
|
||||
}
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-red-900\/30 {
|
||||
background-color: color-mix(in oklab, rgb(130, 24, 26) 30%, transparent);
|
||||
}
|
||||
|
||||
@supports (color: color-mix(in lch, red, blue)) {
|
||||
.dark\:bg-red-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: color-mix(in oklab, var(--color-red-900) 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-yellow-900:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: rgb(115, 62, 10);
|
||||
background-color: var(--color-yellow-900);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:bg-yellow-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: rgba(115, 62, 10, 0.3);
|
||||
}
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
.dark\:bg-yellow-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: color-mix(in oklab, rgb(115, 62, 10) 30%, transparent);
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-red-900\/30 {
|
||||
background-color: color-mix(in oklab, var(--color-red-900) 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-yellow-900 {
|
||||
background-color: rgb(115, 62, 10);
|
||||
background-color: var(--color-yellow-900);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-yellow-900\/30 {
|
||||
background-color: rgba(115, 62, 10, 0.3);
|
||||
}
|
||||
@supports (color: color-mix(in lab, red, red)) {
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-yellow-900\/30 {
|
||||
background-color: color-mix(in oklab, rgb(115, 62, 10) 30%, transparent);
|
||||
}
|
||||
|
||||
@supports (color: color-mix(in lch, red, blue)) {
|
||||
.dark\:bg-yellow-900\/30:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: color-mix(in oklab, var(--color-yellow-900) 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-blue-100:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(219, 234, 254);
|
||||
color: var(--color-blue-100);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-blue-400:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(86, 162, 255);
|
||||
color: color(display-p3 0.39744 0.62813 0.99212);
|
||||
color: var(--color-blue-400);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-blue-500:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(50, 128, 255);
|
||||
color: color(display-p3 0.26642 0.49122 0.98862);
|
||||
color: var(--color-blue-500);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-gray-100:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(243, 244, 246);
|
||||
color: var(--color-gray-100);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-green-100:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(220, 252, 231);
|
||||
color: var(--color-green-100);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-green-400:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(5, 223, 114);
|
||||
color: var(--color-green-400);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-green-500:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(0, 198, 90);
|
||||
color: color(display-p3 0.30873 0.77475 0.37431);
|
||||
color: var(--color-green-500);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-red-100:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(255, 226, 226);
|
||||
color: var(--color-red-100);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-red-400:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(255, 101, 104);
|
||||
color: color(display-p3 0.93353 0.43168 0.42349);
|
||||
color: var(--color-red-400);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-yellow-100:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(254, 249, 194);
|
||||
color: var(--color-yellow-100);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.dark\:text-yellow-400:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
color: rgb(247, 201, 0);
|
||||
color: color(display-p3 0.95994 0.79017 0.05852);
|
||||
color: var(--color-yellow-400);
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@media (hover: hover) {
|
||||
.dark\:hover\:bg-gray-700:hover:not(#\#):not(#\#):not(#\#):not(#\#) {
|
||||
background-color: #374151;
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:bg-yellow-900\/30 {
|
||||
background-color: color-mix(in oklab, var(--color-yellow-900) 30%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-blue-100 {
|
||||
color: rgb(219, 234, 254);
|
||||
color: var(--color-blue-100);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-blue-400 {
|
||||
color: rgb(86, 162, 255);
|
||||
color: color(display-p3 0.39744 0.62813 0.99212);
|
||||
color: var(--color-blue-400);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-blue-500 {
|
||||
color: rgb(50, 128, 255);
|
||||
color: color(display-p3 0.26642 0.49122 0.98862);
|
||||
color: var(--color-blue-500);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-gray-100 {
|
||||
color: rgb(243, 244, 246);
|
||||
color: var(--color-gray-100);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-green-100 {
|
||||
color: rgb(220, 252, 231);
|
||||
color: var(--color-green-100);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-green-400 {
|
||||
color: rgb(5, 223, 114);
|
||||
color: var(--color-green-400);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-green-500 {
|
||||
color: rgb(0, 198, 90);
|
||||
color: color(display-p3 0.30873 0.77475 0.37431);
|
||||
color: var(--color-green-500);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-red-100 {
|
||||
color: rgb(255, 226, 226);
|
||||
color: var(--color-red-100);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-red-400 {
|
||||
color: rgb(255, 101, 104);
|
||||
color: color(display-p3 0.93353 0.43168 0.42349);
|
||||
color: var(--color-red-400);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-yellow-100 {
|
||||
color: rgb(254, 249, 194);
|
||||
color: var(--color-yellow-100);
|
||||
}
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:text-yellow-400 {
|
||||
color: rgb(247, 201, 0);
|
||||
color: color(display-p3 0.95994 0.79017 0.05852);
|
||||
color: var(--color-yellow-400);
|
||||
}
|
||||
@media (hover: hover) {
|
||||
.dark:not(#\#):not(#\#):not(#\#):not(#\#) .dark\:hover\:bg-gray-700:hover {
|
||||
background-color: #374151;
|
||||
}
|
||||
}
|
||||
.\[\&_p\]\:leading-relaxed:not(#\#):not(#\#):not(#\#):not(#\#) p {
|
||||
|
||||
@@ -3,6 +3,9 @@ use leptos::task::spawn_local;
|
||||
use leptos_shadcn_button::{Button, ButtonVariant, ButtonSize};
|
||||
use leptos_shadcn_avatar::{Avatar, AvatarFallback};
|
||||
use leptos_shadcn_separator::Separator;
|
||||
use leptos::html;
|
||||
use leptos_use::storage::use_local_storage;
|
||||
use ::codee::string::FromToStringCodec;
|
||||
|
||||
#[component]
|
||||
pub fn Sidebar() -> impl IntoView {
|
||||
@@ -67,6 +70,37 @@ pub fn Sidebar() -> impl IntoView {
|
||||
username().chars().next().unwrap_or('?').to_uppercase().to_string()
|
||||
};
|
||||
|
||||
// --- THEME LOGIC START ---
|
||||
let (current_theme, set_current_theme, _) = use_local_storage::<String, FromToStringCodec>("vibetorrent_theme");
|
||||
|
||||
// Initialize with default if empty
|
||||
let current_theme_val = current_theme.get();
|
||||
if current_theme_val.is_empty() {
|
||||
set_current_theme.set("dark".to_string());
|
||||
}
|
||||
|
||||
// Automatically sync theme to document attribute
|
||||
Effect::new(move |_| {
|
||||
let theme = current_theme.get().to_lowercase();
|
||||
if let Some(doc) = document().document_element() {
|
||||
let _ = doc.set_attribute("data-theme", &theme);
|
||||
// Also set class for Shadcn dark mode support
|
||||
if theme == "dark" || theme == "dracula" || theme == "dim" || theme == "abyss" || theme == "sunset" || theme == "cyberpunk" || theme == "nord" || theme == "business" || theme == "night" || theme == "black" || theme == "luxury" || theme == "coffee" || theme == "forest" || theme == "halloween" || theme == "synthwave" {
|
||||
let _ = doc.class_list().add_1("dark");
|
||||
} else {
|
||||
let _ = doc.class_list().remove_1("dark");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let theme_details_ref = NodeRef::<html::Details>::new();
|
||||
let close_details = move |node_ref: NodeRef<html::Details>| {
|
||||
if let Some(el) = node_ref.get_untracked() {
|
||||
el.set_open(false);
|
||||
}
|
||||
};
|
||||
// --- THEME LOGIC END ---
|
||||
|
||||
view! {
|
||||
<div class="w-full h-full flex flex-col bg-card" style="padding-top: env(safe-area-inset-top);">
|
||||
<div class="p-4 flex-1 overflow-y-auto">
|
||||
@@ -169,6 +203,52 @@ pub fn Sidebar() -> impl IntoView {
|
||||
<div class="font-medium text-sm truncate text-foreground">{username}</div>
|
||||
<div class="text-[10px] text-muted-foreground truncate">"Online"</div>
|
||||
</div>
|
||||
|
||||
// --- THEME BUTTON ---
|
||||
<details class="group relative" node_ref=theme_details_ref>
|
||||
<summary class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-8 w-8 cursor-pointer outline-none list-none [&::-webkit-details-marker]:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
|
||||
</svg>
|
||||
</summary>
|
||||
|
||||
<div class="absolute bottom-full left-0 mb-2 z-[100] min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md hidden group-open:block animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2 max-h-64 overflow-y-auto">
|
||||
<ul class="w-full">
|
||||
{
|
||||
let themes = vec![
|
||||
"light", "dark", "dim", "nord", "cupcake", "dracula", "cyberpunk", "emerald", "sunset", "abyss"
|
||||
];
|
||||
themes.into_iter().map(|theme| {
|
||||
let theme_name = theme.to_string();
|
||||
let theme_name_for_class = theme_name.clone();
|
||||
let theme_name_for_onclick = theme_name.clone();
|
||||
let is_active = move || current_theme.get() == theme_name_for_class;
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
class=move || {
|
||||
let base = "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent hover:text-accent-foreground capitalize";
|
||||
if is_active() { format!("{} bg-accent text-accent-foreground font-medium", base) } else { base.to_string() }
|
||||
}
|
||||
on:click=move |_| {
|
||||
set_current_theme.set(theme_name_for_onclick.clone());
|
||||
close_details(theme_details_ref);
|
||||
}
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Show when=is_active.clone() fallback=|| ()>
|
||||
<span>"✓"</span>
|
||||
</Show>
|
||||
</span>
|
||||
{theme_name}
|
||||
</button>
|
||||
</li>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
<Button
|
||||
variant=ButtonVariant::Ghost
|
||||
size=ButtonSize::Icon
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use leptos::prelude::*;
|
||||
use leptos::html;
|
||||
use leptos_use::storage::use_local_storage;
|
||||
use ::codee::string::FromToStringCodec;
|
||||
use shared::GlobalLimitRequest;
|
||||
use crate::api;
|
||||
|
||||
@@ -30,27 +28,7 @@ pub fn StatusBar() -> impl IntoView {
|
||||
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
|
||||
let stats = store.global_stats;
|
||||
|
||||
let (current_theme, set_current_theme, _) = use_local_storage::<String, FromToStringCodec>("vibetorrent_theme");
|
||||
|
||||
// Initialize with default if empty
|
||||
let current_theme_val = current_theme.get();
|
||||
if current_theme_val.is_empty() {
|
||||
set_current_theme.set("dark".to_string());
|
||||
}
|
||||
|
||||
// Automatically sync theme to document attribute
|
||||
Effect::new(move |_| {
|
||||
let theme = current_theme.get().to_lowercase();
|
||||
if let Some(doc) = document().document_element() {
|
||||
let _ = doc.set_attribute("data-theme", &theme);
|
||||
// Also set class for Shadcn dark mode support
|
||||
if theme == "dark" || theme == "dracula" || theme == "dim" || theme == "abyss" {
|
||||
let _ = doc.class_list().add_1("dark");
|
||||
} else {
|
||||
let _ = doc.class_list().remove_1("dark");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Preset limits in bytes/s
|
||||
let limits: Vec<(i64, &str)> = vec!(
|
||||
@@ -91,7 +69,6 @@ pub fn StatusBar() -> impl IntoView {
|
||||
|
||||
let down_details_ref = NodeRef::<html::Details>::new();
|
||||
let up_details_ref = NodeRef::<html::Details>::new();
|
||||
let theme_details_ref = NodeRef::<html::Details>::new();
|
||||
|
||||
let close_details = move |node_ref: NodeRef<html::Details>| {
|
||||
if let Some(el) = node_ref.get_untracked() {
|
||||
@@ -201,49 +178,6 @@ pub fn StatusBar() -> impl IntoView {
|
||||
</details>
|
||||
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<details class="group relative" node_ref=theme_details_ref>
|
||||
<summary class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7 cursor-pointer outline-none list-none [&::-webkit-details-marker]:hidden">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
|
||||
</svg>
|
||||
</summary>
|
||||
|
||||
<div class="absolute bottom-full right-0 mb-2 z-[100] min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md hidden group-open:block animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2 max-h-96 overflow-y-auto">
|
||||
<ul class="w-full">
|
||||
{
|
||||
let themes = vec![
|
||||
"light", "dark", "dim", "nord", "cupcake", "dracula", "cyberpunk", "emerald", "sunset", "abyss"
|
||||
];
|
||||
themes.into_iter().map(|theme| {
|
||||
let theme_name = theme.to_string();
|
||||
let theme_name_for_class = theme_name.clone();
|
||||
let theme_name_for_onclick = theme_name.clone();
|
||||
let is_active = move || current_theme.get() == theme_name_for_class;
|
||||
view! {
|
||||
<li>
|
||||
<button
|
||||
class=move || {
|
||||
let base = "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent hover:text-accent-foreground capitalize";
|
||||
if is_active() { format!("{} bg-accent text-accent-foreground font-medium", base) } else { base.to_string() }
|
||||
}
|
||||
on:click=move |_| {
|
||||
set_current_theme.set(theme_name_for_onclick.clone());
|
||||
close_details(theme_details_ref);
|
||||
}
|
||||
>
|
||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<Show when=is_active.clone() fallback=|| ()>
|
||||
<span>"✓"</span>
|
||||
</Show>
|
||||
</span>
|
||||
{theme_name}
|
||||
</button> </li>
|
||||
}
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</details>
|
||||
<button
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:bg-accent hover:text-accent-foreground h-7 w-7"
|
||||
title="Settings & Notification Permissions"
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
use wasm_bindgen::prelude::*;
|
||||
use web_sys::{Notification, NotificationOptions};
|
||||
use leptos::prelude::*;
|
||||
use leptos_use::{use_web_notification, UseWebNotificationReturn, NotificationPermission};
|
||||
|
||||
/// Request browser notification permission from user
|
||||
pub async fn request_notification_permission() -> bool {
|
||||
if !is_notification_supported() {
|
||||
return false;
|
||||
}
|
||||
if let Ok(promise) = Notification::request_permission() {
|
||||
if let Ok(result) = wasm_bindgen_futures::JsFuture::from(promise).await {
|
||||
return result.as_string().unwrap_or_default() == "granted";
|
||||
@@ -21,6 +23,9 @@ pub fn is_notification_supported() -> bool {
|
||||
|
||||
/// Get current notification permission status
|
||||
pub fn get_notification_permission() -> String {
|
||||
if !is_notification_supported() {
|
||||
return "denied".to_string();
|
||||
}
|
||||
match Notification::permission() {
|
||||
web_sys::NotificationPermission::Granted => "granted".to_string(),
|
||||
web_sys::NotificationPermission::Denied => "denied".to_string(),
|
||||
@@ -32,8 +37,6 @@ pub fn get_notification_permission() -> String {
|
||||
/// Hook for using browser notifications within Leptos components or effects.
|
||||
/// This uses leptos-use for reactive permission tracking.
|
||||
pub fn use_app_notification() -> impl Fn(&str, &str) + Clone {
|
||||
let UseWebNotificationReturn { permission, .. } = use_web_notification();
|
||||
|
||||
move |title: &str, body: &str| {
|
||||
// Check user preference from localStorage
|
||||
let window = web_sys::window().expect("no global window");
|
||||
@@ -42,8 +45,8 @@ pub fn use_app_notification() -> impl Fn(&str, &str) + Clone {
|
||||
.and_then(|s| s.get_item("vibetorrent_browser_notifications").ok().flatten())
|
||||
.unwrap_or_else(|| "true".to_string());
|
||||
|
||||
// Use the reactive permission signal from leptos-use
|
||||
if enabled == "true" && permission.get() == NotificationPermission::Granted {
|
||||
// Check platform support and permission
|
||||
if enabled == "true" && is_notification_supported() && get_notification_permission() == "granted" {
|
||||
show_browser_notification(title, body);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const cargoRegistry = path.join(
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
darkMode: "class",
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{rs,html}",
|
||||
|
||||
Reference in New Issue
Block a user