13 Commits

Author SHA1 Message Date
d36aaac836 empty 2025-08-13 23:14:21 -04:00
bb2c6c4683 removed woodpecker file 2025-08-13 21:57:33 -04:00
f8d457a096 moved woodpecker file to root 2025-08-13 21:51:10 -04:00
165adb2775 added woodpecker, updated build to update latest
All checks were successful
Build and Push Image / build-and-push (push) Successful in 6m21s
2025-08-13 21:48:08 -04:00
e832ccf07e ContextMenu Renderer Finalized 2025-08-10 00:11:05 -04:00
6df6de098b WIP ContextMenu Renderer 2025-08-09 17:14:41 -04:00
364b23e62a context menu rendering wip 2025-08-03 12:12:13 -04:00
7b2ed32043 added context menu 2025-08-02 13:38:15 -04:00
d094f7bbef misc 2025-07-29 20:27:59 -04:00
f98429159f Fix incorrect dispose
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
2025-07-23 16:22:09 -04:00
3dca408356 Cleanup potential memory leaks
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
2025-07-23 15:45:35 -04:00
1655e342b7 Removed avif + heif support 2025-07-17 20:02:41 -04:00
7223c35658 Added avif + heif support
Some checks failed
Build and Push Image / build-and-push (push) Has been cancelled
2025-07-17 15:09:16 -04:00
16 changed files with 305 additions and 71 deletions

View File

@@ -1,37 +1,39 @@
name: "Build and Push Image" name: "Build and Push Image"
on: on:
push: push:
tags: tags:
- "v*" - "v*"
jobs: jobs:
build-and-push: build-and-push:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkou code - name: Checkou code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up Docker Build
uses: docker/setup-buildx-action@v3
- name: Login - name: Set up Docker Build
uses: docker/login-action@v3 uses: docker/setup-buildx-action@v3
with:
registry: git.kaisei.app
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Extract tag version - name: Login
id: extract_tag uses: docker/login-action@v3
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV with:
registry: git.kaisei.app
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASS }}
- name: Build and Push - name: Extract tag version
uses: docker/build-push-action@v5 id: extract_tag
with: run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
file: AobaServer/Dockerfile
context: . - name: Build and Push
push: true uses: docker/build-push-action@v5
tags: git.kaisei.app/amatsugu/aoba:${{ env.VERSION }} with:
build-args: VERSION=${{ env.VERSION }} file: AobaServer/Dockerfile
context: .
push: true
tags: |
git.kaisei.app/amatsugu/aoba:${{ env.VERSION }}
git.kaisei.app/amatsugu/aoba:latest
build-args: VERSION=${{ env.VERSION }}

View File

@@ -1,8 +1,9 @@
$mainBGColor: #584577; $mainBGColor: #584577;
$featureColor: #ce2d4f; $featureColor: #ce2d4f;
$accentColor: #f0eaf8; $focusColor: #ff3862;
$accentColor: #f0eaf8;
$mainTextColor: #eee;
$brightTextColor: #fff; $mainTextColor: #eee;
$invertTextColor: #222; $brightTextColor: #fff;
$invertBrightTextColor: #000; $invertTextColor: #222;
$invertBrightTextColor: #000;

View File

@@ -157,3 +157,41 @@ form {
padding: 5px; padding: 5px;
user-select: all; user-select: all;
} }
.contextMenu {
backdrop-filter: blur(10px) brightness(0.5) grayscale(1);
position: absolute;
z-index: 100;
.itemList {
display: flex;
flex-direction: column;
max-width: 300px;
}
.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;
}
.label {
display: flex;
align-items: center;
padding: 5px 10px;
}
&:hover {
background-color: $accentColor;
color: $invertTextColor;
border-left: 10px solid $focusColor;
}
}
}

View File

