Compare commits
10 Commits
3dca408356
...
d36aaac836
| Author | SHA1 | Date | |
|---|---|---|---|
| d36aaac836 | |||
| bb2c6c4683 | |||
| f8d457a096 | |||
| 165adb2775 | |||
| e832ccf07e | |||
| 6df6de098b | |||
| 364b23e62a | |||
| 7b2ed32043 | |||
| d094f7bbef | |||
| f98429159f |
58
.github/workflows/release.yaml
vendored
58
.github/workflows/release.yaml
vendored
@@ -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 }}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
97
AobaClient/src/components/context_menu.rs
Normal file
97
AobaClient/src/components/context_menu.rs
Normal 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}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 { }
|
||||||
})}
|
})}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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! {
|
||||||
|
|||||||
@@ -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::*;
|
||||||
|
|||||||
@@ -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> { }
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,9 +125,9 @@ public class ThumbnailService(IMongoDatabase db, AobaService aobaService)
|
|||||||
Size = new Size(300, 300)
|
Size = new Size(300, 300)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
img.Value.Dispose();
|
|
||||||
var result = new MemoryStream();
|
var result = new MemoryStream();
|
||||||
await img.Value.SaveAsWebpAsync(result, cancellationToken);
|
await img.Value.SaveAsWebpAsync(result, cancellationToken);
|
||||||
|
img.Value.Dispose();
|
||||||
result.Position = 0;
|
result.Position = 0;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user