Compare commits
12 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47dc4da6d1 | ||
|
|
c501ed9207 | ||
|
|
4861faee18 | ||
|
|
6a4943d692 | ||
|
|
b27caa77f2 | ||
|
|
cba8c20d9b | ||
|
|
0cdd92dc95 | ||
|
|
b9798ce0e2 | ||
|
|
6a882b75b6 | ||
|
|
40c9f66e5c | ||
|
|
93e853977a | ||
|
|
e3bc956256 |
@@ -7,7 +7,7 @@ edition = "2021"
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
leptos = { version = "0.8.15", features = ["csr"] }
|
leptos = { version = "0.8.15", features = ["csr", "msgpack"] }
|
||||||
leptos_router = { version = "0.8.11" }
|
leptos_router = { version = "0.8.11" }
|
||||||
|
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
|
|||||||
1159
frontend/package-lock.json
generated
1159
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,7 @@
|
|||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"postcss-cli": "^11.0.1",
|
"postcss-cli": "^11.0.1",
|
||||||
"postcss-preset-env": "^11.1.3",
|
"postcss-preset-env": "^10.1.3",
|
||||||
"tailwindcss": "^4.1.18"
|
"tailwindcss": "^4.1.18"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::components::layout::protected::Protected;
|
use crate::components::layout::protected::Protected;
|
||||||
use crate::components::toast::ToastContainer;
|
|
||||||
use crate::components::torrent::table::TorrentTable;
|
use crate::components::torrent::table::TorrentTable;
|
||||||
use crate::components::torrent::detail::TorrentDetail;
|
use crate::components::torrent::detail::TorrentDetail;
|
||||||
use crate::components::auth::login::Login;
|
use crate::components::auth::login::Login;
|
||||||
@@ -9,9 +8,19 @@ 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;
|
use leptos_shadcn_skeleton::Skeleton;
|
||||||
|
use leptos_shadcn_toast::SonnerProvider;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<SonnerProvider>
|
||||||
|
<InnerApp />
|
||||||
|
</SonnerProvider>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn InnerApp() -> impl IntoView {
|
||||||
crate::store::provide_torrent_store();
|
crate::store::provide_torrent_store();
|
||||||
let store = use_context::<crate::store::TorrentStore>();
|
let store = use_context::<crate::store::TorrentStore>();
|
||||||
|
|
||||||
@@ -188,8 +197,6 @@ pub fn App() -> impl IntoView {
|
|||||||
}/>
|
}/>
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
|
|
||||||
<ToastContainer />
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ 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 leptos_use::storage::use_local_storage;
|
||||||
use ::codee::string::FromToStringCodec;
|
use ::codee::string::FromToStringCodec;
|
||||||
|
|
||||||
@@ -93,11 +93,11 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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() {
|
let toggle_theme = move |_| {
|
||||||
el.set_open(false);
|
let new_theme = if current_theme.get() == "dark" { "light" } else { "dark" };
|
||||||
}
|
set_current_theme.set(new_theme.to_string());
|
||||||
};
|
};
|
||||||
// --- THEME LOGIC END ---
|
// --- THEME LOGIC END ---
|
||||||
|
|
||||||
@@ -192,7 +192,7 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
<div class="p-4 bg-card">
|
<div class="p-4 bg-card" style="padding-bottom: calc(1rem + env(safe-area-inset-bottom));">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<Avatar class="h-8 w-8">
|
<Avatar class="h-8 w-8">
|
||||||
<AvatarFallback class="bg-primary text-primary-foreground text-xs font-medium">
|
<AvatarFallback class="bg-primary text-primary-foreground text-xs font-medium">
|
||||||
@@ -205,50 +205,25 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
// --- THEME BUTTON ---
|
// --- THEME BUTTON ---
|
||||||
<details class="group relative" node_ref=theme_details_ref>
|
<Button
|
||||||
<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">
|
variant=ButtonVariant::Ghost
|
||||||
<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">
|
size=ButtonSize::Icon
|
||||||
<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" />
|
class="h-8 w-8 text-muted-foreground hover:text-foreground"
|
||||||
|
on_click=Callback::new(toggle_theme)
|
||||||
|
>
|
||||||
|
// Sun icon for dark mode (to switch to light), Moon for light (to switch to dark)
|
||||||
|
// Actually show current state or action? Usually action.
|
||||||
|
// If dark, show Sun. If light, show Moon.
|
||||||
|
<Show when=move || current_theme.get() == "dark" fallback=|| view! {
|
||||||
|
<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="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z" />
|
||||||
</svg>
|
</svg>
|
||||||
</summary>
|
}>
|
||||||
|
<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">
|
||||||
<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">
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z" />
|
||||||
<ul class="w-full">
|
</svg>
|
||||||
{
|
</Show>
|
||||||
let themes = vec![
|
</Button>
|
||||||
"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,5 +1,4 @@
|
|||||||
pub mod context_menu;
|
pub mod context_menu;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
pub mod toast;
|
|
||||||
pub mod torrent;
|
pub mod torrent;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
use leptos::prelude::*;
|
|
||||||
use shared::NotificationLevel;
|
|
||||||
use leptos_shadcn_alert::{Alert, AlertVariant};
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Toast Components - Using ShadCN Alert
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
fn level_to_variant(level: &NotificationLevel) -> AlertVariant {
|
|
||||||
match level {
|
|
||||||
NotificationLevel::Info => AlertVariant::Default,
|
|
||||||
NotificationLevel::Success => AlertVariant::Success,
|
|
||||||
NotificationLevel::Warning => AlertVariant::Warning,
|
|
||||||
NotificationLevel::Error => AlertVariant::Destructive,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn level_icon(level: &NotificationLevel) -> impl IntoView {
|
|
||||||
match level {
|
|
||||||
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">
|
|
||||||
<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" />
|
|
||||||
</svg>
|
|
||||||
}.into_any(),
|
|
||||||
NotificationLevel::Success => 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">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
||||||
</svg>
|
|
||||||
}.into_any(),
|
|
||||||
NotificationLevel::Warning => 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">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
|
||||||
</svg>
|
|
||||||
}.into_any(),
|
|
||||||
NotificationLevel::Error => 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">
|
|
||||||
<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>
|
|
||||||
}.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! {
|
|
||||||
<Alert variant=variant class="pointer-events-auto shadow-lg">
|
|
||||||
<div class="flex items-center gap-3">
|
|
||||||
{icon}
|
|
||||||
<div class="text-sm font-medium">{message}</div>
|
|
||||||
</div>
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Main toast container - renders all active notifications
|
|
||||||
#[component]
|
|
||||||
pub fn ToastContainer() -> impl IntoView {
|
|
||||||
let store = use_context::<crate::store::TorrentStore>().expect("TorrentStore not provided");
|
|
||||||
let notifications = store.notifications;
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div
|
|
||||||
class="fixed bottom-0 right-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px] gap-2"
|
|
||||||
>
|
|
||||||
<For
|
|
||||||
each=move || notifications.get()
|
|
||||||
key=|item| item.id
|
|
||||||
children=move |item| {
|
|
||||||
view! {
|
|
||||||
<ToastItem
|
|
||||||
level=item.notification.level
|
|
||||||
message=item.notification.message
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ pub fn AddTorrentDialog(
|
|||||||
on_close: Callback<()>,
|
on_close: Callback<()>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let store = use_context::<TorrentStore>().expect("TorrentStore not provided");
|
let store = use_context::<TorrentStore>().expect("TorrentStore not provided");
|
||||||
let notifications = store.notifications;
|
|
||||||
|
|
||||||
let uri = signal(String::new());
|
let uri = signal(String::new());
|
||||||
let is_loading = signal(false);
|
let is_loading = signal(false);
|
||||||
@@ -34,11 +33,7 @@ pub fn AddTorrentDialog(
|
|||||||
match api::torrent::add(&uri_val).await {
|
match api::torrent::add(&uri_val).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
log::info!("Torrent added successfully");
|
log::info!("Torrent added successfully");
|
||||||
crate::store::show_toast_with_signal(
|
crate::store::toast_success("Torrent başarıyla eklendi");
|
||||||
notifications,
|
|
||||||
shared::NotificationLevel::Success,
|
|
||||||
"Torrent başarıyla eklendi"
|
|
||||||
);
|
|
||||||
on_close.run(());
|
on_close.run(());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
use crate::store::{get_action_messages, show_toast_with_signal};
|
use crate::store::{get_action_messages, show_toast};
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use shared::NotificationLevel;
|
use shared::NotificationLevel;
|
||||||
use crate::components::context_menu::TorrentContextMenu;
|
use crate::components::context_menu::TorrentContextMenu;
|
||||||
@@ -116,7 +116,6 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
let (success_msg_str, error_msg_str): (&'static str, &'static str) = get_action_messages(&action);
|
let (success_msg_str, error_msg_str): (&'static str, &'static str) = get_action_messages(&action);
|
||||||
let success_msg = success_msg_str.to_string();
|
let success_msg = success_msg_str.to_string();
|
||||||
let error_msg = error_msg_str.to_string();
|
let error_msg = error_msg_str.to_string();
|
||||||
let notifications = store.notifications;
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let result = match action.as_str() {
|
let result = match action.as_str() {
|
||||||
"delete" => api::torrent::delete(&hash).await,
|
"delete" => api::torrent::delete(&hash).await,
|
||||||
@@ -126,8 +125,8 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
_ => api::torrent::action(&hash, &action).await,
|
_ => api::torrent::action(&hash, &action).await,
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => show_toast_with_signal(notifications, NotificationLevel::Success, success_msg),
|
Ok(_) => show_toast(NotificationLevel::Success, success_msg),
|
||||||
Err(e) => show_toast_with_signal(notifications, NotificationLevel::Error, format!("{}: {:?}", error_msg, e)),
|
Err(e) => show_toast(NotificationLevel::Error, format!("{}: {:?}", error_msg, e)),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,40 +7,18 @@ use std::collections::HashMap;
|
|||||||
use struct_patch::traits::Patch;
|
use struct_patch::traits::Patch;
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
|
||||||
pub struct NotificationItem {
|
|
||||||
pub id: u64,
|
|
||||||
pub notification: SystemNotification,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show_toast_with_signal(
|
|
||||||
notifications: RwSignal<Vec<NotificationItem>>,
|
|
||||||
level: NotificationLevel,
|
|
||||||
message: impl Into<String>,
|
|
||||||
) {
|
|
||||||
let id = js_sys::Date::now() as u64;
|
|
||||||
let notification = SystemNotification {
|
|
||||||
level,
|
|
||||||
message: message.into(),
|
|
||||||
};
|
|
||||||
let item = NotificationItem { id, notification };
|
|
||||||
|
|
||||||
notifications.update(|list| list.push(item));
|
|
||||||
|
|
||||||
leptos::prelude::set_timeout(
|
|
||||||
move || {
|
|
||||||
notifications.update(|list| list.retain(|i| i.id != id));
|
|
||||||
},
|
|
||||||
std::time::Duration::from_secs(5),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn show_toast(level: NotificationLevel, message: impl Into<String>) {
|
pub fn show_toast(level: NotificationLevel, message: impl Into<String>) {
|
||||||
if let Some(store) = use_context::<TorrentStore>() {
|
let msg = message.into();
|
||||||
show_toast_with_signal(store.notifications, level, message);
|
match level {
|
||||||
|
NotificationLevel::Info => { leptos_shadcn_toast::toast::info(&msg).show(); },
|
||||||
|
NotificationLevel::Success => { leptos_shadcn_toast::toast::success(&msg).show(); },
|
||||||
|
NotificationLevel::Warning => { leptos_shadcn_toast::toast::warning(&msg).show(); },
|
||||||
|
NotificationLevel::Error => { leptos_shadcn_toast::toast::error(&msg).show(); },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub fn toast_success(message: impl Into<String>) { show_toast(NotificationLevel::Success, message); }
|
pub fn toast_success(message: impl Into<String>) { show_toast(NotificationLevel::Success, message); }
|
||||||
pub fn toast_error(message: impl Into<String>) { show_toast(NotificationLevel::Error, message); }
|
pub fn toast_error(message: impl Into<String>) { show_toast(NotificationLevel::Error, message); }
|
||||||
|
|
||||||
@@ -67,7 +45,6 @@ pub struct TorrentStore {
|
|||||||
pub filter: RwSignal<FilterStatus>,
|
pub filter: RwSignal<FilterStatus>,
|
||||||
pub search_query: RwSignal<String>,
|
pub search_query: RwSignal<String>,
|
||||||
pub global_stats: RwSignal<GlobalStats>,
|
pub global_stats: RwSignal<GlobalStats>,
|
||||||
pub notifications: RwSignal<Vec<NotificationItem>>,
|
|
||||||
pub user: RwSignal<Option<String>>,
|
pub user: RwSignal<Option<String>>,
|
||||||
pub selected_torrent: RwSignal<Option<String>>,
|
pub selected_torrent: RwSignal<Option<String>>,
|
||||||
}
|
}
|
||||||
@@ -77,16 +54,14 @@ pub fn provide_torrent_store() {
|
|||||||
let filter = RwSignal::new(FilterStatus::All);
|
let filter = RwSignal::new(FilterStatus::All);
|
||||||
let search_query = RwSignal::new(String::new());
|
let search_query = RwSignal::new(String::new());
|
||||||
let global_stats = RwSignal::new(GlobalStats::default());
|
let global_stats = RwSignal::new(GlobalStats::default());
|
||||||
let notifications = RwSignal::new(Vec::<NotificationItem>::new());
|
|
||||||
let user = RwSignal::new(Option::<String>::None);
|
let user = RwSignal::new(Option::<String>::None);
|
||||||
let selected_torrent = RwSignal::new(Option::<String>::None);
|
let selected_torrent = RwSignal::new(Option::<String>::None);
|
||||||
|
|
||||||
let show_browser_notification = crate::utils::notification::use_app_notification();
|
let show_browser_notification = crate::utils::notification::use_app_notification();
|
||||||
|
|
||||||
let store = TorrentStore { torrents, filter, search_query, global_stats, notifications, user, selected_torrent };
|
let store = TorrentStore { torrents, filter, search_query, global_stats, user, selected_torrent };
|
||||||
provide_context(store);
|
provide_context(store);
|
||||||
|
|
||||||
let notifications_for_sse = notifications;
|
|
||||||
let global_stats_for_sse = global_stats;
|
let global_stats_for_sse = global_stats;
|
||||||
let torrents_for_sse = torrents;
|
let torrents_for_sse = torrents;
|
||||||
let show_browser_notification = show_browser_notification.clone();
|
let show_browser_notification = show_browser_notification.clone();
|
||||||
@@ -112,7 +87,7 @@ pub fn provide_torrent_store() {
|
|||||||
got_first_message = true;
|
got_first_message = true;
|
||||||
backoff_ms = 1000;
|
backoff_ms = 1000;
|
||||||
if was_connected && disconnect_notified {
|
if was_connected && disconnect_notified {
|
||||||
show_toast_with_signal(notifications_for_sse, NotificationLevel::Success, "Sunucu bağlantısı yeniden kuruldu");
|
show_toast(NotificationLevel::Success, "Sunucu bağlantısı yeniden kuruldu");
|
||||||
disconnect_notified = false;
|
disconnect_notified = false;
|
||||||
}
|
}
|
||||||
was_connected = true;
|
was_connected = true;
|
||||||
@@ -149,7 +124,7 @@ pub fn provide_torrent_store() {
|
|||||||
}
|
}
|
||||||
AppEvent::Stats(stats) => { global_stats_for_sse.set(stats); }
|
AppEvent::Stats(stats) => { global_stats_for_sse.set(stats); }
|
||||||
AppEvent::Notification(n) => {
|
AppEvent::Notification(n) => {
|
||||||
show_toast_with_signal(notifications_for_sse, n.level.clone(), n.message.clone());
|
show_toast(n.level.clone(), n.message.clone());
|
||||||
if n.message.contains("tamamlandı") || n.level == shared::NotificationLevel::Error {
|
if n.message.contains("tamamlandı") || n.level == shared::NotificationLevel::Error {
|
||||||
show_browser_notification("VibeTorrent", &n.message);
|
show_browser_notification("VibeTorrent", &n.message);
|
||||||
}
|
}
|
||||||
@@ -164,14 +139,14 @@ pub fn provide_torrent_store() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if was_connected && !disconnect_notified {
|
if was_connected && !disconnect_notified {
|
||||||
show_toast_with_signal(notifications_for_sse, NotificationLevel::Warning, "Sunucu bağlantısı kesildi, yeniden bağlanılıyor...");
|
show_toast(NotificationLevel::Warning, "Sunucu bağlantısı kesildi, yeniden bağlanılıyor...");
|
||||||
disconnect_notified = true;
|
disconnect_notified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
if was_connected && !disconnect_notified {
|
if was_connected && !disconnect_notified {
|
||||||
show_toast_with_signal(notifications_for_sse, NotificationLevel::Warning, "Sunucu bağlantısı kurulamıyor...");
|
show_toast(NotificationLevel::Warning, "Sunucu bağlantısı kurulamıyor...");
|
||||||
disconnect_notified = true;
|
disconnect_notified = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||||
struct-patch = "0.5"
|
struct-patch = "0.5"
|
||||||
rmp-serde = "1.3"
|
rmp-serde = "1.3"
|
||||||
|
bytes = "1"
|
||||||
|
http = "1"
|
||||||
|
|
||||||
# Leptos 0.8.7
|
# Leptos 0.8.7
|
||||||
leptos = { version = "0.8.7", features = ["nightly"] }
|
leptos = { version = "0.8.15", features = ["nightly", "msgpack"] }
|
||||||
leptos_router = { version = "0.8.7", features = ["nightly"] }
|
leptos_router = { version = "0.8.7", features = ["nightly"] }
|
||||||
leptos_axum = { version = "0.8.7", optional = true }
|
leptos_axum = { version = "0.8.7", optional = true }
|
||||||
axum = { version = "0.8", features = ["macros"], optional = true }
|
axum = { version = "0.8", features = ["macros"], optional = true }
|
||||||
|
|
||||||
# SSR Dependencies (XML-RPC & SCGI)
|
# SSR Dependencies (XML-RPC & SCGI)
|
||||||
tokio = { version = "1", features = ["full"], optional = true }
|
tokio = { version = "1", features = ["full"], optional = true }
|
||||||
bytes = { version = "1", optional = true }
|
|
||||||
thiserror = { version = "2", optional = true }
|
thiserror = { version = "2", optional = true }
|
||||||
quick-xml = { version = "0.31", features = ["serde", "serialize"], optional = true }
|
quick-xml = { version = "0.31", features = ["serde", "serialize"], optional = true }
|
||||||
|
|
||||||
@@ -34,7 +35,6 @@ bcrypt = { version = "0.17", optional = true }
|
|||||||
default = []
|
default = []
|
||||||
ssr = [
|
ssr = [
|
||||||
"dep:tokio",
|
"dep:tokio",
|
||||||
"dep:bytes",
|
|
||||||
"dep:thiserror",
|
"dep:thiserror",
|
||||||
"dep:quick-xml",
|
"dep:quick-xml",
|
||||||
"dep:leptos_axum",
|
"dep:leptos_axum",
|
||||||
|
|||||||
1
shared/src/codec.rs
Normal file
1
shared/src/codec.rs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pub use leptos::server_fn::codec::MsgPack;
|
||||||
@@ -11,6 +11,8 @@ pub mod xmlrpc;
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
pub mod db;
|
pub mod db;
|
||||||
|
|
||||||
|
pub mod codec;
|
||||||
|
|
||||||
pub mod server_fns;
|
pub mod server_fns;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use crate::codec::MsgPack;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct UserResponse {
|
pub struct UserResponse {
|
||||||
@@ -19,7 +20,7 @@ pub struct SetupStatus {
|
|||||||
pub completed: bool,
|
pub completed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(GetSetupStatus, "/api/server_fns/GetSetupStatus")]
|
#[server(GetSetupStatus, "/api/server_fns/GetSetupStatus", input = MsgPack, output = MsgPack)]
|
||||||
pub async fn get_setup_status() -> Result<SetupStatus, ServerFnError> {
|
pub async fn get_setup_status() -> Result<SetupStatus, ServerFnError> {
|
||||||
use crate::DbContext;
|
use crate::DbContext;
|
||||||
|
|
||||||
@@ -32,7 +33,7 @@ pub async fn get_setup_status() -> Result<SetupStatus, ServerFnError> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(Setup, "/api/server_fns/Setup")]
|
#[server(Setup, "/api/server_fns/Setup", input = MsgPack, output = MsgPack)]
|
||||||
pub async fn setup(username: String, password: String) -> Result<(), ServerFnError> {
|
pub async fn setup(username: String, password: String) -> Result<(), ServerFnError> {
|
||||||
use crate::DbContext;
|
use crate::DbContext;
|
||||||
|
|
||||||
@@ -54,7 +55,7 @@ pub async fn setup(username: String, password: String) -> Result<(), ServerFnErr
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(Login, "/api/server_fns/Login")]
|
#[server(Login, "/api/server_fns/Login", input = MsgPack, output = MsgPack)]
|
||||||
pub async fn login(username: String, password: String) -> Result<UserResponse, ServerFnError> {
|
pub async fn login(username: String, password: String) -> Result<UserResponse, ServerFnError> {
|
||||||
use crate::DbContext;
|
use crate::DbContext;
|
||||||
use leptos_axum::ResponseOptions;
|
use leptos_axum::ResponseOptions;
|
||||||
@@ -110,7 +111,7 @@ pub async fn login(username: String, password: String) -> Result<UserResponse, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(Logout, "/api/server_fns/Logout")]
|
#[server(Logout, "/api/server_fns/Logout", input = MsgPack, output = MsgPack)]
|
||||||
pub async fn logout() -> Result<(), ServerFnError> {
|
pub async fn logout() -> Result<(), ServerFnError> {
|
||||||
use leptos_axum::ResponseOptions;
|
use leptos_axum::ResponseOptions;
|
||||||
use cookie::{Cookie, SameSite};
|
use cookie::{Cookie, SameSite};
|
||||||
@@ -131,7 +132,7 @@ pub async fn logout() -> Result<(), ServerFnError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(GetUser, "/api/server_fns/GetUser")]
|
#[server(GetUser, "/api/server_fns/GetUser", input = MsgPack, output = MsgPack)]
|
||||||
pub async fn get_user() -> Result<Option<UserResponse>, ServerFnError> {
|
pub async fn get_user() -> Result<Option<UserResponse>, ServerFnError> {
|
||||||
use axum::http::HeaderMap;
|
use axum::http::HeaderMap;
|
||||||
use leptos_axum::extract;
|
use leptos_axum::extract;
|
||||||
|
|||||||
Reference in New Issue
Block a user