diff --git a/AobaClient/src/components/media_grid.rs b/AobaClient/src/components/media_grid.rs index fc1d522..0026564 100644 --- a/AobaClient/src/components/media_grid.rs +++ b/AobaClient/src/components/media_grid.rs @@ -1,7 +1,7 @@ use dioxus::prelude::*; use crate::{ - components::{MediaItem, MediaItemPlaceHolder}, + components::{MediaClassChangeEvent, MediaItem, MediaItemPlaceHolder}, rpc::{ aoba::{MediaModel, PageFilter}, get_rpc_client, @@ -9,7 +9,8 @@ use crate::{ }; #[derive(PartialEq, Clone, Props)] -pub struct MediaGridProps { +pub struct MediaGridProps +{ pub query: Signal, pub max_page: Signal, pub total_items: Signal, @@ -18,19 +19,21 @@ pub struct MediaGridProps { pub on_page_loaded: Option>, } -pub struct PaginationInfo { +pub struct PaginationInfo +{ pub total_pages: i32, pub total_items: i32, } #[component] -pub fn MediaGrid(props: MediaGridProps) -> Element { +pub fn MediaGrid(props: MediaGridProps) -> Element +{ let mut error_display = use_signal(|| { rsx! {} }); let mut items = use_signal::>>(|| None); let media_result = use_resource(use_reactive!(|(props)| async move { - items.set(None); + // items.set(None); let mut client = get_rpc_client(); let request = PageFilter { page_size: Some(props.page_size.cloned()), @@ -38,24 +41,32 @@ pub fn MediaGrid(props: MediaGridProps) -> Element { query: Some(props.query.cloned()), }; let result = client.list_media(request).await; - if let Ok(items) = result { + if let Ok(items) = result + { let res = items.into_inner(); return Ok(res); - } else { + } + else + { let err = result.err().unwrap(); let message = err.message(); return Err(format!("Failed to load results: {message}")); } })); - use_effect(move || match media_result() { - Some(value) => match value { - Ok(result) => { - if let Some(pagination) = result.pagination { + use_effect(move || match media_result() + { + Some(value) => match value + { + Ok(result) => + { + if let Some(pagination) = result.pagination + { let total_pages = pagination.total_pages; let total_items = pagination.total_items; - if let Some(handler) = props.on_page_loaded { + if let Some(handler) = props.on_page_loaded + { handler.call(PaginationInfo { total_pages, total_items, @@ -71,7 +82,8 @@ pub fn MediaGrid(props: MediaGridProps) -> Element { } }), }, - _ => {} + _ => + {} }); rsx! { @@ -79,7 +91,35 @@ pub fn MediaGrid(props: MediaGridProps) -> Element { class: "mediaGrid", {error_display} {match items(){ - Some(itms) => rsx!{MediaList { items: itms }}, + Some(itms) => rsx!{ + MediaList { + items: itms, + on_item_deleted: move |id|{ + if let Some(cur) = items.cloned(){ + let filtered = cur.iter() + .filter(|i| i.id.clone().expect("No id").value != id) + .map(|i|i.clone()) + .collect(); + items.set(Some(filtered)); + } + }, + on_class_changed: move |e: MediaClassChangeEvent|{ + if let Some(cur) = items.cloned(){ + let updated = cur.iter() + .map(|i|{ + let mut itm = i.clone(); + let id = itm.id.clone().expect("No id").value; + if id == e.id{ + itm.class = e.class; + } + return itm; + }) + .collect(); + items.set(Some(updated)); + } + } + } + }, None => rsx!{PlaceholderGrid { count: props.page_size.cloned() as usize }} }} } @@ -87,7 +127,8 @@ pub fn MediaGrid(props: MediaGridProps) -> Element { } #[component] -fn PlaceholderGrid(count: usize) -> Element { +fn PlaceholderGrid(count: usize) -> Element +{ rsx! { div{ class: "mediaGrid", @@ -99,12 +140,18 @@ fn PlaceholderGrid(count: usize) -> Element { } #[component] -fn MediaList(items: Vec) -> Element { +fn MediaList( + items: Vec, + on_item_deleted: Option>, + on_class_changed: Option>, +) -> Element +{ rsx! { - {items.iter().enumerate().map(|(index, itm)| rsx!{ + {items.iter().map(|itm| rsx!{ MediaItem { item: itm.clone(), - index + on_deleted: on_item_deleted, + on_class_changed: on_class_changed } })} } diff --git a/AobaClient/src/components/media_item.rs b/AobaClient/src/components/media_item.rs index 35fe680..b22e3ea 100644 --- a/AobaClient/src/components/media_item.rs +++ b/AobaClient/src/components/media_item.rs @@ -13,17 +13,16 @@ use crate::{ pub struct MediaClassChangeEvent { - pub index: usize, - pub class: String, + pub id: String, + pub class: i32, } #[derive(PartialEq, Clone, Props)] pub struct MediaItemProps { pub item: MediaModel, - pub index: usize, pub on_class_changed: Option>, - pub on_deleted: Option>, + pub on_deleted: Option>, } #[component] @@ -100,8 +99,11 @@ pub fn MediaItem(props: MediaItemProps) -> Element value: "{id}", on_select: move |id: String|{ spawn(async move { - if let Ok(_) = set_class(id, 0).await{ + if let Ok(_) = set_class(&id, 0).await{ class_signal.set(""); + if let Some(handler) = props.on_class_changed{ + handler.call(MediaClassChangeEvent { id, class: 0 }); + } } }); }, @@ -118,12 +120,15 @@ pub fn MediaItem(props: MediaItemProps) -> Element { if class_signal() != "blur" { rsx!{ContextMenuItem { - index: 2 as usize, + index: 3 as usize, value: "{id}", on_select: move |id: String|{ spawn(async move { - if let Ok(_) = set_class(id, 1).await{ + if let Ok(_) = set_class(&id, 1).await{ class_signal.set("blur"); + if let Some(handler) = props.on_class_changed{ + handler.call(MediaClassChangeEvent { id, class: 1 }); + } } }); }, @@ -140,12 +145,15 @@ pub fn MediaItem(props: MediaItemProps) -> Element { if class_signal() != "secret" { rsx!{ContextMenuItem { - index: 2 as usize, + index: 4 as usize, value: "{id}", on_select: move |id: String|{ spawn(async move { - if let Ok(_) = set_class(id, 2).await{ + if let Ok(_) = set_class(&id, 2).await{ class_signal.set("secret"); + if let Some(handler) = props.on_class_changed{ + handler.call(MediaClassChangeEvent { id, class: 2 }); + } } }); }, @@ -160,8 +168,17 @@ pub fn MediaItem(props: MediaItemProps) -> Element }else{rsx!{}} } ContextMenuItem { - index: 2 as usize, - value: "", + index: 5 as usize, + value: "{id}", + on_select: move |id: String|{ + spawn(async move { + if let Ok(_) = delete_media(id.clone()).await{ + if let Some(handler) = props.on_deleted { + handler.call(id); + } + } + }); + }, div{ class: "contextItem", div{ @@ -192,13 +209,19 @@ pub fn MediaItemPlaceHolder() -> Element }; } -async fn set_class(id: String, class: i32) -> Result, Status> +async fn delete_media(id: String) -> Result, Status> +{ + let mut client = get_rpc_client(); + return client.delete_media(Id { value: id }).await; +} + +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 }), + id: Some(Id { value: id.clone() }), }) .await; } diff --git a/AobaCore/Services/AobaService.cs b/AobaCore/Services/AobaService.cs index 1c989d5..07d4e8b 100644 --- a/AobaCore/Services/AobaService.cs +++ b/AobaCore/Services/AobaService.cs @@ -133,6 +133,23 @@ public class AobaService(IMongoDatabase db) } } + public async Task DeleteFilesAsync(IEnumerable mediaIds, CancellationToken cancellationToken = default) + { + foreach (var id in mediaIds) + { + try + { + cancellationToken.ThrowIfCancellationRequested(); + await _gridFs.DeleteAsync(id, CancellationToken.None); + await _media.DeleteOneAsync(m => m.MediaId == id, CancellationToken.None); + } + catch (GridFSFileNotFoundException) + { + //ignore if file was not found + } + } + } + public async Task DeriveTagsAsync(CancellationToken cancellationToken = default) diff --git a/AobaServer/Proto/Aoba.proto b/AobaServer/Proto/Aoba.proto index 526e6bb..928fb88 100644 --- a/AobaServer/Proto/Aoba.proto +++ b/AobaServer/Proto/Aoba.proto @@ -8,6 +8,7 @@ import "Proto/Types.proto"; service AobaRpc { rpc GetMedia (Id) returns (MediaResponse); rpc DeleteMedia (Id) returns (google.protobuf.Empty); + rpc DeleteMediaBulk (IdList) returns (google.protobuf.Empty); rpc UpdateMedia (google.protobuf.Empty) returns (google.protobuf.Empty); rpc ListMedia(PageFilter) returns (ListResponse); rpc GetUser(Id) returns (UserResponse); diff --git a/AobaServer/Proto/Types.proto b/AobaServer/Proto/Types.proto index 02a452a..e1fc694 100644 --- a/AobaServer/Proto/Types.proto +++ b/AobaServer/Proto/Types.proto @@ -40,6 +40,10 @@ message Id { string value = 1; } +message IdList { + repeated Id value = 1; +} + message MediaResponse { optional MediaModel value = 1; } diff --git a/AobaServer/Services/AobaRpcService.cs b/AobaServer/Services/AobaRpcService.cs index 52aacd2..cd8e516 100644 --- a/AobaServer/Services/AobaRpcService.cs +++ b/AobaServer/Services/AobaRpcService.cs @@ -57,4 +57,15 @@ public class AobaRpcService(AobaService aobaService, AccountsService accountsSer }; } + public override async Task DeleteMedia(Id request, ServerCallContext context) + { + await aobaService.DeleteFileAsync(request.ToObjectId(), context.CancellationToken); + return new Empty(); + } + + public override async Task DeleteMediaBulk(IdList request, ServerCallContext context) + { + await aobaService.DeleteFilesAsync(request.ToObjectId(), context.CancellationToken); + return new Empty(); + } } \ No newline at end of file diff --git a/AobaServer/Utils/ProtoExtensions.cs b/AobaServer/Utils/ProtoExtensions.cs index aa3e739..596a42f 100644 --- a/AobaServer/Utils/ProtoExtensions.cs +++ b/AobaServer/Utils/ProtoExtensions.cs @@ -70,4 +70,9 @@ public static class ProtoExtensions { return id.Value.ToObjectId(); } + + public static IEnumerable ToObjectId(this IdList id) + { + return id.Value.Select(v => v.ToObjectId()); + } }