@@ -7,7 +7,7 @@ pub struct InputProps {
pub label: Option<String>, pub label: Option<String>,
pub placeholder: Option<String>, pub placeholder: Option<String>,
pub name: String, pub name: String,
pub oninput: Option<EventHandler<FormEvent>>, pub oninput: Option<EventHandler<Event<FormData>>>,
pub required: Option<bool>, pub required: Option<bool>,
} }
@@ -22,9 +22,12 @@ pub fn Input(props: InputProps) -> Element {
r#type: props.r#type.unwrap_or("text".into()), r#type: props.r#type.unwrap_or("text".into()),
value: props.value, value: props.value,
oninput: move |e| { oninput: move |e| {
if let Some(mut s) = props.value { if let Some(mut s) = props.value {
s.set(e.value()); s.set(e.value());
} }
if let Some(handler) = props.oninput{
handler.call(e);
}
}, },
name: props.name, name: props.name,
placeholder: ph, placeholder: ph,

View File

@@ -0,0 +1,97 @@
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}
}
}
}
}

View File

@@ -51,8 +51,11 @@ pub fn MediaGrid(props: MediaGridProps) -> Element {
Ok(result) => rsx! { Ok(result) => rsx! {
div { div {
class: "mediaGrid", class: "mediaGrid",
// oncontextmenu: oncontext,
{result.items.iter().map(|itm| rsx!{ {result.items.iter().map(|itm| rsx!{
MediaItem { item: Some(itm.clone()) } MediaItem {
item: itm.clone()
}
})}, })},
} }
}, },
@@ -69,7 +72,7 @@ pub fn MediaGrid(props: MediaGridProps) -> Element {
div{ div{
class: "mediaGrid", class: "mediaGrid",
{(0..50).map(|_| rsx!{ {(0..50).map(|_| rsx!{
MediaItem {} MediaItem { }
})} })}
} }
}, },

View File

