Compare commits

...

8 Commits

Author SHA1 Message Date
spinline
87ddd3bb93 fix: iOS Dark Mode ve Tema Değişimi Düzeltildi
All checks were successful
Build MIPS Binary / build (push) Successful in 5m17s
- tailwind.config.js: darkMode: 'class' olarak ayarlandı (iOS sistem tercihi yerine .dark class kullanımı için)
- statusbar.rs: Tema değişiminde .dark class ekleme mantığı tüm dark temaları kapsayacak şekilde genişletildi
- index.html: Sayfa yüklenirken .dark class ekleyen inline script güncellendi
- public/tailwind.css: PostCSS ile yeniden derlendi (nesting düzleştirildi + .dark seçiciler eklendi)
2026-02-11 01:00:04 +03:00
spinline
463249982c fix: iOS Safari uyumluluk - CSS nesting düzleştirildi
All checks were successful
Build MIPS Binary / build (push) Successful in 5m16s
- PostCSS tabanlı build'e geçildi (@tailwindcss/postcss + postcss-preset-env)
- CSS native nesting (&) düzleştirilerek eski Safari desteği sağlandı
- iOS 15+ ve Safari 15+ desteği eklendi
2026-02-11 00:54:44 +03:00
spinline
9447a66cc1 feat: loading ekranı shadcn Skeleton ile değiştirildi
All checks were successful
Build MIPS Binary / build (push) Successful in 5m16s
- Yükleniyor... spinner yerine uygulamanın layout'unu simüle eden skeleton UI
- Sidebar, header, tablo satırları ve statusbar skeleton'ları
2026-02-11 00:43:05 +03:00
spinline
45247a020e fix: AddTorrent dialog stili düzeltildi, skeleton crate eklendi
Some checks failed
Build MIPS Binary / build (push) Has been cancelled
- Dialog buggy leptos-shadcn-dialog yerine doğrudan shadcn HTML markup ile yeniden yazıldı
- Backdrop overlay, card panel, X close butonu eklendi
- leptos-shadcn-skeleton dependency eklendi
- Tailwind CSS rebuild edildi
2026-02-11 00:40:39 +03:00
spinline
77b77c7775 fix: Tailwind CSS rebuild - shadcn crate class'ları @source ile dahil edildi
All checks were successful
Build MIPS Binary / build (push) Successful in 5m15s
- input.css'e @source directive eklendi (cargo registry leptos-shadcn path'i)
- public/tailwind.css yeniden build edildi (1800 → 2940 satır)
- backdrop-blur, data-[state], focus-visible, peer-disabled vb. class'lar artık mevcut
2026-02-11 00:30:35 +03:00
spinline
8ef3008cb8 fix: context menu viewport sınır kontrolü - alta/sağa taşma düzeltildi
All checks were successful
Build MIPS Binary / build (push) Successful in 5m14s
2026-02-11 00:24:42 +03:00
spinline
ca1dd0caac refactor: tüm bileşenler leptos-shadcn-ui'ye dönüştürüldü
All checks were successful
Build MIPS Binary / build (push) Successful in 5m16s
- login.rs: Card, Input, Button, Label, Alert
- setup.rs: Card, Input, Button, Label, Alert
- add_torrent.rs: Dialog, Input, Button, Alert
- toast.rs: Alert bileşeni ile
- Cargo.toml: dialog, label, alert, toast, dropdown-menu, tooltip eklendi
2026-02-11 00:17:22 +03:00
spinline
ad336789d9 fix: custom × butonu kaldırıldı, native search clear kullanılıyor
All checks were successful
Build MIPS Binary / build (push) Successful in 5m14s
2026-02-11 00:11:28 +03:00
18 changed files with 7032 additions and 1893 deletions

114
Cargo.lock generated
View File

@@ -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
View File

@@ -0,0 +1 @@
node_modules/

View File

@@ -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"

View File

@@ -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>

View File

@@ -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;
} }

File diff suppressed because it is too large Load Diff

View File

@@ -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": {

View 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

View File

@@ -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()>

View File

@@ -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>
} }
} }

View File

@@ -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>
} }
} }

View File

@@ -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ı
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

View File

@@ -44,7 +44,7 @@ pub fn StatusBar() -> impl IntoView {
if let Some(doc) = document().document_element() { if let Some(doc) = document().document_element() {
let _ = doc.set_attribute("data-theme", &theme); let _ = doc.set_attribute("data-theme", &theme);
// Also set class for Shadcn dark mode support // Also set class for Shadcn dark mode support
if theme == "dark" || theme == "dracula" || theme == "dim" || theme == "abyss" { 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"); let _ = doc.class_list().add_1("dark");
} else { } else {
let _ = doc.class_list().remove_1("dark"); let _ = doc.class_list().remove_1("dark");

View File

@@ -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>

View File

@@ -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>
} }
} }

View File

@@ -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>
} }
} }

View File

@@ -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: {