feat: Implement PWA support, responsive drawer layout, and custom context menu
This commit is contained in:
59
Cargo.lock
generated
59
Cargo.lock
generated
@@ -685,6 +685,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"gloo-net 0.5.0",
|
"gloo-net 0.5.0",
|
||||||
"leptos",
|
"leptos",
|
||||||
|
"leptos_router",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@@ -1340,6 +1341,32 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos_router"
|
||||||
|
version = "0.6.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d71dea7d42c0d29c40842750232d3425ed1cf10e313a1f898076d20871dad32"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"gloo-net 0.6.0",
|
||||||
|
"itertools",
|
||||||
|
"js-sys",
|
||||||
|
"lazy_static",
|
||||||
|
"leptos",
|
||||||
|
"linear-map",
|
||||||
|
"once_cell",
|
||||||
|
"percent-encoding",
|
||||||
|
"send_wrapper",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_qs 0.13.0",
|
||||||
|
"thiserror",
|
||||||
|
"tracing",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos_server"
|
name = "leptos_server"
|
||||||
version = "0.6.15"
|
version = "0.6.15"
|
||||||
@@ -1362,6 +1389,16 @@ version = "0.2.180"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linear-map"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bfae20f6b19ad527b550c223fddc3077a547fc70cda94b9b566575423fd303ee"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_test",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2008,6 +2045,17 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_qs"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd34f36fe4c5ba9654417139a9b3a20d2e1de6012ee678ad14d240c22c78d8d6"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "0.6.9"
|
version = "0.6.9"
|
||||||
@@ -2017,6 +2065,15 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_test"
|
||||||
|
version = "1.0.177"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7f901ee573cab6b3060453d2d5f0bae4e6d628c23c0a962ff9b5f1d7c8d4f1ed"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_urlencoded"
|
name = "serde_urlencoded"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
@@ -2047,7 +2104,7 @@ dependencies = [
|
|||||||
"send_wrapper",
|
"send_wrapper",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_qs",
|
"serde_qs 0.12.0",
|
||||||
"server_fn_macro_default",
|
"server_fn_macro_default",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"url",
|
"url",
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ crate-type = ["cdylib", "rlib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
leptos = { version = "0.6", features = ["csr"] }
|
leptos = { version = "0.6", features = ["csr"] }
|
||||||
|
leptos_router = { version = "0.6", features = ["csr"] }
|
||||||
|
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
console_log = "1"
|
console_log = "1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|||||||
@@ -51,6 +51,19 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script>
|
||||||
|
if ('serviceWorker' in navigator) {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
navigator.serviceWorker.register('/sw.js')
|
||||||
|
.then(registration => {
|
||||||
|
console.log('SW registered: ', registration);
|
||||||
|
})
|
||||||
|
.catch(registrationError => {
|
||||||
|
console.log('SW registration failed: ', registrationError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -5,16 +5,35 @@
|
|||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#111827",
|
"background_color": "#111827",
|
||||||
"theme_color": "#000000",
|
"theme_color": "#000000",
|
||||||
|
"orientation": "any",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "icon-512.png",
|
"src": "icon-512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png"
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icon-192.png",
|
"src": "icon-192.png",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png"
|
"type": "image/png",
|
||||||
|
"purpose": "any maskable"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"screenshots": [
|
||||||
|
{
|
||||||
|
"src": "icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"form_factor": "wide",
|
||||||
|
"label": "Desktop Home Screen"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"form_factor": "narrow",
|
||||||
|
"label": "Mobile Home Screen"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -192,6 +192,96 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
.drawer-side {
|
||||||
|
:where(&) {
|
||||||
|
@layer daisyui.l1.l2.l3 {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@layer daisyui.l1.l2.l3 {
|
||||||
|
pointer-events: none;
|
||||||
|
visibility: hidden;
|
||||||
|
position: fixed;
|
||||||
|
inset-inline-start: calc(0.25rem * 0);
|
||||||
|
top: calc(0.25rem * 0);
|
||||||
|
z-index: 10;
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-row-start: 1;
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
grid-template-rows: repeat(1, minmax(0, 1fr));
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-items: start;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 0%;
|
||||||
|
transition: opacity 0.2s ease-out 0.1s allow-discrete, visibility 0.3s ease-out 0.1s allow-discrete;
|
||||||
|
height: 100vh;
|
||||||
|
height: 100dvh;
|
||||||
|
> .drawer-overlay {
|
||||||
|
position: sticky;
|
||||||
|
top: calc(0.25rem * 0);
|
||||||
|
cursor: pointer;
|
||||||
|
place-self: stretch;
|
||||||
|
background-color: oklch(0% 0 0 / 40%);
|
||||||
|
}
|
||||||
|
> * {
|
||||||
|
grid-column-start: 1;
|
||||||
|
grid-row-start: 1;
|
||||||
|
}
|
||||||
|
> *:not(.drawer-overlay) {
|
||||||
|
will-change: transform;
|
||||||
|
transition: translate 0.3s ease-out, width 0.2s ease-out;
|
||||||
|
translate: -100%;
|
||||||
|
[dir="rtl"] & {
|
||||||
|
translate: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.drawer-toggle {
|
||||||
|
@layer daisyui.l1.l2.l3 {
|
||||||
|
position: fixed;
|
||||||
|
height: calc(0.25rem * 0);
|
||||||
|
width: calc(0.25rem * 0);
|
||||||
|
appearance: none;
|
||||||
|
opacity: 0%;
|
||||||
|
}
|
||||||
|
@layer daisyui.l1.l2 {
|
||||||
|
:where(&:checked ~ .drawer-side) {
|
||||||
|
scrollbar-color: currentColor oklch(0 0 0 / calc(var(--page-has-backdrop, 0) * 0.4));
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
scrollbar-color: color-mix(in oklch, currentColor 35%, #0000) oklch(0 0 0 / calc(var(--page-has-backdrop, 0) * 0.4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:where(:root:has(&:checked)) {
|
||||||
|
--page-has-backdrop: 1;
|
||||||
|
--page-overflow: hidden;
|
||||||
|
--page-scroll-bg: var(--page-scroll-bg-on);
|
||||||
|
--page-scroll-gutter: stable;
|
||||||
|
--page-scroll-transition: var(--page-scroll-transition-on);
|
||||||
|
animation: set-page-has-scroll forwards;
|
||||||
|
animation-timeline: scroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@layer daisyui.l1.l2 {
|
||||||
|
:where(&:checked ~ .drawer-side) {
|
||||||
|
pointer-events: auto;
|
||||||
|
visibility: visible;
|
||||||
|
overflow-y: auto;
|
||||||
|
opacity: 100%;
|
||||||
|
& > *:not(.drawer-overlay) {
|
||||||
|
translate: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:focus-visible ~ .drawer-content label.drawer-button {
|
||||||
|
outline: 2px solid;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.menu {
|
.menu {
|
||||||
@layer daisyui.l1.l2.l3 {
|
@layer daisyui.l1.l2.l3 {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -781,6 +871,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.drawer {
|
||||||
|
@layer daisyui.l1.l2.l3 {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
width: 100%;
|
||||||
|
grid-auto-columns: max-content auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
.progress {
|
.progress {
|
||||||
@layer daisyui.l1.l2.l3 {
|
@layer daisyui.l1.l2.l3 {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -831,24 +929,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.absolute {
|
|
||||||
position: absolute;
|
|
||||||
}
|
|
||||||
.fixed {
|
.fixed {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
.relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
.static {
|
.static {
|
||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
.inset-0 {
|
.inset-0 {
|
||||||
inset: calc(var(--spacing) * 0);
|
inset: calc(var(--spacing) * 0);
|
||||||
}
|
}
|
||||||
|
.z-40 {
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
.z-\[100\] {
|
.z-\[100\] {
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
.z-\[200\] {
|
.z-\[200\] {
|
||||||
z-index: 200;
|
z-index: 200;
|
||||||
}
|
}
|
||||||
|
.drawer-content {
|
||||||
|
@layer daisyui.l1.l2.l3 {
|
||||||
|
grid-column-start: 2;
|
||||||
|
grid-row-start: 1;
|
||||||
|
min-width: calc(0.25rem * 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
.filter {
|
.filter {
|
||||||
@layer daisyui.l1.l2.l3 {
|
@layer daisyui.l1.l2.l3 {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -909,6 +1017,45 @@
|
|||||||
.my-1 {
|
.my-1 {
|
||||||
margin-block: calc(var(--spacing) * 1);
|
margin-block: calc(var(--spacing) * 1);
|
||||||
}
|
}
|
||||||
|
.label {
|
||||||
|
@layer daisyui.l1.l2.l3 {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(0.25rem * 1.5);
|
||||||
|
white-space: nowrap;
|
||||||
|
color: currentcolor;
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
color: color-mix(in oklab, currentcolor 60%, transparent);
|
||||||
|
}
|
||||||
|
&:has(input) {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
&:is(.input > *, .select > *) {
|
||||||
|
display: flex;
|
||||||
|
height: calc(100% - 0.5rem);
|
||||||
|
align-items: center;
|
||||||
|
padding-inline: calc(0.25rem * 3);
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: inherit;
|
||||||
|
&:first-child {
|
||||||
|
margin-inline-start: calc(0.25rem * -3);
|
||||||
|
margin-inline-end: calc(0.25rem * 3);
|
||||||
|
border-inline-end: var(--border) solid currentColor;
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-inline-end: var(--border) solid color-mix(in oklab, currentColor 10%, #0000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:last-child {
|
||||||
|
margin-inline-start: calc(0.25rem * 3);
|
||||||
|
margin-inline-end: calc(0.25rem * -3);
|
||||||
|
border-inline-start: var(--border) solid currentColor;
|
||||||
|
@supports (color: color-mix(in lab, red, red)) {
|
||||||
|
border-inline-start: var(--border) solid color-mix(in oklab, currentColor 10%, #0000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
.join-item {
|
.join-item {
|
||||||
&:where(*:not(:first-child, :disabled, [disabled], .btn-disabled)) {
|
&:where(*:not(:first-child, :disabled, [disabled], .btn-disabled)) {
|
||||||
margin-inline-start: calc(var(--border, 1px) * -1);
|
margin-inline-start: calc(var(--border, 1px) * -1);
|
||||||
@@ -1051,6 +1198,9 @@
|
|||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.inline-block {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
.inline-flex {
|
.inline-flex {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
@@ -1094,6 +1244,9 @@
|
|||||||
.min-h-14 {
|
.min-h-14 {
|
||||||
min-height: calc(var(--spacing) * 14);
|
min-height: calc(var(--spacing) * 14);
|
||||||
}
|
}
|
||||||
|
.min-h-full {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
.w-4 {
|
.w-4 {
|
||||||
width: calc(var(--spacing) * 4);
|
width: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@@ -1103,9 +1256,6 @@
|
|||||||
.w-8 {
|
.w-8 {
|
||||||
width: calc(var(--spacing) * 8);
|
width: calc(var(--spacing) * 8);
|
||||||
}
|
}
|
||||||
.w-20 {
|
|
||||||
width: calc(var(--spacing) * 20);
|
|
||||||
}
|
|
||||||
.w-24 {
|
.w-24 {
|
||||||
width: calc(var(--spacing) * 24);
|
width: calc(var(--spacing) * 24);
|
||||||
}
|
}
|
||||||
@@ -1118,9 +1268,6 @@
|
|||||||
.w-full {
|
.w-full {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.w-screen {
|
|
||||||
width: 100vw;
|
|
||||||
}
|
|
||||||
.max-w-full {
|
.max-w-full {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
@@ -1142,6 +1289,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-context-menu {
|
||||||
|
cursor: context-menu;
|
||||||
|
}
|
||||||
.cursor-pointer {
|
.cursor-pointer {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -1172,6 +1322,13 @@
|
|||||||
.gap-4 {
|
.gap-4 {
|
||||||
gap: calc(var(--spacing) * 4);
|
gap: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
|
.space-y-6 {
|
||||||
|
:where(& > :not(:last-child)) {
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
|
margin-block-start: calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));
|
||||||
|
margin-block-end: calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)));
|
||||||
|
}
|
||||||
|
}
|
||||||
.truncate {
|
.truncate {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -1256,12 +1413,18 @@
|
|||||||
background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
|
background-color: color-mix(in oklab, var(--color-white) 10%, transparent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.stroke-current {
|
||||||
|
stroke: currentcolor;
|
||||||
|
}
|
||||||
.checkbox-xs {
|
.checkbox-xs {
|
||||||
@layer daisyui.l1.l2 {
|
@layer daisyui.l1.l2 {
|
||||||
padding: 0.125rem;
|
padding: 0.125rem;
|
||||||
--size: calc(var(--size-selector, 0.25rem) * 4);
|
--size: calc(var(--size-selector, 0.25rem) * 4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.p-0 {
|
||||||
|
padding: calc(var(--spacing) * 0);
|
||||||
|
}
|
||||||
.p-4 {
|
.p-4 {
|
||||||
padding: calc(var(--spacing) * 4);
|
padding: calc(var(--spacing) * 4);
|
||||||
}
|
}
|
||||||
@@ -1348,6 +1511,9 @@
|
|||||||
--tw-tracking: var(--tracking-wider);
|
--tw-tracking: var(--tracking-wider);
|
||||||
letter-spacing: var(--tracking-wider);
|
letter-spacing: var(--tracking-wider);
|
||||||
}
|
}
|
||||||
|
.whitespace-nowrap {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.progress-primary {
|
.progress-primary {
|
||||||
@layer daisyui.l1.l2 {
|
@layer daisyui.l1.l2 {
|
||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
@@ -1482,6 +1648,10 @@
|
|||||||
--tw-duration: 200ms;
|
--tw-duration: 200ms;
|
||||||
transition-duration: 200ms;
|
transition-duration: 200ms;
|
||||||
}
|
}
|
||||||
|
.duration-300 {
|
||||||
|
--tw-duration: 300ms;
|
||||||
|
transition-duration: 300ms;
|
||||||
|
}
|
||||||
.btn-outline {
|
.btn-outline {
|
||||||
@layer daisyui.l1 {
|
@layer daisyui.l1 {
|
||||||
&:not( .btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn), :disabled, [disabled], .btn-disabled ) {
|
&:not( .btn-active, :hover, :active:focus, :focus-visible, input:checked:not(.filter .btn), :disabled, [disabled], .btn-disabled ) {
|
||||||
@@ -1638,6 +1808,67 @@
|
|||||||
border-radius: var(--radius-lg);
|
border-radius: var(--radius-lg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.md\:p-6 {
|
||||||
|
@media (width >= 48rem) {
|
||||||
|
padding: calc(var(--spacing) * 6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.lg\:drawer-open {
|
||||||
|
@media (width >= 64rem) {
|
||||||
|
@layer daisyui.l1.l2 {
|
||||||
|
& > .drawer-toggle:checked {
|
||||||
|
& ~ .drawer-side {
|
||||||
|
scrollbar-color: revert-layer;
|
||||||
|
}
|
||||||
|
:root:has(&) {
|
||||||
|
--page-overflow: revert-layer;
|
||||||
|
--page-scroll-gutter: revert-layer;
|
||||||
|
--page-scroll-bg: revert-layer;
|
||||||
|
--page-scroll-transition: revert-layer;
|
||||||
|
--page-has-backdrop: revert-layer;
|
||||||
|
animation: revert-layer;
|
||||||
|
animation-timeline: revert-layer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@layer daisyui.l1.l2 {
|
||||||
|
& > .drawer-side {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
> .drawer-toggle {
|
||||||
|
display: none;
|
||||||
|
& ~ .drawer-side {
|
||||||
|
pointer-events: auto;
|
||||||
|
visibility: visible;
|
||||||
|
position: sticky;
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
overscroll-behavior: auto;
|
||||||
|
opacity: 100%;
|
||||||
|
& > .drawer-overlay {
|
||||||
|
cursor: default;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
& > *:not(.drawer-overlay) {
|
||||||
|
translate: 0%;
|
||||||
|
[dir="rtl"] & {
|
||||||
|
translate: 0%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:checked ~ .drawer-side {
|
||||||
|
pointer-events: auto;
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.lg\:hidden {
|
||||||
|
@media (width >= 64rem) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@layer base {
|
@layer base {
|
||||||
html, body {
|
html, body {
|
||||||
@@ -1911,6 +2142,11 @@
|
|||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
}
|
}
|
||||||
|
@property --tw-space-y-reverse {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
@property --tw-border-style {
|
@property --tw-border-style {
|
||||||
syntax: "*";
|
syntax: "*";
|
||||||
inherits: false;
|
inherits: false;
|
||||||
@@ -2090,6 +2326,7 @@
|
|||||||
--tw-rotate-z: initial;
|
--tw-rotate-z: initial;
|
||||||
--tw-skew-x: initial;
|
--tw-skew-x: initial;
|
||||||
--tw-skew-y: initial;
|
--tw-skew-y: initial;
|
||||||
|
--tw-space-y-reverse: 0;
|
||||||
--tw-border-style: solid;
|
--tw-border-style: solid;
|
||||||
--tw-font-weight: initial;
|
--tw-font-weight: initial;
|
||||||
--tw-tracking: initial;
|
--tw-tracking: initial;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
use leptos_router::*;
|
||||||
use crate::components::layout::sidebar::Sidebar;
|
use crate::components::layout::sidebar::Sidebar;
|
||||||
use crate::components::layout::toolbar::Toolbar;
|
use crate::components::layout::toolbar::Toolbar;
|
||||||
use crate::components::layout::statusbar::StatusBar;
|
use crate::components::layout::statusbar::StatusBar;
|
||||||
@@ -9,22 +10,32 @@ pub fn App() -> impl IntoView {
|
|||||||
crate::store::provide_torrent_store();
|
crate::store::provide_torrent_store();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex flex-col h-screen w-screen overflow-hidden bg-base-100 text-base-content text-sm select-none">
|
<div class="drawer lg:drawer-open">
|
||||||
// Toolbar at the top
|
<input id="my-drawer" type="checkbox" class="drawer-toggle" />
|
||||||
<Toolbar />
|
|
||||||
|
|
||||||
<div class="flex flex-1 overflow-hidden">
|
<div class="drawer-content flex flex-col h-screen overflow-hidden bg-base-100 text-base-content text-sm select-none transition-colors duration-300">
|
||||||
// Sidebar on the left
|
// Toolbar at the top
|
||||||
<Sidebar />
|
<Toolbar />
|
||||||
|
|
||||||
// Main Content Area
|
<main class="flex-1 flex flex-col min-w-0 bg-base-100 overflow-hidden p-4 md:p-6 space-y-6">
|
||||||
<main class="flex-1 flex flex-col min-w-0 bg-base-100">
|
<Router>
|
||||||
<TorrentTable />
|
<Routes>
|
||||||
|
<Route path="/" view=move || view! { <TorrentTable /> } />
|
||||||
|
<Route path="/settings" view=move || view! { <div class="p-4">"Settings Page (Coming Soon)"</div> } />
|
||||||
|
</Routes>
|
||||||
|
</Router>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
// Status Bar at the bottom
|
||||||
|
<StatusBar />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
// Status Bar at the bottom
|
<div class="drawer-side z-40">
|
||||||
<StatusBar />
|
<label for="my-drawer" aria-label="close sidebar" class="drawer-overlay"></label>
|
||||||
|
<div class="menu p-0 min-h-full bg-base-200 text-base-content border-r border-base-300">
|
||||||
|
<Sidebar />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,40 @@
|
|||||||
use leptos::*;
|
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(
|
||||||
@@ -17,69 +53,72 @@ 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! {
|
||||||
<div
|
<div
|
||||||
class="fixed inset-0 z-[100]"
|
node_ref=target
|
||||||
on:click=move |_| on_close.call(())
|
class="fixed z-[100] bg-[#111116]/95 backdrop-blur-xl border border-white/10 rounded-xl shadow-2xl py-2 min-w-[200px] animate-in fade-in zoom-in-95 duration-100"
|
||||||
|
style=format!("left: {}px; top: {}px", position.0, position.1)
|
||||||
on:contextmenu=move |e| e.prevent_default()
|
on:contextmenu=move |e| e.prevent_default()
|
||||||
>
|
>
|
||||||
<div
|
<div class="px-3 py-1 text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">"Actions"</div>
|
||||||
class="absolute bg-[#111116]/95 backdrop-blur-xl border border-white/10 rounded-xl shadow-2xl py-2 min-w-[200px] animate-in fade-in zoom-in-95 duration-100"
|
|
||||||
style=format!("left: {}px; top: {}px", position.0, position.1)
|
<button
|
||||||
on:click=move |e| e.stop_propagation()
|
class="w-full text-left px-4 py-2.5 hover:bg-white/10 flex items-center gap-3 transition-colors text-white"
|
||||||
|
on:click={
|
||||||
|
let handle_action = handle_action.clone();
|
||||||
|
move |_| handle_action("start")
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<div class="px-3 py-1 text-xs font-bold text-gray-500 uppercase tracking-wider mb-1">"Actions"</div>
|
<svg class="w-4 h-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||||
|
"Resume"
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full text-left px-4 py-2.5 hover:bg-white/10 flex items-center gap-3 transition-colors text-white"
|
class="w-full text-left px-4 py-2.5 hover:bg-white/10 flex items-center gap-3 transition-colors text-white"
|
||||||
on:click={
|
on:click={
|
||||||
let handle_action = handle_action.clone();
|
let handle_action = handle_action.clone();
|
||||||
move |_| handle_action("start")
|
move |_| handle_action("stop")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4 text-green-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
<svg class="w-4 h-4 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||||
"Resume"
|
"Pause"
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<div class="h-px bg-white/10 my-1"></div>
|
||||||
class="w-full text-left px-4 py-2.5 hover:bg-white/10 flex items-center gap-3 transition-colors text-white"
|
|
||||||
on:click={
|
|
||||||
let handle_action = handle_action.clone();
|
|
||||||
move |_| handle_action("stop")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<svg class="w-4 h-4 text-yellow-500" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
|
||||||
"Pause"
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="h-px bg-white/10 my-1"></div>
|
<button
|
||||||
|
class="w-full text-left px-4 py-2.5 hover:bg-red-500/20 text-red-500 hover:text-red-400 flex items-center gap-3 transition-colors"
|
||||||
|
on:click={
|
||||||
|
let handle_action = handle_action.clone();
|
||||||
|
move |_| handle_action("delete")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||||
|
"Delete"
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="w-full text-left px-4 py-2.5 hover:bg-red-500/20 text-red-500 hover:text-red-400 flex items-center gap-3 transition-colors"
|
class="w-full text-left px-4 py-2.5 hover:bg-red-900/20 text-red-600 hover:text-red-400 flex items-center gap-3 transition-colors text-xs"
|
||||||
on:click={
|
on:click={
|
||||||
let handle_action = handle_action.clone();
|
let handle_action = handle_action.clone();
|
||||||
move |_| handle_action("delete")
|
move |_| handle_action("delete_with_data")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||||
"Delete"
|
<span>"Delete with Data"</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
class="w-full text-left px-4 py-2.5 hover:bg-red-900/20 text-red-600 hover:text-red-400 flex items-center gap-3 transition-colors text-xs"
|
|
||||||
on:click={
|
|
||||||
let handle_action = handle_action.clone();
|
|
||||||
move |_| handle_action("delete_with_data")
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
||||||
<span>"Delete with Data"</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}.into_view()
|
}.into_view()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<aside class="w-64 bg-base-200 h-full flex flex-col border-r border-base-300">
|
<div class="w-64 h-full flex flex-col">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<h2 class="text-xl font-bold px-4 mb-2 text-primary">"Filters"</h2>
|
<h2 class="text-xl font-bold px-4 mb-2 text-primary">"Filters"</h2>
|
||||||
<ul class="menu w-full rounded-box gap-1">
|
<ul class="menu w-full rounded-box gap-1">
|
||||||
@@ -78,6 +78,6 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
<li><a>"Error"</a></li>
|
<li><a>"Error"</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ use leptos::*;
|
|||||||
pub fn Toolbar() -> impl IntoView {
|
pub fn Toolbar() -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<div class="h-14 min-h-14 flex items-center px-4 border-b border-base-300 bg-base-100 gap-4">
|
<div class="h-14 min-h-14 flex items-center px-4 border-b border-base-300 bg-base-100 gap-4">
|
||||||
|
<label for="my-drawer" class="btn btn-square btn-ghost lg:hidden drawer-button">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="inline-block w-5 h-5 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||||
|
</label>
|
||||||
<div class="join">
|
<div class="join">
|
||||||
<button class="join-item btn btn-sm btn-outline gap-2" title="Open Torrent">
|
<button class="join-item btn btn-sm btn-outline gap-2" title="Open Torrent">
|
||||||
<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">
|
<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">
|
||||||
|
|||||||
@@ -106,9 +106,33 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let (menu_visible, set_menu_visible) = create_signal(false);
|
||||||
|
let (menu_position, set_menu_position) = create_signal((0, 0));
|
||||||
|
let (active_hash, set_active_hash) = create_signal(String::new());
|
||||||
|
|
||||||
|
let handle_context_menu = move |e: web_sys::MouseEvent, hash: String| {
|
||||||
|
e.prevent_default();
|
||||||
|
set_menu_position.set((e.client_x(), e.client_y()));
|
||||||
|
set_active_hash.set(hash);
|
||||||
|
set_menu_visible.set(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_action = move |(action, hash): (String, String)| {
|
||||||
|
logging::log!("TorrentTable Action: {} on {}", action, hash);
|
||||||
|
// TODO: Implement actual store calls here (start/stop/delete)
|
||||||
|
match action.as_str() {
|
||||||
|
"start" => { /* store.start_torrent(&hash) */ },
|
||||||
|
"stop" => { /* store.stop_torrent(&hash) */ },
|
||||||
|
"delete" => { /* store.delete_torrent(&hash, false) */ },
|
||||||
|
"delete_with_data" => { /* store.delete_torrent(&hash, true) */ },
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
set_menu_visible.set(false);
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="overflow-x-auto h-full bg-base-100">
|
<div class="overflow-x-auto h-full bg-base-100 relative"> // Added relative for positioning context if needed, though menu is fixed
|
||||||
<table class="table table-xs table-pin-rows w-full max-w-full">
|
<table class="table table-xs table-pin-rows w-full max-w-full whitespace-nowrap">
|
||||||
<thead>
|
<thead>
|
||||||
<tr class="bg-base-200 text-base-content/70">
|
<tr class="bg-base-200 text-base-content/70">
|
||||||
<th class="w-8">
|
<th class="w-8">
|
||||||
@@ -128,8 +152,6 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Status)>
|
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::Status)>
|
||||||
<div class="flex items-center">"Status" {move || sort_arrow(SortColumn::Status)}</div>
|
<div class="flex items-center">"Status" {move || sort_arrow(SortColumn::Status)}</div>
|
||||||
</th>
|
</th>
|
||||||
// <th class="w-20">"Seeds"</th> // Not available in shared::Torrent
|
|
||||||
// <th class="w-20">"Peers"</th> // Not available in shared::Torrent
|
|
||||||
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::DownSpeed)>
|
<th class="w-24 cursor-pointer hover:bg-base-300 transition-colors group select-none" on:click=move |_| handle_sort(SortColumn::DownSpeed)>
|
||||||
<div class="flex items-center">"Down Speed" {move || sort_arrow(SortColumn::DownSpeed)}</div>
|
<div class="flex items-center">"Down Speed" {move || sort_arrow(SortColumn::DownSpeed)}</div>
|
||||||
</th>
|
</th>
|
||||||
@@ -152,9 +174,16 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
shared::TorrentStatus::Error => "text-error",
|
shared::TorrentStatus::Error => "text-error",
|
||||||
_ => "text-base-content/50"
|
_ => "text-base-content/50"
|
||||||
};
|
};
|
||||||
|
let t_hash = t.hash.clone(); // Clone for closure using it in handler
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<tr class="hover group border-b border-base-200">
|
<tr
|
||||||
|
class="hover group border-b border-base-200 cursor-context-menu"
|
||||||
|
on:contextmenu={
|
||||||
|
let t_hash = t_hash.clone();
|
||||||
|
move |e: web_sys::MouseEvent| handle_context_menu(e, t_hash.clone())
|
||||||
|
}
|
||||||
|
>
|
||||||
<th>
|
<th>
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" class="checkbox checkbox-xs rounded-none" />
|
<input type="checkbox" class="checkbox checkbox-xs rounded-none" />
|
||||||
@@ -171,16 +200,22 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class={format!("text-[11px] font-medium {}", status_class)}>{status_str}</td>
|
<td class={format!("text-[11px] font-medium {}", status_class)}>{status_str}</td>
|
||||||
// <td class="text-right font-mono text-[11px] opacity-80">-</td>
|
|
||||||
// <td class="text-right font-mono text-[11px] opacity-80">-</td>
|
|
||||||
<td class="text-right font-mono text-[11px] opacity-80 text-success">{format_speed(t.down_rate)}</td>
|
<td class="text-right font-mono text-[11px] opacity-80 text-success">{format_speed(t.down_rate)}</td>
|
||||||
<td class="text-right font-mono text-[11px] opacity-80 text-primary">{format_speed(t.up_rate)}</td>
|
<td class="text-right font-mono text-[11px] opacity-80 text-primary">{format_speed(t.up_rate)}</td>
|
||||||
<td class="text-right font-mono text-[11px] opacity-80">{if t.eta > 0 { format!("{}s", t.eta) } else { "∞".to_string() }}</td> // Temporary ETA format
|
<td class="text-right font-mono text-[11px] opacity-80">{if t.eta > 0 { format!("{}s", t.eta) } else { "∞".to_string() }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>()}
|
}).collect::<Vec<_>>()}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<crate::components::context_menu::ContextMenu
|
||||||
|
visible=menu_visible.get()
|
||||||
|
position=menu_position.get()
|
||||||
|
torrent_hash=active_hash.get()
|
||||||
|
on_close=Callback::from(move |_| set_menu_visible.set(false))
|
||||||
|
on_action=Callback::from(on_action)
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
41
frontend/sw.js
Normal file
41
frontend/sw.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const CACHE_NAME = 'vibetorrent-v1';
|
||||||
|
const ASSETS_TO_CACHE = [
|
||||||
|
'/',
|
||||||
|
'/index.html',
|
||||||
|
'/vibetorrent_frontend.js',
|
||||||
|
'/vibetorrent_frontend_bg.wasm',
|
||||||
|
'/tailwind.css',
|
||||||
|
'/manifest.json',
|
||||||
|
'/icon-192.png',
|
||||||
|
'/icon-512.png'
|
||||||
|
];
|
||||||
|
|
||||||
|
self.addEventListener('install', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.open(CACHE_NAME).then((cache) => {
|
||||||
|
return cache.addAll(ASSETS_TO_CACHE);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('fetch', (event) => {
|
||||||
|
event.respondWith(
|
||||||
|
caches.match(event.request).then((response) => {
|
||||||
|
return response || fetch(event.request);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
self.addEventListener('activate', (event) => {
|
||||||
|
event.waitUntil(
|
||||||
|
caches.keys().then((cacheNames) => {
|
||||||
|
return Promise.all(
|
||||||
|
cacheNames.map((key) => {
|
||||||
|
if (key !== CACHE_NAME) {
|
||||||
|
return caches.delete(key);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -12,9 +12,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [],
|
||||||
require('daisyui'),
|
|
||||||
],
|
|
||||||
daisyui: {
|
daisyui: {
|
||||||
themes: ["dark", "light", "cupcake", "bumblebee", "emerald", "corporate", "synthwave", "retro", "cyberpunk", "valentine", "halloween", "garden", "forest", "aqua", "lofi", "pastel", "fantasy", "wireframe", "black", "luxury", "dracula", "cmyk", "autumn", "business", "acid", "lemonade", "night", "coffee", "winter"],
|
themes: ["dark", "light", "cupcake", "bumblebee", "emerald", "corporate", "synthwave", "retro", "cyberpunk", "valentine", "halloween", "garden", "forest", "aqua", "lofi", "pastel", "fantasy", "wireframe", "black", "luxury", "dracula", "cmyk", "autumn", "business", "acid", "lemonade", "night", "coffee", "winter"],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user