@@ -1,25 +1,72 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use web_sys::window;
use crate::{HOST, rpc::aoba::MediaModel}; use crate::{
HOST,
components::{ContextMenu, ContextMenuItem, ContextMenuRenderer},
rpc::aoba::MediaModel,
};
#[derive(PartialEq, Clone, Props)] #[derive(PartialEq, Clone, Props)]
pub struct MediaItemProps { pub struct MediaItemProps {
pub item: Option<MediaModel>, pub item: Option<MediaModel>,
// pub oncontextmenu: Option<EventHandler<Event<MouseData>>>,
} }
#[component] #[component]
pub fn MediaItem(props: MediaItemProps) -> Element { pub fn MediaItem(props: MediaItemProps) -> Element {
let mut ct_renderer = use_context::<ContextMenuRenderer>();
if let Some(item) = props.item { if let Some(item) = props.item {
let mtype = item.media_type().as_str_name(); let mtype = item.media_type().as_str_name();
let filename = item.file_name; let filename = item.file_name;
let id = item.id.unwrap().value; let id = item.id.unwrap().value;
let thumb = item.thumb_url; let thumb = item.thumb_url;
let url = item.media_url; 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));
};
return rsx! { return rsx! {
a { a {
class: "mediaItem", class: "mediaItem",
href: "{HOST}/{url}", href: "{HOST}{url}",
target: "_blank", target: "_blank",
oncontextmenu: oncontext,
"data-id" : id, "data-id" : id,
img { src: "{HOST}{thumb}" } img { src: "{HOST}{thumb}" }
span { class: "info", span { class: "info",
@@ -29,7 +76,7 @@ pub fn MediaItem(props: MediaItemProps) -> Element {
span { "{item.view_count}" } span { "{item.view_count}" }
} }
} }
} },
}; };
} else { } else {
return rsx! { return rsx! {

View File

@@ -1,10 +1,12 @@
pub mod basic; pub mod basic;
mod context_menu;
mod media_grid; mod media_grid;
mod media_item; mod media_item;
mod metrics_token; mod metrics_token;
mod navbar; mod navbar;
mod notif; mod notif;
mod search; mod search;
pub use context_menu::*;
pub use media_grid::*; pub use media_grid::*;
pub use media_item::*; pub use media_item::*;
pub use metrics_token::*; pub use metrics_token::*;

View File

@@ -1,6 +1,11 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use crate::{Route, components::Navbar, contexts::AuthContext, views::Login}; use crate::{
Route,
components::{ContextMenuRenderer, ContextMenuRoot, Navbar},
contexts::AuthContext,
views::Login,
};
#[component] #[component]
pub fn MainLayout() -> Element { pub fn MainLayout() -> Element {
@@ -8,12 +13,24 @@ pub fn MainLayout() -> Element {
if auth_context.jwt.cloned().is_none() { if auth_context.jwt.cloned().is_none() {
return rsx! { return rsx! {
Login {} Login { }
}; };
} }
let mut ct_renderer = use_context::<ContextMenuRenderer>();
return rsx! { return rsx! {
Navbar {} ContextMenuRoot { }
div { id: "content", Outlet::<Route> {} } Navbar { }
div {
id: "content",
onclick: move |_| {
ct_renderer.close();
},
oncontextmenu: move |_| {
ct_renderer.close();
},
Outlet::<Route> { }
}
}; };
} }

View File

@@ -11,6 +11,8 @@ use contexts::AuthContext;
use dioxus::prelude::*; use dioxus::prelude::*;
use route::Route; use route::Route;
use crate::components::ContextMenuRenderer;
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub const HOST: &'static str = "http://localhost:8081"; pub const HOST: &'static str = "http://localhost:8081";
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -30,7 +32,8 @@ fn main() {
#[component] #[component]
fn App() -> Element { fn App() -> Element {
let _auth_state = use_context_provider(|| AuthContext::new()); use_context_provider(|| AuthContext::new());
use_context_provider(|| ContextMenuRenderer::default());
rsx! { rsx! {
document::Link { rel: "icon", href: FAVICON } document::Link { rel: "icon", href: FAVICON }
document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" } document::Link { rel: "preconnect", href: "https://fonts.googleapis.com" }
@@ -41,6 +44,7 @@ fn App() -> Element {
rel: "stylesheet", rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap", href: "https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,100..900;1,100..900&display=swap",
} }
Router::<Route> {}
Router::<Route> { }
} }
} }

View File

@@ -9,14 +9,14 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="FFMpegCore" Version="5.2.0" /> <PackageReference Include="FFMpegCore" Version="5.2.0" />
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" /> <PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
<PackageReference Include="MaybeError" Version="1.1.0" /> <PackageReference Include="MaybeError" Version="1.1.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.6" /> <PackageReference Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.7" />
<PackageReference Include="MongoDB.Driver" Version="3.4.0" /> <PackageReference Include="MongoDB.Driver" Version="3.4.1" />
<PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.1.0" /> <PackageReference Include="MongoDB.Driver.Core.Extensions.DiagnosticSources" Version="2.1.0" />
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" /> <PackageReference Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.1" /> <PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.12.1" />
<PackageReference Include="ZLinq" Version="1.4.12" /> <PackageReference Include="ZLinq" Version="1.5.1" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -79,6 +79,10 @@ public class AobaService(IMongoDatabase db)
{ {
return ex; return ex;
} }
finally
{
data.Dispose();
}
} }
public async Task<MaybeEx<GridFSDownloadStream, GridFSException>> GetFileStreamAsync(ObjectId mediaId, bool seekable = false, CancellationToken cancellationToken = default) public async Task<MaybeEx<GridFSDownloadStream, GridFSException>> GetFileStreamAsync(ObjectId mediaId, bool seekable = false, CancellationToken cancellationToken = default)

View File

@@ -3,6 +3,7 @@
using FFMpegCore; using FFMpegCore;
using FFMpegCore.Pipes; using FFMpegCore.Pipes;
using MaybeError.Errors; using MaybeError.Errors;
using MongoDB.Bson; using MongoDB.Bson;
@@ -10,6 +11,7 @@ using MongoDB.Driver;
using MongoDB.Driver.GridFS; using MongoDB.Driver.GridFS;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing;
using System; using System;
@@ -91,17 +93,29 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
{ {
return type switch return type switch
{ {
MediaType.Image => await GenerateImageThumbnailAsync(stream, size, cancellationToken), MediaType.Image => await GenerateImageThumbnailAsync(stream, size, ext, cancellationToken),
MediaType.Video => GenerateVideoThumbnail(stream, size, cancellationToken), MediaType.Video => GenerateVideoThumbnail(stream, size, cancellationToken),
MediaType.Text or MediaType.Code => await GenerateDocumentThumbnailAsync(stream, size, cancellationToken), MediaType.Text or MediaType.Code => await GenerateDocumentThumbnailAsync(stream, size, cancellationToken),
_ => new Error($"No Thumbnail for {type}"), _ => new Error($"No Thumbnail for {type}"),
}; };
} }
public static async Task<Stream> GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, CancellationToken cancellationToken = default) private static Maybe<Image> LoadImage(Stream stream, string ext)
{ {
var img = Image.Load(stream); if (ext is ".heif" or ".avif")
img.Mutate(o => {
return new Error("Unsupported image type");
}
else
return Image.Load(stream);
}
public static async Task<Maybe<Stream>> GenerateImageThumbnailAsync(Stream stream, ThumbnailSize size, string ext, CancellationToken cancellationToken = default)
{
var img = LoadImage(stream, ext);
if (img.HasError)
return img.Error;
img.Value.Mutate(o =>
{ {
var size = var size =
o.Resize(new ResizeOptions o.Resize(new ResizeOptions
@@ -112,7 +126,8 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
}); });
}); });
var result = new MemoryStream(); var result = new MemoryStream();
await img.SaveAsWebpAsync(result, cancellationToken); await img.Value.SaveAsWebpAsync(result, cancellationToken);
img.Value.Dispose();
result.Position = 0; result.Position = 0;
return result; return result;
} }
@@ -141,7 +156,7 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
output.Position = 0; output.Position = 0;
return output; return output;
} }
catch(Exception ex) catch (Exception ex)
{ {
return ex; return ex;
} }
@@ -155,4 +170,4 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
{ {
return new NotImplementedException(); return new NotImplementedException();
} }
} }

