diff --git a/AobaClient/Cargo.lock b/AobaClient/Cargo.lock index 53a420f..d9460a7 100644 --- a/AobaClient/Cargo.lock +++ b/AobaClient/Cargo.lock @@ -22,6 +22,7 @@ name = "aoba-client" version = "0.1.0" dependencies = [ "dioxus", + "dioxus-primitives", "dotenv", "prost", "serde", @@ -555,6 +556,17 @@ dependencies = [ "web-sys", ] +[[package]] +name = "dioxus-attributes" +version = "0.1.0" +source = "git+https://github.com/DioxusLabs/components#ccdb07f69383de008a0afadda0e5ab7ec14c1a9c" +dependencies = [ + "dioxus-rsx", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dioxus-cli-config" version = "0.7.3" @@ -663,7 +675,7 @@ dependencies = [ "futures-channel", "futures-util", "generational-box", - "lazy-js-bundle", + "lazy-js-bundle 0.7.3", "serde", "serde_json", "tracing", @@ -813,7 +825,7 @@ dependencies = [ "futures-util", "generational-box", "keyboard-types", - "lazy-js-bundle", + "lazy-js-bundle 0.7.3", "rustversion", "tracing", ] @@ -837,7 +849,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a42a7f73ad32a5054bd8c1014f4ac78cca3b7f6889210ee2b57ea31b33b6d32f" dependencies = [ "js-sys", - "lazy-js-bundle", + "lazy-js-bundle 0.7.3", "rustc-hash 2.1.1", "sledgehammer_bindgen", "sledgehammer_utils", @@ -858,6 +870,21 @@ dependencies = [ "tracing-wasm", ] +[[package]] +name = "dioxus-primitives" +version = "0.0.1" +source = "git+https://github.com/DioxusLabs/components#ccdb07f69383de008a0afadda0e5ab7ec14c1a9c" +dependencies = [ + "dioxus", + "dioxus-attributes", + "dioxus-sdk-time", + "lazy-js-bundle 0.6.2", + "num-integer", + "serde", + "time", + "tracing", +] + [[package]] name = "dioxus-router" version = "0.7.3" @@ -906,6 +933,18 @@ dependencies = [ "syn", ] +[[package]] +name = "dioxus-sdk-time" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80c25ae93a3f72e734873b97fbd09d9b1b6adff97205fb0ffd8543e3564fb78e" +dependencies = [ + "dioxus", + "futures", + "gloo-timers", + "tokio", +] + [[package]] name = "dioxus-signals" version = "0.7.3" @@ -966,7 +1005,7 @@ dependencies = [ "generational-box", "gloo-timers", "js-sys", - "lazy-js-bundle", + "lazy-js-bundle 0.7.3", "rustc-hash 2.1.1", "send_wrapper", "serde", @@ -1647,6 +1686,12 @@ dependencies = [ "bitflags", ] +[[package]] +name = "lazy-js-bundle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e49596223b9d9d4947a14a25c142a6e7d8ab3f27eb3ade269d238bb8b5c267e2" + [[package]] name = "lazy-js-bundle" version = "0.7.3" @@ -1896,6 +1941,15 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1927,6 +1981,15 @@ dependencies = [ "syn", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2792,7 +2855,9 @@ checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", "itoa", + "libc", "num-conv", + "num_threads", "powerfmt", "serde_core", "time-core", diff --git a/AobaClient/Cargo.toml b/AobaClient/Cargo.toml index 9a30a48..27a1769 100644 --- a/AobaClient/Cargo.toml +++ b/AobaClient/Cargo.toml @@ -16,6 +16,7 @@ tonic = { version = "*", default-features = false, features = [ prost = "0.13" tonic-web-wasm-client = "0.7" web-sys = { version = "0.3.91", features = ["Storage", "Window"] } +dioxus-primitives = { git = "https://github.com/DioxusLabs/components", version = "0.0.1", default-features = false } [build-dependencies] tonic-build = { version = "*", default-features = false, features = ["prost"] } diff --git a/AobaClient/assets/dx-components-theme.css b/AobaClient/assets/dx-components-theme.css new file mode 100644 index 0000000..0c97f0d --- /dev/null +++ b/AobaClient/assets/dx-components-theme.css @@ -0,0 +1,83 @@ +/* This file contains the global styles for the styled dioxus components. You only + * need to import this file once in your project root. + */ +@import url("https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap"); + +body { + padding: 0; + margin: 0; + background-color: var(--primary-color); + color: var(--secondary-color-4); + font-family: Inter, sans-serif; + font-optical-sizing: auto; + font-style: normal; + font-weight: 400; +} + +@media (prefers-color-scheme: dark) { + :root { + --dark: initial; + --light: ; + } +} + +@media (prefers-color-scheme: light) { + :root { + --dark: ; + --light: initial; + } +} + +:root { + /* Primary colors */ + --primary-color: var(--dark, #000) var(--light, #fff); + --primary-color-1: var(--dark, #0e0e0e) var(--light, #fbfbfb); + --primary-color-2: var(--dark, #0a0a0a) var(--light, #fff); + --primary-color-3: var(--dark, #141313) var(--light, #f8f8f8); + --primary-color-4: var(--dark, #1a1a1a) var(--light, #f8f8f8); + --primary-color-5: var(--dark, #262626) var(--light, #f5f5f5); + --primary-color-6: var(--dark, #232323) var(--light, #e5e5e5); + --primary-color-7: var(--dark, #3e3e3e) var(--light, #b0b0b0); + + /* Secondary colors */ + --secondary-color: var(--dark, #fff) var(--light, #000); + --secondary-color-1: var(--dark, #fafafa) var(--light, #000); + --secondary-color-2: var(--dark, #e6e6e6) var(--light, #0d0d0d); + --secondary-color-3: var(--dark, #dcdcdc) var(--light, #2b2b2b); + --secondary-color-4: var(--dark, #d4d4d4) var(--light, #111); + --secondary-color-5: var(--dark, #a1a1a1) var(--light, #848484); + --secondary-color-6: var(--dark, #5d5d5d) var(--light, #d0d0d0); + + /* Highlight colors */ + --focused-border-color: var(--dark, #2b7fff) var(--light, #2b7fff); + --primary-success-color: var(--dark, #02271c) var(--light, #ecfdf5); + --secondary-success-color: var(--dark, #b6fae3) var(--light, #10b981); + --primary-warning-color: var(--dark, #342203) var(--light, #fffbeb); + --secondary-warning-color: var(--dark, #feeac7) var(--light, #f59e0b); + --primary-error-color: var(--dark, #a22e2e) var(--light, #dc2626); + --secondary-error-color: var(--dark, #9b1c1c) var(--light, #ef4444); + --contrast-error-color: var(--dark, var(--secondary-color-3)) + var(--light, var(--primary-color)); + --primary-info-color: var(--dark, var(--primary-color-5)) + var(--light, var(--primary-color)); + --secondary-info-color: var(--dark, var(--primary-color-7)) + var(--light, var(--secondary-color-3)); +} + +/* Modern browsers with `scrollbar-*` support */ +@supports (scrollbar-width: auto) { + :not(:hover) { + scrollbar-color: rgb(0 0 0 / 0%) rgb(0 0 0 / 0%); + } + + :hover { + scrollbar-color: var(--secondary-color-2) rgb(0 0 0 / 0%); + } +} + +/* Legacy browsers with `::-webkit-scrollbar-*` support */ +@supports selector(::-webkit-scrollbar) { + :root::-webkit-scrollbar-track { + background: transparent; + } +} diff --git a/AobaClient/assets/style/dx-components.scss b/AobaClient/assets/style/dx-components.scss new file mode 100644 index 0000000..26b6ffd --- /dev/null +++ b/AobaClient/assets/style/dx-components.scss @@ -0,0 +1,10 @@ +@import "colors"; +div[role="menu"] { + backdrop-filter: blur(10px) brightness(0.5) grayscale(1); + display: flex; + flex-direction: column; + max-width: 300px; + width: auto; + outline: none; + width: max-content; +} diff --git a/AobaClient/assets/style/main.scss b/AobaClient/assets/style/main.scss index daeaffd..338da79 100644 --- a/AobaClient/assets/style/main.scss +++ b/AobaClient/assets/style/main.scss @@ -112,6 +112,14 @@ $mediaItemSize: 300px; &.placeholder { } + + &.nsfw img { + filter: blur(20px); + transition: filter 0.25s ease-out; + } + &.nsfw:hover img { + filter: blur(0px); + } } } @@ -170,34 +178,42 @@ form { flex-direction: column; max-width: 300px; } +} +.contextItem { + border-left: 4px solid $featureColor; + $size: 30px; + display: grid; + grid-template-columns: $size 1fr; + grid-template-areas: "Icon Label"; + height: $size; + transition: border 0.1s ease-out; + cursor: default; + width: 100%; - .contextItem { - border-left: 4px solid $featureColor; - $size: 30px; - display: grid; - grid-template-columns: $size 1fr auto; - height: $size; - transition: border 0.1s ease-out; - cursor: default; + &.clickable { + cursor: pointer; + } - &.clickable { - cursor: pointer; - } + .label { + grid-area: Label; + display: flex; + align-items: center; + padding: 5px 10px; + } - .label { - display: flex; - align-items: center; - padding: 5px 10px; - } + .icon { + grid-area: Icon; + display: flex; + justify-content: center; + align-items: center; + } - &:hover { - background-color: $accentColor; - color: $invertTextColor; - border-left: 10px solid $focusColor; - } + &:hover { + background-color: $accentColor; + color: $invertTextColor; + border-left: 10px solid $focusColor; } } - .pagination { display: flex; justify-content: center; diff --git a/AobaClient/src/components/context_menu.rs b/AobaClient/src/components/context_menu.rs deleted file mode 100644 index 8eabd50..0000000 --- a/AobaClient/src/components/context_menu.rs +++ /dev/null @@ -1,97 +0,0 @@ -use core::str; - -use dioxus::prelude::*; - -mod props { - use dioxus::prelude::*; - - #[derive(PartialEq, Clone, Props)] - pub struct ContextMenu { - pub top: f64, - pub left: f64, - pub items: Element, - } - - #[derive(PartialEq, Clone, Props, Default)] - pub struct ContextMenuItem { - pub name: String, - pub sub_items: Option, - pub onclick: Option>, - } -} - -#[derive(Clone, Copy, Default)] -pub struct ContextMenuRenderer { - pub menu: Signal>, -} - -impl ContextMenuRenderer { - pub fn close(&mut self) { - self.menu.set(None); - } - - pub fn render(&self) -> Element { - if let Some(menu) = self.menu.cloned() { - rsx! { - {menu} - } - } else { - rsx! {} - } - } -} - -#[component] -pub fn ContextMenuRoot() -> Element { - let renderer = use_context::(); - rsx! { - {renderer.render()} - } -} - -#[component] -pub fn ContextMenu(props: props::ContextMenu) -> Element { - rsx! { - div { - class: "contextMenu", - style: "left: {props.left}px; top: {props.top}px;", - ItemList { items: props.items } - } - } -} - -#[component] -fn ItemList(items: Element) -> Element { - rsx! { - div{ - class: "itemList", - {items} - } - } -} - -#[component] -pub fn ContextMenuItem(props: props::ContextMenuItem) -> Element { - let mut renderer = use_context::(); - if let Some(_sub) = props.sub_items { - todo!("Sub Menu"); - } - rsx! { - div{ - onclick: move |e|{ - if let Some(handler) = props.onclick{ - handler.call(e); - } - renderer.close(); - }, - class: "contextItem", - div { - class: "icon" - }, - div { - class: "label", - {props.name} - } - } - } -} diff --git a/AobaClient/src/components/context_menu/component.rs b/AobaClient/src/components/context_menu/component.rs new file mode 100644 index 0000000..0a1ee02 --- /dev/null +++ b/AobaClient/src/components/context_menu/component.rs @@ -0,0 +1,64 @@ +use dioxus::prelude::*; +use dioxus_primitives::context_menu::{ + self, ContextMenuContentProps, ContextMenuItemProps, ContextMenuProps, ContextMenuTriggerProps, +}; + +#[component] +pub fn ContextMenu(props: ContextMenuProps) -> Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + context_menu::ContextMenu { + disabled: props.disabled, + open: props.open, + default_open: props.default_open, + on_open_change: props.on_open_change, + roving_loop: props.roving_loop, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn ContextMenuTrigger(props: ContextMenuTriggerProps) -> Element { + rsx! { + context_menu::ContextMenuTrigger { + padding: "20px", + background: "var(--primary-color)", + border: "1px dashed var(--primary-color-6)", + border_radius: ".5rem", + cursor: "context-menu", + user_select: "none", + text_align: "center", + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn ContextMenuContent(props: ContextMenuContentProps) -> Element { + rsx! { + context_menu::ContextMenuContent { + class: "context-menu-content", + id: props.id, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn ContextMenuItem(props: ContextMenuItemProps) -> Element { + rsx! { + context_menu::ContextMenuItem { + class: "context-menu-item", + disabled: props.disabled, + value: props.value, + index: props.index, + on_select: props.on_select, + attributes: props.attributes, + {props.children} + } + } +} diff --git a/AobaClient/src/components/context_menu/mod.rs b/AobaClient/src/components/context_menu/mod.rs new file mode 100644 index 0000000..1723764 --- /dev/null +++ b/AobaClient/src/components/context_menu/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/AobaClient/src/components/context_menu/style.css b/AobaClient/src/components/context_menu/style.css new file mode 100644 index 0000000..a19ecb4 --- /dev/null +++ b/AobaClient/src/components/context_menu/style.css @@ -0,0 +1,71 @@ +.context-menu-content { + z-index: 1000; + min-width: 220px; + padding: 0.25rem; + border-radius: 0.5rem; + background: var(--dark, var(--primary-color-5)) + var(--light, var(--primary-color)); + box-shadow: inset 0 0 0 1px var(--dark, var(--primary-color-7)) + var(--light, var(--primary-color-6)); + opacity: 0; + pointer-events: none; + will-change: transform, opacity; +} + +.context-menu-content[data-state="closed"] { + animation: context-menu-animate-out 150ms ease-in forwards; +} + +@keyframes context-menu-animate-out { + 0% { + opacity: 1; + transform: scale(1) translateY(0); + } + + 100% { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } +} + +.context-menu-content[data-state="open"] { + animation: context-menu-animate-in 150ms ease-out forwards; +} + +@keyframes context-menu-animate-in { + 0% { + opacity: 0; + transform: scale(0.95) translateY(-2px); + } + + 100% { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.context-menu-item { + display: flex; + align-items: center; + padding: 8px 12px; + border-radius: calc(0.5rem - 0.25rem); + color: var(--secondary-color-4); + cursor: pointer; + font-size: 14px; + outline: none; + transition: background-color 100ms ease-out; + user-select: none; +} + +.context-menu-item[data-disabled="true"] { + color: var(--secondary-color-5); + cursor: not-allowed; +} + +.context-menu-item:hover:not([data-disabled="true"]), +.context-menu-item:focus-visible { + background: var(--light, var(--primary-color-4)) + var(--dark, var(--primary-color-7)); + color: var(--light, var(--secondary-color-1)) + var(--dark, var(--secondary-color-4)); +} diff --git a/AobaClient/src/components/media_item.rs b/AobaClient/src/components/media_item.rs index e0bd11f..b1adc08 100644 --- a/AobaClient/src/components/media_item.rs +++ b/AobaClient/src/components/media_item.rs @@ -1,84 +1,171 @@ use dioxus::prelude::*; +use dioxus_primitives::context_menu::{ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuTrigger}; +use tonic::{Response, Status}; use web_sys::window; use crate::{ HOST, - components::{ContextMenu, ContextMenuItem, ContextMenuRenderer}, - rpc::aoba::MediaModel, + route::Route, + rpc::{ + aoba::{Id, MediaClass, MediaModel, SetMediaClassRequest}, + get_rpc_client, + }, }; #[derive(PartialEq, Clone, Props)] -pub struct MediaItemProps { +pub struct MediaItemProps +{ pub item: Option, // pub oncontextmenu: Option>>, } #[component] -pub fn MediaItem(props: MediaItemProps) -> Element { - let mut ct_renderer = use_context::(); - - if let Some(item) = props.item { +pub fn MediaItem(props: MediaItemProps) -> Element +{ + let mut class_signal = use_signal(|| ""); + if let Some(item) = props.item + { let mtype = item.media_type().as_str_name(); let filename = item.file_name; let id = item.id.unwrap().value; let thumb = item.thumb_url; + let class = item.class; let url = item.media_url; let download = format!("{HOST}{url}"); - let oncontext = move |event: Event| { - println!("ContextMenu"); - event.prevent_default(); - event.stop_propagation(); - let data = event.data(); - if data.modifiers().ctrl() { - return; - } - let pos = data.coordinates().client(); - let left = pos.x; - let top = pos.y; - let download = download.clone(); - - let menu: Element = rsx! { - ContextMenu { - left: left, - top: top, - items: rsx! { - ContextMenuItem { - name: "Details", - }, - ContextMenuItem { - name: "Download", - onclick: move |_|{ - _ = window().unwrap().open_with_url_and_target(&download, "_blank"); - } - }, - ContextMenuItem { - name: "Delete", - }, - }, - } - }; - ct_renderer.menu.set(Some(menu)); + match class + { + 1 => class_signal.set("nsfw"), + 2 => class_signal.set("secret"), + _ => class_signal.set(""), }; return rsx! { - a { - class: "mediaItem", - href: "{HOST}{url}", - target: "_blank", - oncontextmenu: oncontext, - "data-id" : id, - img { src: "{HOST}{thumb}" } - span { class: "info", - span { class: "name", "{filename}" } - span { class: "details", - span { "{mtype}" } - span { "{item.view_count}" } + ContextMenu{ + ContextMenuTrigger{ + a { + class: "mediaItem {class_signal()}", + href: "{HOST}{url}", + target: "_blank", + "data-id" : id.clone(), + img { src: "{HOST}{thumb}" } + span { class: "info", + span { class: "name", "{filename}" } + span { class: "details", + span { "{mtype}" } + span { "{item.view_count}" } + } + } + }, + }, + ContextMenuContent{ + ContextMenuItem { + index: 0 as usize, + value: id.clone(), + on_select: move |id: String|{ + window().expect("Failed to get window") + .location().set_href(&format!("/media/{}", id)) + .expect("Failed to open Url"); + }, + div{ + class: "contextItem", + div{ + class: "label", + "Details" + } + } + }, + ContextMenuItem { + index: 1 as usize, + value: "{download}", + on_select: move |url: String|{ + window().expect("Failed to get window").open_with_url_and_target(&url, "_blank").expect("Failed to open url"); + }, + div{ + class: "contextItem", + div{ + class: "label", + "Download" + } + } + }, + { + if class != 0 { + rsx!{ContextMenuItem { + index: 2 as usize, + value: "{id}", + on_select: async |id: String|{ + _ = set_class(id, 0).await; + }, + div{ + class: "contextItem", + div{ + class: "label", + "Mark Standard" + } + } + }} + }else{ + rsx!{} + } } + { + if class != 1 { + rsx!{ContextMenuItem { + index: 2 as usize, + value: "{id}", + on_select: async |id: String|{ + _ = set_class(id, 1).await; + }, + div{ + class: "contextItem", + div{ + class: "label", + "Mark NSFW" + } + } + }} + }else{ + rsx!{} + } + } + { + if class != 1 { + rsx!{ContextMenuItem { + index: 2 as usize, + value: "{id}", + on_select: async |id: String|{ + _ = set_class(id, 2).await; + }, + div{ + class: "contextItem", + div{ + class: "label", + "Mark Secret" + } + } + }} + }else{ + rsx!{} + } + } + ContextMenuItem { + index: 2 as usize, + value: "", + div{ + class: "contextItem", + div{ + class: "label", + "Delete" + } + } + }, } - }, + } }; - } else { + } + else + { return rsx! { div { class: "mediaItem placeholder", img { }, @@ -93,3 +180,14 @@ pub fn MediaItem(props: MediaItemProps) -> Element { }; } } + +async fn set_class(id: String, class: i32) -> Result, Status> +{ + let mut client = get_rpc_client(); + return client + .set_media_class(SetMediaClassRequest { + class: class, + id: Some(Id { value: id }), + }) + .await; +} diff --git a/AobaClient/src/components/mod.rs b/AobaClient/src/components/mod.rs index 945a1bd..c8c5993 100644 --- a/AobaClient/src/components/mod.rs +++ b/AobaClient/src/components/mod.rs @@ -1,5 +1,5 @@ pub mod basic; -mod context_menu; +// mod context_menu; mod icons; mod media_grid; mod media_item; @@ -9,7 +9,7 @@ mod notif; mod pagination; mod passkey; mod search; -pub use context_menu::*; +// pub use context_menu::*; pub use media_grid::*; pub use media_item::*; pub use metrics_token::*; @@ -18,3 +18,4 @@ pub use notif::*; pub use pagination::*; pub use passkey::*; pub use search::*; +pub mod radio_group; diff --git a/AobaClient/src/components/pagination.rs b/AobaClient/src/components/pagination.rs index a66638e..c00f17a 100644 --- a/AobaClient/src/components/pagination.rs +++ b/AobaClient/src/components/pagination.rs @@ -1,7 +1,9 @@ use dioxus::prelude::*; +use web_sys::window; #[component] -pub fn Pagination(page: Signal, max_page: Signal, item_count: Signal) -> Element { +pub fn Pagination(page: Signal, max_page: Signal, item_count: Signal) -> Element +{ let cur_page_val = page.cloned(); let max_page_val = max_page.cloned(); let item_count_val = item_count.cloned(); @@ -9,22 +11,47 @@ pub fn Pagination(page: Signal, max_page: Signal, item_count: Signal Element { + rsx! { + document::Link { rel: "stylesheet", href: asset!("./style.css") } + radio_group::RadioGroup { + class: "radio-group", + value: props.value, + default_value: props.default_value, + on_value_change: props.on_value_change, + disabled: props.disabled, + required: props.required, + name: props.name, + horizontal: props.horizontal, + roving_loop: props.roving_loop, + attributes: props.attributes, + {props.children} + } + } +} + +#[component] +pub fn RadioItem(props: RadioItemProps) -> Element { + rsx! { + radio_group::RadioItem { + class: "radio-item", + value: props.value, + index: props.index, + disabled: props.disabled, + attributes: props.attributes, + {props.children} + } + } +} diff --git a/AobaClient/src/components/radio_group/mod.rs b/AobaClient/src/components/radio_group/mod.rs new file mode 100644 index 0000000..1723764 --- /dev/null +++ b/AobaClient/src/components/radio_group/mod.rs @@ -0,0 +1,2 @@ +mod component; +pub use component::*; \ No newline at end of file diff --git a/AobaClient/src/components/radio_group/style.css b/AobaClient/src/components/radio_group/style.css new file mode 100644 index 0000000..570ee2c --- /dev/null +++ b/AobaClient/src/components/radio_group/style.css @@ -0,0 +1,48 @@ +.radio-group { + display: flex; + flex-direction: column; + gap: .75rem; +} + +.radio-item { + display: flex; + flex-direction: row; + align-items: center; + padding: 0; + border: none; + background-color: transparent; + color: var(--secondary-color-4); + font-size: 14px; + gap: .75rem; + + &::before { + display: block; + width: 1rem; + height: 1rem; + box-sizing: border-box; + border-radius: 1.5rem; + background: var(--light, var(--primary-color)) var(--dark, var(--primary-color-3)); + box-shadow: 0 0 0 1px var(--light, var(--primary-color-6)) + var(--dark, var(--primary-color-7)); + content: ""; + cursor: pointer; + } + + &:focus-visible { + outline: none; + } + + &:focus-visible::before { + box-shadow: 0 0 0 2px var(--focused-border-color); + } + + &[data-state="checked"]::before { + border: 0.25rem solid var(--light, var(--primary-color)) var(--dark, var(--primary-color-3)); + background: var(--secondary-color-4); + } + + &[data-disabled="true"]::before { + cursor: not-allowed; + opacity: 0.5; + } +} diff --git a/AobaClient/src/layouts/main_layout.rs b/AobaClient/src/layouts/main_layout.rs index 76dd84b..3171fe2 100644 --- a/AobaClient/src/layouts/main_layout.rs +++ b/AobaClient/src/layouts/main_layout.rs @@ -1,35 +1,32 @@ use dioxus::prelude::*; -use crate::{ - Route, - components::{ContextMenuRenderer, ContextMenuRoot, Navbar}, - contexts::AuthContext, - views::Login, -}; +use crate::{Route, components::Navbar, contexts::AuthContext, views::Login}; #[component] -pub fn MainLayout() -> Element { +pub fn MainLayout() -> Element +{ let auth_context = use_context::(); - if auth_context.jwt.cloned().is_none() { + if auth_context.jwt.cloned().is_none() + { return rsx! { Login { } }; } - let mut ct_renderer = use_context::(); + // let mut ct_renderer = use_context::(); return rsx! { - ContextMenuRoot { } + // ContextMenuRoot { } Navbar { } div { id: "content", - onclick: move |_| { - ct_renderer.close(); - }, - oncontextmenu: move |_| { - ct_renderer.close(); - }, + // onclick: move |_| { + // ct_renderer.close(); + // }, + // oncontextmenu: move |_| { + // ct_renderer.close(); + // }, Outlet:: { } } }; diff --git a/AobaClient/src/main.rs b/AobaClient/src/main.rs index 5fe0f79..b4f6cc4 100644 --- a/AobaClient/src/main.rs +++ b/AobaClient/src/main.rs @@ -11,8 +11,6 @@ use contexts::AuthContext; use dioxus::prelude::*; use route::Route; -use crate::components::ContextMenuRenderer; - #[cfg(debug_assertions)] pub const HOST: &'static str = "http://localhost:8081"; #[cfg(debug_assertions)] @@ -25,21 +23,25 @@ pub const HOST: &'static str = "https://aoba.app"; const FAVICON: Asset = asset!("/assets/favicon.ico"); const MAIN_CSS: Asset = asset!("/assets/style/main.scss"); const INPUT_CSS: Asset = asset!("/assets/style/inputs.scss"); +const DX_COMPONENTS: Asset = asset!("/assets/style/dx-components.scss"); -fn main() { +fn main() +{ dioxus::launch(App); } #[component] -fn App() -> Element { +fn App() -> Element +{ use_context_provider(|| AuthContext::new()); - use_context_provider(|| ContextMenuRenderer::default()); + // use_context_provider(|| ContextMenuRenderer::default()); rsx! { document::Link { rel: "icon", href: FAVICON } document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" } document::Link { rel: "preconnect", href: "https://fonts.gstatic.com" } document::Link { rel: "stylesheet", href: MAIN_CSS } document::Link { rel: "stylesheet", href: INPUT_CSS } + document::Link { rel: "stylesheet", href: DX_COMPONENTS } document::Link { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap", diff --git a/AobaClient/src/route.rs b/AobaClient/src/route.rs index f018fe5..a24009f 100644 --- a/AobaClient/src/route.rs +++ b/AobaClient/src/route.rs @@ -1,6 +1,6 @@ use crate::{ layouts::MainLayout, - views::{Home, Media, Settings}, + views::{Home, HomePaged, Media, Settings}, }; use dioxus::prelude::*; @@ -11,6 +11,8 @@ pub enum Route { #[route("/")] Home { }, + #[route("/?:page&:q")] + HomePaged { page: i32, q: String }, #[route("/media/:id")] Media { id: String }, #[route("/settings")] diff --git a/AobaClient/src/rpc.rs b/AobaClient/src/rpc.rs index ee442f3..378bd8d 100644 --- a/AobaClient/src/rpc.rs +++ b/AobaClient/src/rpc.rs @@ -9,7 +9,8 @@ use crate::{ rpc::aoba::{auth_rpc_client::AuthRpcClient, metrics_rpc_client::MetricsRpcClient}, }; -pub mod aoba { +pub mod aoba +{ tonic::include_proto!("aoba"); } @@ -21,31 +22,38 @@ static RPC_CLIENT: RpcConnection = RpcConnection { }; #[derive(Default)] -pub struct RpcConnection { +pub struct RpcConnection +{ aoba: RwLock>>>, auth: RwLock>>, metrics: RwLock>>>, jwt: RwLock>, } -impl RpcConnection { - pub fn get_client(&self) -> AobaRpcClient> { +impl RpcConnection +{ + pub fn get_client(&self) -> AobaRpcClient> + { self.ensure_client(); return self.aoba.read().unwrap().clone().unwrap(); } - pub fn get_auth_client(&self) -> AuthRpcClient { + pub fn get_auth_client(&self) -> AuthRpcClient + { self.ensure_client(); return self.auth.read().unwrap().clone().unwrap(); } - pub fn get_metrics_client(&self) -> MetricsRpcClient> { + pub fn get_metrics_client(&self) -> MetricsRpcClient> + { self.ensure_client(); return self.metrics.read().unwrap().clone().unwrap(); } - fn ensure_client(&self) { - if self.aoba.read().unwrap().is_none() { + fn ensure_client(&self) + { + if self.aoba.read().unwrap().is_none() + { let wasm_client = Client::new(RPC_HOST.into()); let aoba_client = AobaRpcClient::with_interceptor(wasm_client.clone(), AuthInterceptor); *self.aoba.write().unwrap() = Some(aoba_client); @@ -58,9 +66,12 @@ impl RpcConnection { #[derive(Clone)] pub struct AuthInterceptor; -impl Interceptor for AuthInterceptor { - fn call(&mut self, mut request: tonic::Request<()>) -> Result, tonic::Status> { - if let Some(jwt) = RPC_CLIENT.jwt.read().unwrap().clone() { +impl Interceptor for AuthInterceptor +{ + fn call(&mut self, mut request: tonic::Request<()>) -> Result, tonic::Status> + { + if let Some(jwt) = RPC_CLIENT.jwt.read().unwrap().clone() + { request .metadata_mut() .insert("authorization", format!("Bearer {jwt}").parse().unwrap()); @@ -69,21 +80,26 @@ impl Interceptor for AuthInterceptor { } } -pub fn get_rpc_client() -> AobaRpcClient> { +pub fn get_rpc_client() -> AobaRpcClient> +{ return RPC_CLIENT.get_client(); } -pub fn get_auth_rpc_client() -> AuthRpcClient { +pub fn get_auth_rpc_client() -> AuthRpcClient +{ return RPC_CLIENT.get_auth_client(); } -pub fn get_metrics_rpc_client() -> MetricsRpcClient> { +pub fn get_metrics_rpc_client() -> MetricsRpcClient> +{ return RPC_CLIENT.get_metrics_client(); } -pub fn login(jwt: String) { +pub fn login(jwt: String) +{ *RPC_CLIENT.jwt.write().unwrap() = Some(jwt); } -pub fn logout() { +pub fn logout() +{ *RPC_CLIENT.jwt.write().unwrap() = None; } diff --git a/AobaClient/src/views/home.rs b/AobaClient/src/views/home.rs index 1b3a4c0..e0932b0 100644 --- a/AobaClient/src/views/home.rs +++ b/AobaClient/src/views/home.rs @@ -2,7 +2,8 @@ use crate::components::{MediaGrid, Pagination, Search}; use dioxus::prelude::*; #[component] -pub fn Home() -> Element { +pub fn Home() -> Element +{ let query = use_signal(|| "".to_string()); let page = use_signal(|| 1 as i32); let max_page = use_signal(|| 1 as i32); @@ -16,3 +17,20 @@ pub fn Home() -> Element { MediaGrid { query: query.cloned(), page: page.cloned(), max_page, total_items: item_count } } } + +#[component] +pub fn HomePaged(page: i32, q: String) -> Element +{ + let query = use_signal(|| q); + let page = use_signal(|| page); + let max_page = use_signal(|| 1 as i32); + let item_count = use_signal(|| 0 as i32); + rsx! { + div { + class: "stickyTop", + Search { query, page }, + Pagination { page, max_page, item_count }, + } + MediaGrid { query: query.cloned(), page: page.cloned(), max_page, total_items: item_count } + } +} diff --git a/AobaClient/src/views/media.rs b/AobaClient/src/views/media.rs index cc4d27a..ed1a9f4 100644 --- a/AobaClient/src/views/media.rs +++ b/AobaClient/src/views/media.rs @@ -1,8 +1,54 @@ +use crate::HOST; +use crate::components::radio_group::{RadioGroup, RadioItem}; +use crate::rpc::aoba::SetMediaClassRequest; +use crate::rpc::{ + aoba::{Id, MediaModel}, + get_rpc_client, +}; use dioxus::prelude::*; #[component] -pub fn Media(id: String) -> Element { +pub fn Media(id: String) -> Element +{ + let media_result = use_resource(use_reactive!(|(id)| async move { + let mut client = get_rpc_client(); + let result = client.get_media(Id { value: id.clone() }).await; + if let Ok(item) = result + { + let res = item.into_inner(); + return res.value; + } + else + { + return None; + } + })); + + return match media_result.cloned().unwrap_or(None) + { + Some(media) => + { + return rsx! {MediaPage{media: media}}; + } + None => rsx! {"Not Found"}, + }; +} + +#[component] +fn MediaPage(media: MediaModel) -> Element +{ + let url = media.thumb_url; + let id = media.id.expect("Media has no id").value.clone(); + let cur_class = match media.class + { + 0 => "Standard", + 1 => "NSFW", + 2 => "Secret", + _ => "Unkown", + }; rsx! { - {id} + img { src: "{HOST}{url}", } + label { "Media Class: {cur_class}" } + } } diff --git a/AobaCore/Services/AobaService.cs b/AobaCore/Services/AobaService.cs index a2a1636..1c989d5 100644 --- a/AobaCore/Services/AobaService.cs +++ b/AobaCore/Services/AobaService.cs @@ -20,7 +20,7 @@ public class AobaService(IMongoDatabase db) return await _media.Find(m => m.LegacyId == id).FirstOrDefaultAsync(cancellationToken); } - public async Task GetMediaFromFileAsync(ObjectId id, CancellationToken cancellationToken = default) + public async Task GetMediaAsync(ObjectId id, CancellationToken cancellationToken = default) { return await _media.Find(m => m.MediaId == id).FirstOrDefaultAsync(cancellationToken); } diff --git a/AobaCore/Services/ThumbnailService.cs b/AobaCore/Services/ThumbnailService.cs index 90a5b2c..7d8f0ad 100644 --- a/AobaCore/Services/ThumbnailService.cs +++ b/AobaCore/Services/ThumbnailService.cs @@ -64,7 +64,7 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService) if (existingThumb != null) return existingThumb; - var media = await aobaService.GetMediaFromFileAsync(mediaId, cancellationToken); + var media = await aobaService.GetMediaAsync(mediaId, cancellationToken); if (media == null) return new Error("Media does not exist"); diff --git a/AobaServer/Proto/Types.proto b/AobaServer/Proto/Types.proto index 6eb416b..00fd441 100644 --- a/AobaServer/Proto/Types.proto +++ b/AobaServer/Proto/Types.proto @@ -41,10 +41,7 @@ message Id { } message MediaResponse { - oneof result { - MediaModel value = 1; - google.protobuf.Empty empty = 2; - } + optional MediaModel value = 1; } message ListResponse { diff --git a/AobaServer/Services/AobaRpcService.cs b/AobaServer/Services/AobaRpcService.cs index 0b863b8..52aacd2 100644 --- a/AobaServer/Services/AobaRpcService.cs +++ b/AobaServer/Services/AobaRpcService.cs @@ -17,7 +17,7 @@ public class AobaRpcService(AobaService aobaService, AccountsService accountsSer { public override async Task GetMedia(Id request, ServerCallContext context) { - var media = await aobaService.GetMediaFromLegacyIdAsync(request.ToObjectId(), context.CancellationToken); + var media = await aobaService.GetMediaAsync(request.ToObjectId(), context.CancellationToken); return media.ToResponse(); } diff --git a/AobaServer/Utils/ProtoExtensions.cs b/AobaServer/Utils/ProtoExtensions.cs index 8a1b571..aa3e739 100644 --- a/AobaServer/Utils/ProtoExtensions.cs +++ b/AobaServer/Utils/ProtoExtensions.cs @@ -35,7 +35,7 @@ public static class ProtoExtensions public static MediaResponse ToResponse(this Media? media) { if(media == null) - return new MediaResponse() { Empty = new Empty() }; + return new MediaResponse() {}; return new MediaResponse() { Value = media.ToMediaModel()