feat: Mobile PWA improvements
- Implemented responsive card layout for mobile devices - Fixed Android bottom navigation bar color matching - Improved mobile context menu (iOS/Android touch fixes) - Cleaned up toolbar and updated theme icon
This commit is contained in:
@@ -26,10 +26,48 @@
|
|||||||
if (t === "Light") t = "light";
|
if (t === "Light") t = "light";
|
||||||
if (t === "Dark" || t === "Midnight") t = "dark";
|
if (t === "Dark" || t === "Midnight") t = "dark";
|
||||||
|
|
||||||
document.documentElement.setAttribute("data-theme", t.toLowerCase());
|
var theme = t.toLowerCase();
|
||||||
|
document.documentElement.setAttribute("data-theme", theme);
|
||||||
if (!localTheme) {
|
if (!localTheme) {
|
||||||
localStorage.setItem("vibetorrent_theme", "Dark");
|
localStorage.setItem("vibetorrent_theme", "Dark");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var meta = document.querySelector('meta[name="theme-color"]');
|
||||||
|
if (meta) {
|
||||||
|
var colorMap = {
|
||||||
|
"light": "#ffffff",
|
||||||
|
"cupcake": "#faf7f5",
|
||||||
|
"bumblebee": "#ffffff",
|
||||||
|
"emerald": "#ffffff",
|
||||||
|
"corporate": "#ffffff",
|
||||||
|
"synthwave": "#2d1b69",
|
||||||
|
"retro": "#ece3ca",
|
||||||
|
"cyberpunk": "#ffee00",
|
||||||
|
"valentine": "#f0d6e8",
|
||||||
|
"halloween": "#212121",
|
||||||
|
"garden": "#e9e7e7",
|
||||||
|
"forest": "#171212",
|
||||||
|
"aqua": "#345da7",
|
||||||
|
"lofi": "#ffffff",
|
||||||
|
"pastel": "#ffffff",
|
||||||
|
"fantasy": "#ffffff",
|
||||||
|
"wireframe": "#ffffff",
|
||||||
|
"black": "#000000",
|
||||||
|
"luxury": "#09090b",
|
||||||
|
"dracula": "#282a36",
|
||||||
|
"cmyk": "#ffffff",
|
||||||
|
"autumn": "#8C0327",
|
||||||
|
"business": "#202020",
|
||||||
|
"acid": "#fafafa",
|
||||||
|
"lemonade": "#F1F8E8",
|
||||||
|
"night": "#0f1729",
|
||||||
|
"coffee": "#20161f",
|
||||||
|
"winter": "#ffffff",
|
||||||
|
"dark": "#1d232a"
|
||||||
|
};
|
||||||
|
var color = colorMap[theme] || "#1d232a";
|
||||||
|
meta.setAttribute("content", color);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
"short_name": "VibeTorrent",
|
"short_name": "VibeTorrent",
|
||||||
"start_url": "/",
|
"start_url": "/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#111827",
|
"background_color": "#1d232a",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#1d232a",
|
||||||
"orientation": "any",
|
"orientation": "any",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1211,6 +1211,9 @@
|
|||||||
.z-\[1\] {
|
.z-\[1\] {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
.z-\[99\] {
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
.z-\[100\] {
|
.z-\[100\] {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
@@ -1673,6 +1676,9 @@
|
|||||||
.transform {
|
.transform {
|
||||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||||
}
|
}
|
||||||
|
.cursor-default {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -1685,6 +1691,9 @@
|
|||||||
.flex-col {
|
.flex-col {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
.content-start {
|
||||||
|
align-content: flex-start;
|
||||||
|
}
|
||||||
.items-center {
|
.items-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +1,3 @@
|
|||||||
use leptos::*;
|
|
||||||
use leptos::html::Div;
|
|
||||||
use wasm_bindgen::JsCast;
|
|
||||||
|
|
||||||
pub fn use_click_outside(
|
|
||||||
target: NodeRef<Div>,
|
|
||||||
callback: impl Fn() + Clone + 'static,
|
|
||||||
) {
|
|
||||||
create_effect(move |_| {
|
|
||||||
if let Some(_) = target.get() {
|
|
||||||
let handle_click = {
|
|
||||||
let callback = callback.clone();
|
|
||||||
let target = target.clone();
|
|
||||||
move |ev: web_sys::MouseEvent| {
|
|
||||||
if let Some(el) = target.get() {
|
|
||||||
let ev_target = ev.target().unwrap().unchecked_into::<web_sys::Node>();
|
|
||||||
let el_node = el.unchecked_ref::<web_sys::Node>();
|
|
||||||
if !el_node.contains(Some(&ev_target)) {
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
let closure = wasm_bindgen::closure::Closure::<dyn FnMut(_)>::new(handle_click);
|
|
||||||
let _ = window.add_event_listener_with_callback("click", closure.as_ref().unchecked_ref());
|
|
||||||
|
|
||||||
// Cleanup
|
|
||||||
on_cleanup(move || {
|
|
||||||
let window = web_sys::window().unwrap();
|
|
||||||
let _ = window.remove_event_listener_with_callback("click", closure.as_ref().unchecked_ref());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ContextMenu(
|
pub fn ContextMenu(
|
||||||
position: (i32, i32),
|
position: (i32, i32),
|
||||||
@@ -53,21 +15,19 @@ pub fn ContextMenu(
|
|||||||
on_close.call(()); // Close menu AFTER
|
on_close.call(()); // Close menu AFTER
|
||||||
};
|
};
|
||||||
|
|
||||||
let target = create_node_ref::<Div>();
|
|
||||||
|
|
||||||
use_click_outside(target, move || {
|
|
||||||
if visible {
|
|
||||||
on_close.call(());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if !visible {
|
if !visible {
|
||||||
return view! {}.into_view();
|
return view! {}.into_view();
|
||||||
}
|
}
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
// Backdrop to catch clicks outside
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-[99] cursor-default"
|
||||||
|
on:click=move |_| on_close.call(())
|
||||||
|
on:contextmenu=move |e| e.prevent_default()
|
||||||
|
></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
node_ref=target
|
|
||||||
class="fixed z-[100] min-w-[200px] animate-in fade-in zoom-in-95 duration-100"
|
class="fixed z-[100] min-w-[200px] animate-in fade-in zoom-in-95 duration-100"
|
||||||
style=format!("left: {}px; top: {}px", position.0, position.1)
|
style=format!("left: {}px; top: {}px", position.0, position.1)
|
||||||
on:contextmenu=move |e| e.prevent_default()
|
on:contextmenu=move |e| e.prevent_default()
|
||||||
|
|||||||
@@ -40,6 +40,18 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
on:click=move |_| {
|
on:click=move |_| {
|
||||||
let doc = web_sys::window().unwrap().document().unwrap();
|
let doc = web_sys::window().unwrap().document().unwrap();
|
||||||
let _ = doc.document_element().unwrap().set_attribute("data-theme", theme);
|
let _ = doc.document_element().unwrap().set_attribute("data-theme", theme);
|
||||||
|
|
||||||
|
// Update theme-color meta tag to match new theme
|
||||||
|
if let Some(meta) = doc.query_selector("meta[name='theme-color']").unwrap() {
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
// Force a style recalc by reading a property or just wait for next tick?
|
||||||
|
// Usually get_computed_style forces it.
|
||||||
|
if let Ok(Some(style)) = window.get_computed_style(&doc.body().unwrap()) {
|
||||||
|
if let Ok(color) = style.get_property_value("background-color") {
|
||||||
|
let _ = meta.set_attribute("content", &color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{theme}
|
{theme}
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="md:hidden grid grid-cols-1 gap-3 p-3 pb-20 overflow-y-auto h-full">
|
<div class="md:hidden grid grid-cols-1 content-start gap-3 p-3 pb-20 overflow-y-auto h-full">
|
||||||
{move || filtered_torrents().into_iter().map(|t| {
|
{move || filtered_torrents().into_iter().map(|t| {
|
||||||
let progress_class = if t.percent_complete >= 100.0 { "progress-success" } else { "progress-primary" };
|
let progress_class = if t.percent_complete >= 100.0 { "progress-success" } else { "progress-primary" };
|
||||||
let status_str = format!("{:?}", t.status);
|
let status_str = format!("{:?}", t.status);
|
||||||
@@ -271,11 +271,12 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
class=move || {
|
class=move || {
|
||||||
let base = "card card-compact bg-base-100 shadow-sm border border-base-200 transition-transform active:scale-[0.99]";
|
let base = "card card-compact bg-base-100 shadow-sm border border-base-200 transition-transform active:scale-[0.99]";
|
||||||
if is_selected_fn() {
|
if is_selected_fn() {
|
||||||
format!("{} ring-2 ring-primary", base)
|
format!("{} ring-2 ring-primary select-none", base)
|
||||||
} else {
|
} else {
|
||||||
base.to_string()
|
format!("{} select-none", base)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
style="user-select: none; -webkit-user-select: none; -webkit-touch-callout: none;"
|
||||||
on:contextmenu={
|
on:contextmenu={
|
||||||
let t_hash = t_hash_ctx.clone();
|
let t_hash = t_hash_ctx.clone();
|
||||||
move |e: web_sys::MouseEvent| handle_context_menu(e, t_hash.clone())
|
move |e: web_sys::MouseEvent| handle_context_menu(e, t_hash.clone())
|
||||||
|
|||||||
Reference in New Issue
Block a user