Compare commits
10 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5b016aca58 | ||
|
|
5bd3d31dd6 | ||
|
|
87ddd3bb93 | ||
|
|
463249982c | ||
|
|
9447a66cc1 | ||
|
|
45247a020e | ||
|
|
77b77c7775 | ||
|
|
8ef3008cb8 | ||
|
|
ca1dd0caac | ||
|
|
ad336789d9 |
114
Cargo.lock
generated
114
Cargo.lock
generated
@@ -1260,17 +1260,24 @@ dependencies = [
|
|||||||
"gloo-timers",
|
"gloo-timers",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"leptos",
|
"leptos",
|
||||||
|
"leptos-shadcn-alert",
|
||||||
"leptos-shadcn-avatar",
|
"leptos-shadcn-avatar",
|
||||||
"leptos-shadcn-badge",
|
"leptos-shadcn-badge",
|
||||||
"leptos-shadcn-button",
|
"leptos-shadcn-button",
|
||||||
"leptos-shadcn-card",
|
"leptos-shadcn-card",
|
||||||
"leptos-shadcn-context-menu",
|
"leptos-shadcn-context-menu",
|
||||||
|
"leptos-shadcn-dialog",
|
||||||
|
"leptos-shadcn-dropdown-menu",
|
||||||
"leptos-shadcn-input",
|
"leptos-shadcn-input",
|
||||||
|
"leptos-shadcn-label",
|
||||||
"leptos-shadcn-progress",
|
"leptos-shadcn-progress",
|
||||||
"leptos-shadcn-scroll-area",
|
"leptos-shadcn-scroll-area",
|
||||||
"leptos-shadcn-separator",
|
"leptos-shadcn-separator",
|
||||||
"leptos-shadcn-sheet",
|
"leptos-shadcn-sheet",
|
||||||
|
"leptos-shadcn-skeleton",
|
||||||
"leptos-shadcn-tabs",
|
"leptos-shadcn-tabs",
|
||||||
|
"leptos-shadcn-toast",
|
||||||
|
"leptos-shadcn-tooltip",
|
||||||
"leptos-use",
|
"leptos-use",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
"log",
|
"log",
|
||||||
@@ -2159,6 +2166,21 @@ dependencies = [
|
|||||||
"send_wrapper",
|
"send_wrapper",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-alert"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "884551df61ade405bdb61b0d5a92a3a88b3a7af7a2d283b8d3e942ae0d71309c"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-shadcn-avatar"
|
name = "leptos-shadcn-avatar"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2234,6 +2256,36 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-dialog"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a3fdbb636393b150c2db1e37d44a6832e9dde177ce2e81281932fefad8bde98e"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-dropdown-menu"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f50189623a176386a30443d281483c5aa6cc34dc45fa11c3e53bd187ffccde21"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-shadcn-input"
|
name = "leptos-shadcn-input"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2250,6 +2302,21 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-label"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e7cad4b5fae11df9bf3b1d4265a56509a9bb7d3a8580e7487f398b733eadf0c"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-shadcn-progress"
|
name = "leptos-shadcn-progress"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2324,6 +2391,21 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-skeleton"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c14b6bd0f2fe191e3e114a34cee889fc983546ad488e76e76511e3d75ea3f86"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-shadcn-tabs"
|
name = "leptos-shadcn-tabs"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2339,6 +2421,38 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-toast"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3315f7ed844e3286704cc7b57db7209cad592c11eee770f5dc48ebdc92d66cfb"
|
||||||
|
dependencies = [
|
||||||
|
"gloo-timers",
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"uuid",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-tooltip"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e41d37932d700444e1d3a21f10f198c3c9e76dde3fd78d58da4b5a099939fd7"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-struct-component"
|
name = "leptos-struct-component"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
1
frontend/.gitignore
vendored
Normal file
1
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules/
|
||||||
@@ -45,4 +45,11 @@ leptos-shadcn-progress = "0.8"
|
|||||||
leptos-shadcn-avatar = "0.8"
|
leptos-shadcn-avatar = "0.8"
|
||||||
leptos-shadcn-sheet = "0.8"
|
leptos-shadcn-sheet = "0.8"
|
||||||
leptos-shadcn-tabs = "0.8"
|
leptos-shadcn-tabs = "0.8"
|
||||||
leptos-shadcn-scroll-area = "0.8"
|
leptos-shadcn-scroll-area = "0.8"
|
||||||
|
leptos-shadcn-dialog = "0.8"
|
||||||
|
leptos-shadcn-label = "0.8"
|
||||||
|
leptos-shadcn-alert = "0.8"
|
||||||
|
leptos-shadcn-toast = "0.8"
|
||||||
|
leptos-shadcn-dropdown-menu = "0.8"
|
||||||
|
leptos-shadcn-tooltip = "0.8"
|
||||||
|
leptos-shadcn-skeleton = "0.8"
|
||||||
@@ -1,101 +1,102 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<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 -->
|
<head>
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta charset="utf-8" />
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="viewport"
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<meta name="apple-mobile-web-app-title" content="VibeTorrent" />
|
<title>VibeTorrent</title>
|
||||||
<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" />
|
|
||||||
|
|
||||||
<!-- Trunk Assets -->
|
<!-- PWA & Mobile Capable -->
|
||||||
<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="0" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<link data-trunk rel="css" href="public/tailwind.css" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
<link data-trunk rel="copy-file" href="manifest.json" />
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||||
<link data-trunk rel="copy-file" href="icon-192.png" />
|
<meta name="apple-mobile-web-app-title" content="VibeTorrent" />
|
||||||
<link data-trunk rel="copy-file" href="icon-512.png" />
|
<meta name="theme-color" content="#111827" />
|
||||||
<link data-trunk rel="copy-file" href="sw.js" />
|
<link rel="manifest" href="manifest.json" />
|
||||||
<script>
|
<link rel="icon" type="image/png" href="icon-192.png" />
|
||||||
(function () {
|
<link rel="apple-touch-icon" href="icon-192.png" />
|
||||||
var localTheme = localStorage.getItem("vibetorrent_theme");
|
<link rel="apple-touch-icon" sizes="192x192" href="icon-192.png" />
|
||||||
var t = localTheme || "dark";
|
<link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" />
|
||||||
if (t === "Amoled") t = "black";
|
|
||||||
if (t === "Light") t = "light";
|
|
||||||
if (t === "Dark" || t === "Midnight") t = "dark";
|
|
||||||
|
|
||||||
var theme = t.toLowerCase();
|
<!-- Trunk Assets -->
|
||||||
document.documentElement.setAttribute("data-theme", theme);
|
<link data-trunk rel="rust" href="Cargo.toml" data-wasm-opt="0" />
|
||||||
if (!localTheme) {
|
<link data-trunk rel="css" href="public/tailwind.css" />
|
||||||
localStorage.setItem("vibetorrent_theme", "dark");
|
<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"]');
|
var theme = t.toLowerCase();
|
||||||
if (meta) {
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
var colorMap = {
|
// Shadcn dark mode CSS değişkenleri .dark class ile çalışıyor
|
||||||
light: "#ffffff",
|
var darkThemes = ["dark", "black", "night", "coffee", "luxury", "business", "dracula", "halloween", "forest", "synthwave", "dim", "nord", "sunset", "cyberpunk", "abyss"];
|
||||||
cupcake: "#faf7f5",
|
if (darkThemes.indexOf(theme) !== -1) {
|
||||||
bumblebee: "#ffffff",
|
document.documentElement.classList.add("dark");
|
||||||
emerald: "#ffffff",
|
} else {
|
||||||
corporate: "#ffffff",
|
document.documentElement.classList.remove("dark");
|
||||||
synthwave: "#2d1b69",
|
}
|
||||||
retro: "#ece3ca",
|
if (!localTheme) {
|
||||||
cyberpunk: "#ffee00",
|
localStorage.setItem("vibetorrent_theme", "dark");
|
||||||
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;">
|
var meta = document.querySelector('meta[name="theme-color"]');
|
||||||
<div
|
if (meta) {
|
||||||
id="app-loading"
|
var colorMap = {
|
||||||
style="
|
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;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
"
|
">
|
||||||
>
|
<div id="app-loading-spinner" style="
|
||||||
<div
|
|
||||||
id="app-loading-spinner"
|
|
||||||
style="
|
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border: 3px solid currentColor;
|
border: 3px solid currentColor;
|
||||||
@@ -103,21 +104,15 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
"
|
"></div>
|
||||||
></div>
|
<div id="app-loading-error" style="display: none; text-align: center; margin-top: 20px; padding: 0 20px">
|
||||||
<div
|
<p style="color: #ef4444; font-weight: bold; margin-bottom: 8px">
|
||||||
id="app-loading-error"
|
Uygulama yüklenemedi
|
||||||
style="display: none; text-align: center; margin-top: 20px; padding: 0 20px"
|
</p>
|
||||||
>
|
<p style="font-size: 14px; opacity: 0.7">
|
||||||
<p style="color: #ef4444; font-weight: bold; margin-bottom: 8px">
|
Bağlantınız yavaş olabilir veya bir sistem hatası oluşmuş olabilir.
|
||||||
Uygulama yüklenemedi
|
</p>
|
||||||
</p>
|
<button onclick="location.reload()" style="
|
||||||
<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;
|
margin-top: 16px;
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
background: #3b82f6;
|
background: #3b82f6;
|
||||||
@@ -126,104 +121,105 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
"
|
">
|
||||||
>
|
Sayfayı Yenile
|
||||||
Sayfayı Yenile
|
</button>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<style>
|
</div>
|
||||||
@keyframes spin {
|
<style>
|
||||||
to {
|
@keyframes spin {
|
||||||
transform: rotate(360deg);
|
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 {
|
// Clean up timeout if app loads
|
||||||
display: none !important;
|
var observer = new MutationObserver(function (mutations) {
|
||||||
}
|
mutations.forEach(function (mutation) {
|
||||||
|
if (
|
||||||
/* iOS Safari Click Fixes */
|
mutation.attributeName === "class" &&
|
||||||
body {
|
document.body.classList.contains("app-loaded")
|
||||||
cursor: pointer;
|
) {
|
||||||
-webkit-tap-highlight-color: transparent;
|
clearTimeout(timeout);
|
||||||
}
|
observer.disconnect();
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, true); // Use capture phase for better mobile support
|
});
|
||||||
|
observer.observe(document.body, { attributes: true });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
if ("serviceWorker" in navigator) {
|
<!-- Service Worker Registration & PWA Setup -->
|
||||||
window.addEventListener("load", () => {
|
<script>
|
||||||
navigator.serviceWorker
|
// Global Dropdown Closer for iOS/Mobile
|
||||||
.register("/sw.js")
|
document.addEventListener('click', function (event) {
|
||||||
.then((registration) => {
|
const details = document.querySelectorAll('details[open]');
|
||||||
console.log("✅ Service Worker registered:", registration);
|
details.forEach(detail => {
|
||||||
|
// Eğer tıklanan yer bu details'in içinde değilse kapat
|
||||||
// Request notification permission after a delay (better UX)
|
if (!detail.contains(event.target)) {
|
||||||
setTimeout(() => {
|
detail.removeAttribute('open');
|
||||||
if ("Notification" in window && Notification.permission === "default") {
|
}
|
||||||
// Only request if user hasn't decided yet
|
});
|
||||||
const shouldRequest = localStorage.getItem("vibetorrent_notification_prompt_shown");
|
}, true); // Use capture phase for better mobile support
|
||||||
if (!shouldRequest) {
|
|
||||||
Notification.requestPermission().then((permission) => {
|
if ("serviceWorker" in navigator) {
|
||||||
console.log("Notification permission:", permission);
|
window.addEventListener("load", () => {
|
||||||
localStorage.setItem("vibetorrent_notification_prompt_shown", "true");
|
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
|
}
|
||||||
})
|
}, 3000); // Wait 3 seconds before asking
|
||||||
.catch((error) => {
|
})
|
||||||
console.warn("⚠️ Service Worker registration failed:", error);
|
.catch((error) => {
|
||||||
});
|
console.warn("⚠️ Service Worker registration failed:", error);
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
</script>
|
}
|
||||||
</body>
|
</script>
|
||||||
</html>
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@config "./tailwind.config.js";
|
@config "./tailwind.config.js";
|
||||||
|
@source "../src/**/*.rs";
|
||||||
|
@source "/Users/bilal/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/leptos-shadcn-*/src/**/*.rs";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* Shadcn Colors */
|
/* Shadcn Colors */
|
||||||
@@ -41,14 +43,17 @@
|
|||||||
from {
|
from {
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
height: var(--radix-accordion-content-height);
|
height: var(--radix-accordion-content-height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes accordion-up {
|
@keyframes accordion-up {
|
||||||
from {
|
from {
|
||||||
height: var(--radix-accordion-content-height);
|
height: var(--radix-accordion-content-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
@@ -123,17 +128,35 @@
|
|||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure Shadcn Utilities are always available */
|
/* Ensure Shadcn Utilities are always available */
|
||||||
.bg-popover { background-color: hsl(var(--popover)); }
|
.bg-popover {
|
||||||
.text-popover-foreground { color: hsl(var(--popover-foreground)); }
|
background-color: hsl(var(--popover));
|
||||||
.border-border { border-color: hsl(var(--border)); }
|
}
|
||||||
.shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); }
|
|
||||||
.z-50 { z-index: 50; }
|
.text-popover-foreground {
|
||||||
.z-100 { z-index: 100; }
|
color: hsl(var(--popover-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-border {
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-md {
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-50 {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-100 {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix for iOS click/blur events */
|
/* Fix for iOS click/blur events */
|
||||||
@@ -151,4 +174,4 @@
|
|||||||
|
|
||||||
:focus {
|
:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
2853
frontend/package-lock.json
generated
2853
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -10,8 +10,11 @@
|
|||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
"postcss-cli": "^11.0.1",
|
||||||
|
"postcss-preset-env": "^11.1.3",
|
||||||
"tailwindcss": "^4.1.18"
|
"tailwindcss": "^4.1.18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
15
frontend/postcss.config.cjs
Normal file
15
frontend/postcss.config.cjs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
"postcss-preset-env": {
|
||||||
|
features: {
|
||||||
|
"nesting-rules": true,
|
||||||
|
},
|
||||||
|
browsers: [
|
||||||
|
"last 2 versions",
|
||||||
|
"iOS >= 15",
|
||||||
|
"Safari >= 15",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,7 @@ use leptos::prelude::*;
|
|||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
use leptos_router::components::{Router, Routes, Route};
|
use leptos_router::components::{Router, Routes, Route};
|
||||||
use leptos_router::hooks::use_navigate;
|
use leptos_router::hooks::use_navigate;
|
||||||
|
use leptos_shadcn_skeleton::Skeleton;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
@@ -116,10 +117,40 @@ pub fn App() -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Show when=move || !is_loading.0.get() fallback=|| view! {
|
<Show when=move || !is_loading.0.get() fallback=|| view! {
|
||||||
<div class="flex items-center justify-center h-screen bg-background">
|
<div class="flex h-screen bg-background">
|
||||||
<div class="flex flex-col items-center gap-4">
|
// Sidebar skeleton
|
||||||
<div class="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
<div class="w-56 border-r border-border p-4 space-y-4">
|
||||||
<p class="text-sm text-muted-foreground">"Yükleniyor..."</p>
|
<Skeleton class="h-8 w-3/4" />
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Skeleton class="h-6 w-full" />
|
||||||
|
<Skeleton class="h-6 w-full" />
|
||||||
|
<Skeleton class="h-6 w-4/5" />
|
||||||
|
<Skeleton class="h-6 w-full" />
|
||||||
|
<Skeleton class="h-6 w-3/5" />
|
||||||
|
<Skeleton class="h-6 w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
// Main content skeleton
|
||||||
|
<div class="flex-1 flex flex-col">
|
||||||
|
// Header skeleton
|
||||||
|
<div class="border-b border-border p-4 flex items-center gap-4">
|
||||||
|
<Skeleton class="h-8 w-48" />
|
||||||
|
<Skeleton class="h-8 w-64" />
|
||||||
|
<div class="ml-auto"><Skeleton class="h-8 w-24" /></div>
|
||||||
|
</div>
|
||||||
|
// Table skeleton rows
|
||||||
|
<div class="flex-1 p-4 space-y-3">
|
||||||
|
<Skeleton class="h-10 w-full" />
|
||||||
|
<Skeleton class="h-10 w-full" />
|
||||||
|
<Skeleton class="h-10 w-full" />
|
||||||
|
<Skeleton class="h-10 w-full" />
|
||||||
|
<Skeleton class="h-10 w-full" />
|
||||||
|
<Skeleton class="h-10 w-3/4" />
|
||||||
|
</div>
|
||||||
|
// Status bar skeleton
|
||||||
|
<div class="border-t border-border p-3">
|
||||||
|
<Skeleton class="h-5 w-96" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}.into_any()>
|
}.into_any()>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
use leptos_shadcn_card::{Card, CardHeader, CardContent};
|
||||||
|
use leptos_shadcn_input::Input;
|
||||||
|
use leptos_shadcn_button::Button;
|
||||||
|
use leptos_shadcn_label::Label;
|
||||||
|
use leptos_shadcn_alert::{Alert, AlertDescription, AlertVariant};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Login() -> impl IntoView {
|
pub fn Login() -> impl IntoView {
|
||||||
@@ -32,8 +37,8 @@ pub fn Login() -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
|
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
|
||||||
<div class="w-full max-w-sm rounded-xl border border-border bg-card text-card-foreground shadow-lg">
|
<Card class="w-full max-w-sm shadow-lg">
|
||||||
<div class="flex flex-col space-y-1.5 p-6 pb-2 items-center">
|
<CardHeader class="pb-2 items-center">
|
||||||
<div class="w-12 h-12 bg-primary rounded-xl flex items-center justify-center text-primary-foreground shadow-sm mb-4">
|
<div class="w-12 h-12 bg-primary rounded-xl flex items-center justify-center text-primary-foreground shadow-sm mb-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.25 0 003 2.48z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.25 0 003 2.48z" />
|
||||||
@@ -42,56 +47,53 @@ pub fn Login() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="font-semibold tracking-tight text-2xl">"VibeTorrent"</h3>
|
<h3 class="font-semibold tracking-tight text-2xl">"VibeTorrent"</h3>
|
||||||
<p class="text-sm text-muted-foreground">"Hesabınıza giriş yapın"</p>
|
<p class="text-sm text-muted-foreground">"Hesabınıza giriş yapın"</p>
|
||||||
</div>
|
</CardHeader>
|
||||||
|
|
||||||
<div class="p-6 pt-4">
|
<CardContent class="pt-4">
|
||||||
<form on:submit=handle_login class="space-y-4">
|
<form on:submit=handle_login class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium leading-none">"Kullanıcı Adı"</label>
|
<Label>"Kullanıcı Adı"</Label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
input_type="text"
|
||||||
placeholder="Kullanıcı adınız"
|
placeholder="Kullanıcı adınız"
|
||||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
value=MaybeProp::derive(move || Some(username.0.get()))
|
||||||
prop:value=move || username.0.get()
|
on_change=Callback::new(move |val: String| username.1.set(val))
|
||||||
on:input=move |ev| username.1.set(event_target_value(&ev))
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
disabled=move || loading.0.get()
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium leading-none">"Şifre"</label>
|
<Label>"Şifre"</Label>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
input_type="password"
|
||||||
placeholder="******"
|
placeholder="******"
|
||||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
value=MaybeProp::derive(move || Some(password.0.get()))
|
||||||
prop:value=move || password.0.get()
|
on_change=Callback::new(move |val: String| password.1.set(val))
|
||||||
on:input=move |ev| password.1.set(event_target_value(&ev))
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
disabled=move || loading.0.get()
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when=move || error.0.get().is_some()>
|
<Show when=move || error.0.get().is_some()>
|
||||||
<div class="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
<Alert variant=AlertVariant::Destructive>
|
||||||
{move || error.0.get().unwrap_or_default()}
|
<AlertDescription>
|
||||||
</div>
|
{move || error.0.get().unwrap_or_default()}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<button
|
<Button
|
||||||
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2 w-full"
|
class="w-full"
|
||||||
type="submit"
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
disabled=move || loading.0.get()
|
|
||||||
>
|
>
|
||||||
<Show when=move || loading.0.get() fallback=|| "Giriş Yap">
|
<Show when=move || loading.0.get() fallback=|| "Giriş Yap">
|
||||||
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
||||||
"Giriş Yapılıyor..."
|
"Giriş Yapılıyor..."
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
use leptos_shadcn_card::{Card, CardHeader, CardContent};
|
||||||
|
use leptos_shadcn_input::Input;
|
||||||
|
use leptos_shadcn_button::Button;
|
||||||
|
use leptos_shadcn_label::Label;
|
||||||
|
use leptos_shadcn_alert::{Alert, AlertDescription, AlertVariant};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Setup() -> impl IntoView {
|
pub fn Setup() -> impl IntoView {
|
||||||
@@ -48,8 +53,8 @@ pub fn Setup() -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
|
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
|
||||||
<div class="w-full max-w-md rounded-xl border border-border bg-card text-card-foreground shadow-lg overflow-hidden">
|
<Card class="w-full max-w-md shadow-lg overflow-hidden">
|
||||||
<div class="flex flex-col space-y-1.5 p-6 pb-2 items-center text-center">
|
<CardHeader class="pb-2 items-center text-center">
|
||||||
<div class="w-12 h-12 bg-primary rounded-xl flex items-center justify-center text-primary-foreground shadow-sm mb-4">
|
<div class="w-12 h-12 bg-primary rounded-xl flex items-center justify-center text-primary-foreground shadow-sm mb-4">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-3.497a2.548 2.548 0 113.586 3.586l-6.837 5.63m-5.108 3.497l2.496-3.03c.317-.384.74-.626 1.208-.766M15.75 9.25a2.548 2.548 0 11-5.096 0 2.548 2.548 0 015.096 0z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-3.497a2.548 2.548 0 113.586 3.586l-6.837 5.63m-5.108 3.497l2.496-3.03c.317-.384.74-.626 1.208-.766M15.75 9.25a2.548 2.548 0 11-5.096 0 2.548 2.548 0 015.096 0z" />
|
||||||
@@ -57,74 +62,63 @@ pub fn Setup() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="font-semibold tracking-tight text-2xl">"VibeTorrent Kurulumu"</h3>
|
<h3 class="font-semibold tracking-tight text-2xl">"VibeTorrent Kurulumu"</h3>
|
||||||
<p class="text-sm text-muted-foreground">"Yönetici hesabınızı oluşturun"</p>
|
<p class="text-sm text-muted-foreground">"Yönetici hesabınızı oluşturun"</p>
|
||||||
</div>
|
</CardHeader>
|
||||||
|
|
||||||
<div class="p-6 pt-4">
|
<CardContent class="pt-4">
|
||||||
<form on:submit=handle_setup class="space-y-4">
|
<form on:submit=handle_setup class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
<Label>"Yönetici Kullanıcı Adı"</Label>
|
||||||
"Yönetici Kullanıcı Adı"
|
<Input
|
||||||
</label>
|
input_type="text"
|
||||||
<input
|
placeholder="admin"
|
||||||
type="text"
|
value=MaybeProp::derive(move || Some(username.0.get()))
|
||||||
placeholder="admin"
|
on_change=Callback::new(move |val: String| username.1.set(val))
|
||||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
prop:value=move || username.0.get()
|
|
||||||
on:input=move |ev| username.1.set(event_target_value(&ev))
|
|
||||||
disabled=move || loading.0.get()
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
<Label>"Şifre"</Label>
|
||||||
"Şifre"
|
<Input
|
||||||
</label>
|
input_type="password"
|
||||||
<input
|
placeholder="******"
|
||||||
type="password"
|
value=MaybeProp::derive(move || Some(password.0.get()))
|
||||||
placeholder="******"
|
on_change=Callback::new(move |val: String| password.1.set(val))
|
||||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
prop:value=move || password.0.get()
|
|
||||||
on:input=move |ev| password.1.set(event_target_value(&ev))
|
|
||||||
disabled=move || loading.0.get()
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
<Label>"Şifre Onay"</Label>
|
||||||
"Şifre Onay"
|
<Input
|
||||||
</label>
|
input_type="password"
|
||||||
<input
|
placeholder="******"
|
||||||
type="password"
|
value=MaybeProp::derive(move || Some(confirm_password.0.get()))
|
||||||
placeholder="******"
|
on_change=Callback::new(move |val: String| confirm_password.1.set(val))
|
||||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
prop:value=move || confirm_password.0.get()
|
|
||||||
on:input=move |ev| confirm_password.1.set(event_target_value(&ev))
|
|
||||||
disabled=move || loading.0.get()
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when=move || error.0.get().is_some() fallback=|| ()>
|
<Show when=move || error.0.get().is_some() fallback=|| ()>
|
||||||
<div class="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive dark:border-destructive dark:bg-destructive/50 dark:text-destructive-foreground">
|
<Alert variant=AlertVariant::Destructive>
|
||||||
<span>{move || error.0.get().unwrap_or_default()}</span>
|
<AlertDescription>
|
||||||
</div>
|
<span>{move || error.0.get().unwrap_or_default()}</span>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<button
|
<Button
|
||||||
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2 w-full"
|
class="w-full"
|
||||||
type="submit"
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
disabled=move || loading.0.get()
|
|
||||||
>
|
>
|
||||||
<Show when=move || loading.0.get() fallback=|| "Kurulumu Tamamla">
|
<Show when=move || loading.0.get() fallback=|| "Kurulumu Tamamla">
|
||||||
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
||||||
"Kuruluyor..."
|
"Kuruluyor..."
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,24 +63,35 @@ pub fn TorrentContextMenu(
|
|||||||
|
|
||||||
<Show when=move || open.get()>
|
<Show when=move || open.get()>
|
||||||
{
|
{
|
||||||
let (x, y) = position.get();
|
let (x, y) = position.get();
|
||||||
view! {
|
// Menü yaklaşık boyutları
|
||||||
<div
|
let menu_width = 200;
|
||||||
class="fixed inset-0 z-[99]"
|
let menu_height = 220;
|
||||||
on:click=move |e: MouseEvent| {
|
let window = web_sys::window().unwrap();
|
||||||
e.stop_propagation();
|
let vw = window.inner_width().unwrap().as_f64().unwrap() as i32;
|
||||||
open.set(false);
|
let vh = window.inner_height().unwrap().as_f64().unwrap() as i32;
|
||||||
}
|
// Sağa taşarsa sola aç, alta taşarsa yukarı aç
|
||||||
on:contextmenu=move |e: MouseEvent| {
|
let final_x = if x + menu_width > vw { x - menu_width } else { x };
|
||||||
e.prevent_default();
|
let final_y = if y + menu_height > vh { y - menu_height } else { y };
|
||||||
e.stop_propagation();
|
let final_x = final_x.max(0);
|
||||||
open.set(false);
|
let final_y = final_y.max(0);
|
||||||
}
|
view! {
|
||||||
/>
|
<div
|
||||||
<div
|
class="fixed inset-0 z-[99]"
|
||||||
class="fixed z-[100] min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
|
on:click=move |e: MouseEvent| {
|
||||||
style=format!("left: {}px; top: {}px;", x, y)
|
e.stop_propagation();
|
||||||
on:click=move |e: MouseEvent| e.stop_propagation()
|
open.set(false);
|
||||||
|
}
|
||||||
|
on:contextmenu=move |e: MouseEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
e.stop_propagation();
|
||||||
|
open.set(false);
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="fixed z-[100] min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
|
||||||
|
style=format!("left: {}px; top: {}px;", final_x, final_y)
|
||||||
|
on:click=move |e: MouseEvent| e.stop_propagation()
|
||||||
>
|
>
|
||||||
// Start
|
// Start
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ use leptos::task::spawn_local;
|
|||||||
use leptos_shadcn_button::{Button, ButtonVariant, ButtonSize};
|
use leptos_shadcn_button::{Button, ButtonVariant, ButtonSize};
|
||||||
use leptos_shadcn_avatar::{Avatar, AvatarFallback};
|
use leptos_shadcn_avatar::{Avatar, AvatarFallback};
|
||||||
use leptos_shadcn_separator::Separator;
|
use leptos_shadcn_separator::Separator;
|
||||||
|
use leptos::html;
|
||||||
|
use leptos_use::storage::use_local_storage;
|
||||||
|
use ::codee::string::FromToStringCodec;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Sidebar() -> impl IntoView {
|
pub fn Sidebar() -> impl IntoView {
|
||||||
@@ -67,6 +70,37 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
username().chars().next().unwrap_or('?').to_uppercase().to_string()
|
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! {
|
view! {
|
||||||
<div class="w-full h-full flex flex-col bg-card" style="padding-top: env(safe-area-inset-top);">
|
<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">
|
<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="font-medium text-sm truncate text-foreground">{username}</div>
|
||||||
<div class="text-[10px] text-muted-foreground truncate">"Online"</div>
|
<div class="text-[10px] text-muted-foreground truncate">"Online"</div>
|
||||||
</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
|
<Button
|
||||||
variant=ButtonVariant::Ghost
|
variant=ButtonVariant::Ghost
|
||||||
size=ButtonSize::Icon
|
size=ButtonSize::Icon
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::html;
|
use leptos::html;
|
||||||
use leptos_use::storage::use_local_storage;
|
|
||||||
use ::codee::string::FromToStringCodec;
|
|
||||||
use shared::GlobalLimitRequest;
|
use shared::GlobalLimitRequest;
|
||||||
use crate::api;
|
use crate::api;
|
||||||
|
|
||||||
@@ -30,27 +28,7 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
|
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
|
||||||
let stats = store.global_stats;
|
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
|
// Preset limits in bytes/s
|
||||||
let limits: Vec<(i64, &str)> = vec!(
|
let limits: Vec<(i64, &str)> = vec!(
|
||||||
@@ -91,7 +69,6 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
|
|
||||||
let down_details_ref = NodeRef::<html::Details>::new();
|
let down_details_ref = NodeRef::<html::Details>::new();
|
||||||
let up_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>| {
|
let close_details = move |node_ref: NodeRef<html::Details>| {
|
||||||
if let Some(el) = node_ref.get_untracked() {
|
if let Some(el) = node_ref.get_untracked() {
|
||||||
@@ -201,49 +178,6 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div class="ml-auto flex items-center gap-4">
|
<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
|
<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"
|
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"
|
title="Settings & Notification Permissions"
|
||||||
|
|||||||
@@ -43,20 +43,12 @@ pub fn Toolbar() -> impl IntoView {
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<Input
|
<Input
|
||||||
input_type="text"
|
input_type="search"
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
value=MaybeProp::derive(move || Some(store.search_query.get()))
|
value=MaybeProp::derive(move || Some(store.search_query.get()))
|
||||||
on_change=Callback::new(move |val: String| store.search_query.set(val))
|
on_change=Callback::new(move |val: String| store.search_query.set(val))
|
||||||
class="pl-8 h-9"
|
class="pl-8 h-9"
|
||||||
/>
|
/>
|
||||||
<Show when=move || !store.search_query.get().is_empty()>
|
|
||||||
<button
|
|
||||||
class="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full text-xs font-medium hover:bg-muted h-5 w-5 opacity-50 hover:opacity-100 transition-opacity"
|
|
||||||
on:click=move |_| store.search_query.set(String::new())
|
|
||||||
>
|
|
||||||
"×"
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,30 +1,22 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use shared::NotificationLevel;
|
use shared::NotificationLevel;
|
||||||
|
use leptos_shadcn_alert::{Alert, AlertVariant};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Toast Components - Shadcn Style
|
// Toast Components - Using ShadCN Alert
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Returns the Shadcn class for the notification level
|
fn level_to_variant(level: &NotificationLevel) -> AlertVariant {
|
||||||
fn get_toast_class(level: &NotificationLevel) -> &'static str {
|
|
||||||
match level {
|
match level {
|
||||||
NotificationLevel::Info => "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-4 shadow-lg transition-all bg-background text-foreground border-border",
|
NotificationLevel::Info => AlertVariant::Default,
|
||||||
NotificationLevel::Success => "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-4 shadow-lg transition-all bg-background text-foreground border-primary/50 text-primary",
|
NotificationLevel::Success => AlertVariant::Success,
|
||||||
NotificationLevel::Warning => "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-4 shadow-lg transition-all bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-500 border-yellow-200 dark:border-yellow-900",
|
NotificationLevel::Warning => AlertVariant::Warning,
|
||||||
NotificationLevel::Error => "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-4 shadow-lg transition-all destructive group border-destructive bg-destructive text-destructive-foreground",
|
NotificationLevel::Error => AlertVariant::Destructive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Individual toast item component
|
fn level_icon(level: &NotificationLevel) -> impl IntoView {
|
||||||
#[component]
|
match level {
|
||||||
fn ToastItem(
|
|
||||||
level: NotificationLevel,
|
|
||||||
message: String,
|
|
||||||
) -> impl IntoView {
|
|
||||||
let toast_class = get_toast_class(&level);
|
|
||||||
|
|
||||||
// Icons
|
|
||||||
let icon_svg = match level {
|
|
||||||
NotificationLevel::Info => view! {
|
NotificationLevel::Info => view! {
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 opacity-90">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 opacity-90">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||||
@@ -45,15 +37,25 @@ fn ToastItem(
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||||
</svg>
|
</svg>
|
||||||
}.into_any(),
|
}.into_any(),
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Individual toast item component
|
||||||
|
#[component]
|
||||||
|
fn ToastItem(
|
||||||
|
level: NotificationLevel,
|
||||||
|
message: String,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let variant = level_to_variant(&level);
|
||||||
|
let icon = level_icon(&level);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class=toast_class>
|
<Alert variant=variant class="pointer-events-auto shadow-lg">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
{icon_svg}
|
{icon}
|
||||||
<div class="text-sm font-medium">{message}</div>
|
<div class="text-sm font-medium">{message}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::html;
|
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
use leptos_shadcn_input::Input;
|
||||||
|
use leptos_shadcn_button::{Button, ButtonVariant};
|
||||||
|
use leptos_shadcn_alert::{Alert, AlertDescription, AlertVariant};
|
||||||
use crate::store::TorrentStore;
|
use crate::store::TorrentStore;
|
||||||
use crate::api;
|
use crate::api;
|
||||||
|
|
||||||
@@ -11,17 +13,10 @@ pub fn AddTorrentDialog(
|
|||||||
let store = use_context::<TorrentStore>().expect("TorrentStore not provided");
|
let store = use_context::<TorrentStore>().expect("TorrentStore not provided");
|
||||||
let notifications = store.notifications;
|
let notifications = store.notifications;
|
||||||
|
|
||||||
let dialog_ref = NodeRef::<html::Dialog>::new();
|
|
||||||
let uri = signal(String::new());
|
let uri = signal(String::new());
|
||||||
let is_loading = signal(false);
|
let is_loading = signal(false);
|
||||||
let error_msg = signal(Option::<String>::None);
|
let error_msg = signal(Option::<String>::None);
|
||||||
|
|
||||||
Effect::new(move |_| {
|
|
||||||
if let Some(dialog) = dialog_ref.get() {
|
|
||||||
let _ = dialog.show_modal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let handle_submit = move |ev: web_sys::SubmitEvent| {
|
let handle_submit = move |ev: web_sys::SubmitEvent| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
let uri_val = uri.0.get();
|
let uri_val = uri.0.get();
|
||||||
@@ -44,9 +39,6 @@ pub fn AddTorrentDialog(
|
|||||||
shared::NotificationLevel::Success,
|
shared::NotificationLevel::Success,
|
||||||
"Torrent başarıyla eklendi"
|
"Torrent başarıyla eklendi"
|
||||||
);
|
);
|
||||||
if let Some(dialog) = dialog_ref.get() {
|
|
||||||
dialog.close();
|
|
||||||
}
|
|
||||||
on_close.run(());
|
on_close.run(());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -58,51 +50,76 @@ pub fn AddTorrentDialog(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let handle_cancel = move |_| {
|
let handle_backdrop = {
|
||||||
if let Some(dialog) = dialog_ref.get() {
|
let on_close = on_close.clone();
|
||||||
dialog.close();
|
move |e: web_sys::MouseEvent| {
|
||||||
|
e.stop_propagation();
|
||||||
|
on_close.run(());
|
||||||
}
|
}
|
||||||
on_close.run(());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<dialog node_ref=dialog_ref class="modal modal-bottom sm:modal-middle">
|
// Backdrop overlay
|
||||||
<div class="modal-box">
|
<div
|
||||||
<h3 class="font-bold text-lg">"Add Torrent"</h3>
|
class="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||||
<p class="py-4 text-sm opacity-70">"Enter a Magnet link or a .torrent file URL."</p>
|
on:click=handle_backdrop
|
||||||
|
/>
|
||||||
<form on:submit=handle_submit>
|
// Dialog panel
|
||||||
<div class="form-control w-full">
|
<div class="fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-card p-6 shadow-lg rounded-lg sm:max-w-[425px]">
|
||||||
<input
|
// Header
|
||||||
type="text"
|
<div class="flex flex-col space-y-1.5 text-center sm:text-left">
|
||||||
placeholder="magnet:?xt=urn:btih:..."
|
<h2 class="text-lg font-semibold leading-none tracking-tight">"Add Torrent"</h2>
|
||||||
class="input input-bordered w-full"
|
<p class="text-sm text-muted-foreground">"Enter a Magnet link or a .torrent file URL."</p>
|
||||||
prop:value=move || uri.0.get()
|
|
||||||
on:input=move |ev| uri.1.set(event_target_value(&ev))
|
|
||||||
disabled=move || is_loading.0.get()
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-action">
|
|
||||||
<button type="button" class="btn btn-ghost" on:click=handle_cancel>"Cancel"</button>
|
|
||||||
<button type="submit" class="btn btn-primary" disabled=move || is_loading.0.get()>
|
|
||||||
{move || if is_loading.0.get() {
|
|
||||||
leptos::either::Either::Left(view! { <span class="loading loading-spinner"></span> "Adding..." })
|
|
||||||
} else {
|
|
||||||
leptos::either::Either::Right(view! { "Add" })
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{move || error_msg.0.get().map(|msg| view! {
|
|
||||||
<div class="text-error text-sm mt-2">{msg}</div>
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button on:click=handle_cancel>"close"</button>
|
<form on:submit=handle_submit class="space-y-4">
|
||||||
|
<Input
|
||||||
|
input_type="text"
|
||||||
|
placeholder="magnet:?xt=urn:btih:..."
|
||||||
|
value=MaybeProp::derive(move || Some(uri.0.get()))
|
||||||
|
on_change=Callback::new(move |val: String| uri.1.set(val))
|
||||||
|
disabled=Signal::derive(move || is_loading.0.get())
|
||||||
|
/>
|
||||||
|
|
||||||
|
{move || error_msg.0.get().map(|msg| view! {
|
||||||
|
<Alert variant=AlertVariant::Destructive>
|
||||||
|
<AlertDescription>{msg}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
|
||||||
|
<Button
|
||||||
|
variant=ButtonVariant::Ghost
|
||||||
|
on_click=Callback::new(move |()| {
|
||||||
|
on_close.run(());
|
||||||
|
})
|
||||||
|
>
|
||||||
|
"Cancel"
|
||||||
|
</Button>
|
||||||
|
<Button disabled=Signal::derive(move || is_loading.0.get())>
|
||||||
|
{move || if is_loading.0.get() {
|
||||||
|
leptos::either::Either::Left(view! {
|
||||||
|
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
||||||
|
"Adding..."
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
leptos::either::Either::Right(view! { "Add" })
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
|
||||||
|
// Close button (X)
|
||||||
|
<button
|
||||||
|
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none"
|
||||||
|
on:click=move |_| on_close.run(())
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
|
||||||
|
<path d="M18 6 6 18"></path>
|
||||||
|
<path d="m6 6 12 12"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">"Close"</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::{Notification, NotificationOptions};
|
use web_sys::{Notification, NotificationOptions};
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos_use::{use_web_notification, UseWebNotificationReturn, NotificationPermission};
|
|
||||||
|
|
||||||
/// Request browser notification permission from user
|
/// Request browser notification permission from user
|
||||||
pub async fn request_notification_permission() -> bool {
|
pub async fn request_notification_permission() -> bool {
|
||||||
|
if !is_notification_supported() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if let Ok(promise) = Notification::request_permission() {
|
if let Ok(promise) = Notification::request_permission() {
|
||||||
if let Ok(result) = wasm_bindgen_futures::JsFuture::from(promise).await {
|
if let Ok(result) = wasm_bindgen_futures::JsFuture::from(promise).await {
|
||||||
return result.as_string().unwrap_or_default() == "granted";
|
return result.as_string().unwrap_or_default() == "granted";
|
||||||
@@ -21,6 +23,9 @@ pub fn is_notification_supported() -> bool {
|
|||||||
|
|
||||||
/// Get current notification permission status
|
/// Get current notification permission status
|
||||||
pub fn get_notification_permission() -> String {
|
pub fn get_notification_permission() -> String {
|
||||||
|
if !is_notification_supported() {
|
||||||
|
return "denied".to_string();
|
||||||
|
}
|
||||||
match Notification::permission() {
|
match Notification::permission() {
|
||||||
web_sys::NotificationPermission::Granted => "granted".to_string(),
|
web_sys::NotificationPermission::Granted => "granted".to_string(),
|
||||||
web_sys::NotificationPermission::Denied => "denied".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.
|
/// Hook for using browser notifications within Leptos components or effects.
|
||||||
/// This uses leptos-use for reactive permission tracking.
|
/// This uses leptos-use for reactive permission tracking.
|
||||||
pub fn use_app_notification() -> impl Fn(&str, &str) + Clone {
|
pub fn use_app_notification() -> impl Fn(&str, &str) + Clone {
|
||||||
let UseWebNotificationReturn { permission, .. } = use_web_notification();
|
|
||||||
|
|
||||||
move |title: &str, body: &str| {
|
move |title: &str, body: &str| {
|
||||||
// Check user preference from localStorage
|
// Check user preference from localStorage
|
||||||
let window = web_sys::window().expect("no global window");
|
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())
|
.and_then(|s| s.get_item("vibetorrent_browser_notifications").ok().flatten())
|
||||||
.unwrap_or_else(|| "true".to_string());
|
.unwrap_or_else(|| "true".to_string());
|
||||||
|
|
||||||
// Use the reactive permission signal from leptos-use
|
// Check platform support and permission
|
||||||
if enabled == "true" && permission.get() == NotificationPermission::Granted {
|
if enabled == "true" && is_notification_supported() && get_notification_permission() == "granted" {
|
||||||
show_browser_notification(title, body);
|
show_browser_notification(title, body);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,20 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const os = require("os");
|
||||||
|
|
||||||
|
// Cargo registry'deki leptos-shadcn crate'lerini Tailwind'e taratmak için
|
||||||
|
const cargoRegistry = path.join(
|
||||||
|
os.homedir(),
|
||||||
|
".cargo/registry/src/*/leptos-shadcn-*/src/**/*.rs"
|
||||||
|
);
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./index.html", "./src/**/*.{rs,html}"],
|
darkMode: "class",
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{rs,html}",
|
||||||
|
cargoRegistry,
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
|||||||
Reference in New Issue
Block a user