added ability to set media class of items
Build and Push Image / build-and-push (push) Successful in 5m4s
Build and Push Image / build-and-push (push) Successful in 5m4s
This commit is contained in:
@@ -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<Element>,
|
||||
pub onclick: Option<EventHandler<MouseEvent>>,
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct ContextMenuRenderer {
|
||||
pub menu: Signal<Option<Element>>,
|
||||
}
|
||||
|
||||
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::<ContextMenuRenderer>();
|
||||
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::<ContextMenuRenderer>();
|
||||
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}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod component;
|
||||
pub use component::*;
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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<MediaModel>,
|
||||
// pub oncontextmenu: Option<EventHandler<Event<MouseData>>>,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn MediaItem(props: MediaItemProps) -> Element {
|
||||
let mut ct_renderer = use_context::<ContextMenuRenderer>();
|
||||
|
||||
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<MouseData>| {
|
||||
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<Response<()>, Status>
|
||||
{
|
||||
let mut client = get_rpc_client();
|
||||
return client
|
||||
.set_media_class(SetMediaClassRequest {
|
||||
class: class,
|
||||
id: Some(Id { value: id }),
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
use dioxus::prelude::*;
|
||||
use web_sys::window;
|
||||
|
||||
#[component]
|
||||
pub fn Pagination(page: Signal<i32>, max_page: Signal<i32>, item_count: Signal<i32>) -> Element {
|
||||
pub fn Pagination(page: Signal<i32>, max_page: Signal<i32>, item_count: Signal<i32>) -> 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<i32>, max_page: Signal<i32>, item_count: Signal<i
|
||||
div {
|
||||
class: "pagination",
|
||||
a {
|
||||
onclick: move|_| page.set(1),
|
||||
onclick: move|_| {
|
||||
page.set(1);
|
||||
on_page_change();
|
||||
},
|
||||
"First"
|
||||
}
|
||||
a {
|
||||
onclick: move|_| page.set((cur_page_val - 1).max(1)),
|
||||
onclick: move|_| {
|
||||
let p = (cur_page_val - 1).max(1);
|
||||
page.set(p);
|
||||
on_page_change();
|
||||
},
|
||||
"Prev"
|
||||
}
|
||||
div { "Page {cur_page_val} of {max_page_val} ({item_count_val} Media Items)" }
|
||||
a {
|
||||
onclick: move|_| page.set((cur_page_val + 1).min(max_page_val)),
|
||||
onclick: move|_| {
|
||||
let p = (cur_page_val + 1).min(max_page_val);
|
||||
page.set(p);
|
||||
on_page_change();
|
||||
},
|
||||
"Next"
|
||||
}
|
||||
a {
|
||||
onclick: move|_| page.set(max_page_val),
|
||||
onclick: move|_| {
|
||||
page.set(max_page_val);
|
||||
on_page_change();
|
||||
},
|
||||
"Last"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_page_change()
|
||||
{
|
||||
let window = window().expect("Failed to get window");
|
||||
let document = window.document().expect("Failed to get document");
|
||||
document
|
||||
.query_selector("#content")
|
||||
.expect("Failed to find content")
|
||||
.expect("Failed to find content")
|
||||
.scroll_to_with_x_and_y(0.0, 0.0);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_primitives::radio_group::{self, RadioGroupProps, RadioItemProps};
|
||||
|
||||
#[component]
|
||||
pub fn RadioGroup(props: RadioGroupProps) -> 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}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mod component;
|
||||
pub use component::*;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user