View File

@@ -16,7 +16,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" /> <PackageReference Include="Isopoh.Cryptography.Argon2" Version="2.0.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.7" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.12.1" /> <PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.12.1" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" /> <PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.22.1" />
<PackageReference Include="MimeTypesMap" Version="1.0.9" /> <PackageReference Include="MimeTypesMap" Version="1.0.9" />
@@ -24,7 +24,7 @@
<PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" /> <PackageReference Include="OpenTelemetry.Exporter.Prometheus.AspNetCore" Version="1.9.0-beta.2" />
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" /> <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" /> <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
<PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" /> <PackageReference Include="OpenTelemetry.Instrumentation.Http" Version="1.12.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -22,6 +22,7 @@ public class MediaApi(AobaService aoba) : ControllerBase
if (media.HasError) if (media.HasError)
return Problem(detail: media.Error.Message, statusCode: StatusCodes.Status400BadRequest); return Problem(detail: media.Error.Message, statusCode: StatusCodes.Status400BadRequest);
return Ok(new return Ok(new
{ {
media = media.Value, media = media.Value,

View File

@@ -103,11 +103,11 @@ message ShareXResponse {
message SearchQuery { message SearchQuery {
optional string queryText = 1; optional string queryText = 1;
repeated Filter filters = 2; repeated Filter filters = 2;
} }
message Filter { message Filter {
string key = 1; string key = 1;
repeated string values = 2; repeated string values = 2;
} }