Add tag display and file search

Signed-off-by: trivernis <trivernis@protonmail.com>
pull/4/head
trivernis 3 years ago
parent e0a42b1b0f
commit aeac1e2e36

@ -55,7 +55,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"directories", "directories",
"mediarepo", "mediarepo",
"rmp-ipc 0.4.3", "rmp-ipc",
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
@ -1840,7 +1840,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "mediarepo" name = "mediarepo"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/Trivernis/mediarepo-daemon?rev=57308089d58706ee1438f6b55d53691e9e08ce6b#57308089d58706ee1438f6b55d53691e9e08ce6b" source = "git+https://github.com/Trivernis/mediarepo-daemon?rev=ce93a5793348a05052b324562730d97f5c6e2128#ce93a5793348a05052b324562730d97f5c6e2128"
dependencies = [ dependencies = [
"log", "log",
"mediarepo-core", "mediarepo-core",
@ -1851,14 +1851,14 @@ dependencies = [
[[package]] [[package]]
name = "mediarepo-core" name = "mediarepo-core"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/Trivernis/mediarepo-daemon?rev=57308089d58706ee1438f6b55d53691e9e08ce6b#57308089d58706ee1438f6b55d53691e9e08ce6b" source = "git+https://github.com/Trivernis/mediarepo-daemon?rev=ce93a5793348a05052b324562730d97f5c6e2128#ce93a5793348a05052b324562730d97f5c6e2128"
dependencies = [ dependencies = [
"base64", "base64",
"futures", "futures",
"image", "image",
"multibase", "multibase",
"multihash", "multihash",
"rmp-ipc 0.6.0", "rmp-ipc",
"sea-orm", "sea-orm",
"serde", "serde",
"sqlx", "sqlx",
@ -1871,7 +1871,7 @@ dependencies = [
[[package]] [[package]]
name = "mediarepo-database" name = "mediarepo-database"
version = "0.2.0" version = "0.2.0"
source = "git+https://github.com/Trivernis/mediarepo-daemon?rev=57308089d58706ee1438f6b55d53691e9e08ce6b#57308089d58706ee1438f6b55d53691e9e08ce6b" source = "git+https://github.com/Trivernis/mediarepo-daemon?rev=ce93a5793348a05052b324562730d97f5c6e2128#ce93a5793348a05052b324562730d97f5c6e2128"
dependencies = [ dependencies = [
"chrono", "chrono",
"mediarepo-core", "mediarepo-core",
@ -1882,7 +1882,7 @@ dependencies = [
[[package]] [[package]]
name = "mediarepo-model" name = "mediarepo-model"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/Trivernis/mediarepo-daemon?rev=57308089d58706ee1438f6b55d53691e9e08ce6b#57308089d58706ee1438f6b55d53691e9e08ce6b" source = "git+https://github.com/Trivernis/mediarepo-daemon?rev=ce93a5793348a05052b324562730d97f5c6e2128#ce93a5793348a05052b324562730d97f5c6e2128"
dependencies = [ dependencies = [
"chrono", "chrono",
"mediarepo-core", "mediarepo-core",
@ -1898,7 +1898,7 @@ dependencies = [
[[package]] [[package]]
name = "mediarepo-socket" name = "mediarepo-socket"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/Trivernis/mediarepo-daemon?rev=57308089d58706ee1438f6b55d53691e9e08ce6b#57308089d58706ee1438f6b55d53691e9e08ce6b" source = "git+https://github.com/Trivernis/mediarepo-daemon?rev=ce93a5793348a05052b324562730d97f5c6e2128#ce93a5793348a05052b324562730d97f5c6e2128"
dependencies = [ dependencies = [
"chrono", "chrono",
"mediarepo-core", "mediarepo-core",
@ -2927,24 +2927,9 @@ dependencies = [
[[package]] [[package]]
name = "rmp-ipc" name = "rmp-ipc"
version = "0.4.3" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ada2c7329ea55e1e80f2a1ae9ddb123f93ae4b349d493273138118feadfecb"
dependencies = [
"lazy_static",
"log",
"rmp-serde",
"serde",
"thiserror",
"tokio",
"typemap_rev",
]
[[package]]
name = "rmp-ipc"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbd5082cefa9a4407327087d75dfadb5bf2d6f07168d0b609a2509f54fa3b243" checksum = "17765aa8bd4d19dd81c53c0707192115f36ec200aea9f4557526932ac1f418e0"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"log", "log",

@ -18,7 +18,7 @@ tauri-build = { version = "1.0.0-beta.4" }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-beta.8", features = ["api-all"] } tauri = { version = "1.0.0-beta.8", features = ["api-all"] }
rmp-ipc = "0.4.3" rmp-ipc = "0.7.0"
thiserror = "1.0.30" thiserror = "1.0.30"
directories = "4.0.1" directories = "4.0.1"
toml = "0.5.8" toml = "0.5.8"
@ -30,7 +30,7 @@ features = ["fs", "io-std", "io-util"]
[dependencies.mediarepo] [dependencies.mediarepo]
git = "https://github.com/Trivernis/mediarepo-daemon" git = "https://github.com/Trivernis/mediarepo-daemon"
rev = "57308089d58706ee1438f6b55d53691e9e08ce6b" rev = "ce93a5793348a05052b324562730d97f5c6e2128"
features = ["library"] features = ["library"]
default-features=false default-features=false

@ -1,4 +1,5 @@
use mediarepo::requests::{GetFileThumbnailsRequest, ReadFileRequest}; use crate::commands::get_ipc;
use mediarepo::requests::{FindFilesByTagsRequest, GetFileThumbnailsRequest, ReadFileRequest};
use mediarepo::responses::{FileResponse, ThumbnailResponse}; use mediarepo::responses::{FileResponse, ThumbnailResponse};
use crate::context::Context; use crate::context::Context;
@ -6,47 +7,83 @@ use crate::error::{AppError, AppResult};
#[tauri::command] #[tauri::command]
pub async fn get_all_files(context: tauri::State<'_, Context>) -> AppResult<Vec<FileResponse>> { pub async fn get_all_files(context: tauri::State<'_, Context>) -> AppResult<Vec<FileResponse>> {
let ipc = context.ipc.read().await; let ipc = get_ipc(context).await?;
if let Some(ipc) = &*ipc { let response = ipc
let response = ipc.emitter.emit_to("files", "all_files", ()).await?.await_reply(&ipc).await?; .emitter
.emit_to("files", "all_files", ())
.await?
.await_reply(&ipc)
.await?;
Ok(response.data::<Vec<FileResponse>>()?) Ok(response.data::<Vec<FileResponse>>()?)
} else {
Err(AppError::new("No ipc connection."))
}
} }
#[tauri::command] #[tauri::command]
pub async fn read_file_by_hash(hash: String, context: tauri::State<'_, Context>) -> AppResult<Vec<u8>> { pub async fn find_files(
let ipc = context.ipc.read().await; tags: Vec<String>,
if let Some(ipc) = &*ipc { context: tauri::State<'_, Context>,
let response = ipc.emitter.emit_to("files", "read_file", ReadFileRequest::Hash(hash)).await?.await_reply(&ipc).await?; ) -> AppResult<Vec<FileResponse>> {
let ipc = get_ipc(context).await?;
let response = ipc
.emitter
.emit_to("files", "find_files", FindFilesByTagsRequest { tags })
.await?
.await_reply(&ipc)
.await?;
Ok(response.data::<Vec<FileResponse>>()?)
}
Ok(response.data::<Vec<u8>>()?) #[tauri::command]
} else { pub async fn read_file_by_hash(
Err(AppError::new("No ipc connection.")) hash: String,
} context: tauri::State<'_, Context>,
) -> AppResult<Vec<u8>> {
let ipc = get_ipc(context).await?;
let response = ipc
.emitter
.emit_to("files", "read_file", ReadFileRequest::Hash(hash))
.await?
.await_reply(&ipc)
.await?;
Ok(response.data_raw().to_vec())
} }
#[tauri::command] #[tauri::command]
pub async fn get_thumbnails(hash: String, context: tauri::State<'_, Context>) -> AppResult<Vec<ThumbnailResponse>> { pub async fn get_thumbnails(
let ipc = context.ipc.read().await; hash: String,
if let Some(ipc) = &*ipc { context: tauri::State<'_, Context>,
let response = ipc.emitter.emit_to("files", "get_thumbnails", GetFileThumbnailsRequest::Hash(hash)).await?.await_reply(&ipc).await?; ) -> AppResult<Vec<ThumbnailResponse>> {
let ipc = get_ipc(context).await?;
let response = ipc
.emitter
.emit_to(
"files",
"get_thumbnails",
GetFileThumbnailsRequest::Hash(hash),
)
.await?
.await_reply(&ipc)
.await?;
Ok(response.data::<Vec<ThumbnailResponse>>()?) Ok(response.data::<Vec<ThumbnailResponse>>()?)
} else {
Err(AppError::new("No ipc connection."))
}
} }
#[tauri::command] #[tauri::command]
pub async fn read_thumbnail(hash: String, context: tauri::State<'_, Context>) -> AppResult<Vec<u8>> { pub async fn read_thumbnail(
hash: String,
context: tauri::State<'_, Context>,
) -> AppResult<Vec<u8>> {
let ipc = context.ipc.read().await; let ipc = context.ipc.read().await;
if let Some(ipc) = &*ipc { if let Some(ipc) = &*ipc {
let response = ipc.emitter.emit_to("files", "read_thumbnail", hash).await?.await_reply(&ipc).await?; let response = ipc
.emitter
.emit_to("files", "read_thumbnail", hash)
.await?
.await_reply(&ipc)
.await?;
Ok(response.data::<Vec<u8>>()?) Ok(response.data_raw().to_vec())
} else { } else {
Err(AppError::new("No ipc connection.")) Err(AppError::new("No ipc connection."))
} }

@ -1,8 +1,11 @@
use rmp_ipc::ipc::context::Context as IPCContext;
use crate::context::Context; use crate::context::Context;
use crate::error::AppResult; use crate::error::{AppError, AppResult};
pub mod repo;
pub mod files; pub mod files;
pub mod repo;
pub mod tags;
#[tauri::command] #[tauri::command]
pub async fn emit_info(context: tauri::State<'_, Context>) -> AppResult<()> { pub async fn emit_info(context: tauri::State<'_, Context>) -> AppResult<()> {
@ -16,3 +19,8 @@ pub async fn emit_info(context: tauri::State<'_, Context>) -> AppResult<()> {
Ok(()) Ok(())
} }
pub async fn get_ipc(context: tauri::State<'_, Context>) -> AppResult<IPCContext> {
let ipc = context.ipc.read().await;
(ipc.clone()).ok_or(AppError::new("No ipc connection."))
}

@ -1,13 +1,13 @@
use std::path::PathBuf;
use serde::{Serialize, Deserialize};
use crate::context::Context; use crate::context::Context;
use crate::error::{AppError, AppResult}; use crate::error::{AppError, AppResult};
use tokio::fs;
use crate::settings::save_settings;
use rmp_ipc::context::Context as IPCContext;
use tauri::Window;
use crate::ipc::build_ipc_context; use crate::ipc::build_ipc_context;
use crate::settings::save_settings;
use rmp_ipc::ipc::context::Context as IPCContext;
use serde::{Deserialize, Serialize};
use std::mem; use std::mem;
use std::path::PathBuf;
use tauri::Window;
use tokio::fs;
static REPO_CONFIG_FILE: &str = "repo.toml"; static REPO_CONFIG_FILE: &str = "repo.toml";
@ -33,13 +33,19 @@ pub async fn get_repositories(context: tauri::State<'_, Context>) -> AppResult<V
} }
#[tauri::command] #[tauri::command]
pub async fn get_active_repository(context: tauri::State<'_, Context>) -> AppResult<Option<Repository>> { pub async fn get_active_repository(
context: tauri::State<'_, Context>,
) -> AppResult<Option<Repository>> {
let repo = context.active_repository.read().await; let repo = context.active_repository.read().await;
Ok(repo.clone()) Ok(repo.clone())
} }
#[tauri::command] #[tauri::command]
pub async fn add_repository(name: String, path: String, context: tauri::State<'_, Context>) -> AppResult<Vec<Repository>> { pub async fn add_repository(
name: String,
path: String,
context: tauri::State<'_, Context>,
) -> AppResult<Vec<Repository>> {
let repo_path = path.clone(); let repo_path = path.clone();
let path = PathBuf::from(path); let path = PathBuf::from(path);
let RepoConfig { listen_address, .. } = read_repo_config(path.join(REPO_CONFIG_FILE)).await?; let RepoConfig { listen_address, .. } = read_repo_config(path.join(REPO_CONFIG_FILE)).await?;
@ -62,9 +68,16 @@ pub async fn add_repository(name: String, path: String, context: tauri::State<'_
} }
#[tauri::command] #[tauri::command]
pub async fn select_repository(window: Window, name: String, context: tauri::State<'_, Context>) -> AppResult<()> { pub async fn select_repository(
window: Window,
name: String,
context: tauri::State<'_, Context>,
) -> AppResult<()> {
let settings = context.settings.read().await; let settings = context.settings.read().await;
let repo = settings.repositories.get(&name).ok_or(AppError::new(format!("Repository '{}' not found", name)))?; let repo = settings
.repositories
.get(&name)
.ok_or(AppError::new(format!("Repository '{}' not found", name)))?;
let ipc = connect(window, &repo.address).await?; let ipc = connect(window, &repo.address).await?;
let mut ipc_ctx = context.ipc.write().await; let mut ipc_ctx = context.ipc.write().await;
let old_ipc = mem::replace(&mut *ipc_ctx, Some(ipc)); let old_ipc = mem::replace(&mut *ipc_ctx, Some(ipc));

@ -0,0 +1,21 @@
use crate::commands::get_ipc;
use crate::context::Context;
use crate::error::AppResult;
use mediarepo::requests::FileIdentifier;
use mediarepo::responses::TagResponse;
#[tauri::command]
pub async fn get_tags_for_file(
hash: String,
context: tauri::State<'_, Context>,
) -> AppResult<Vec<TagResponse>> {
let ipc = get_ipc(context).await?;
let response = ipc
.emitter
.emit_to("tags", "tags_for_file", FileIdentifier::Hash(hash))
.await?
.await_reply(&ipc)
.await?;
Ok(response.data::<Vec<TagResponse>>()?)
}

@ -1,14 +1,14 @@
use std::sync::Arc;
use tokio::sync::RwLock;
use rmp_ipc::context::Context as IPCContext;
use crate::commands::repo::Repository; use crate::commands::repo::Repository;
use crate::settings::Settings; use crate::settings::Settings;
use rmp_ipc::ipc::context::Context as IPCContext;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)] #[derive(Clone)]
pub struct Context { pub struct Context {
pub active_repository: Arc<RwLock<Option<Repository>>>, pub active_repository: Arc<RwLock<Option<Repository>>>,
pub ipc: Arc<RwLock<Option<IPCContext>>>, pub ipc: Arc<RwLock<Option<IPCContext>>>,
pub settings: Arc<RwLock<Settings>> pub settings: Arc<RwLock<Settings>>,
} }
impl Context { impl Context {
@ -16,7 +16,7 @@ impl Context {
Self { Self {
ipc: Arc::new(RwLock::new(None)), ipc: Arc::new(RwLock::new(None)),
active_repository: Arc::new(RwLock::new(None)), active_repository: Arc::new(RwLock::new(None)),
settings: Arc::new(RwLock::new(settings)) settings: Arc::new(RwLock::new(settings)),
} }
} }
} }

@ -1,13 +1,14 @@
mod files; mod files;
use crate::error::AppResult;
use mediarepo::responses::InfoResponse; use mediarepo::responses::InfoResponse;
use rmp_ipc::context::{Context as IPCContext, Context};
use rmp_ipc::{Event, IPCBuilder};
use rmp_ipc::error::Result; use rmp_ipc::error::Result;
use rmp_ipc::error_event::{ERROR_EVENT_NAME, ErrorEventData}; use rmp_ipc::error_event::{ErrorEventData, ERROR_EVENT_NAME};
use rmp_ipc::event::Event;
use rmp_ipc::ipc::context::Context as IPCContext;
use rmp_ipc::IPCBuilder;
use tauri::Window; use tauri::Window;
use typemap_rev::TypeMapKey; use typemap_rev::TypeMapKey;
use crate::error::AppResult;
pub struct WindowKey; pub struct WindowKey;
@ -19,27 +20,32 @@ pub async fn build_ipc_context(window: Window, address: &str) -> AppResult<IPCCo
let ctx = IPCBuilder::new() let ctx = IPCBuilder::new()
.address(address) .address(address)
.insert::<WindowKey>(window) .insert::<WindowKey>(window)
.on(ERROR_EVENT_NAME, |c, e|Box::pin(handle_error(c, e))) .on(ERROR_EVENT_NAME, |c, e| Box::pin(handle_error(c, e)))
.on("info", |c, e| Box::pin(handle_info(c, e))) .on("info", |c, e| Box::pin(handle_info(c, e)))
.build_client().await?; .build_client()
.await?;
Ok(ctx) Ok(ctx)
} }
async fn handle_error(ctx: &Context, event: Event) -> Result<()> { async fn handle_error(ctx: &IPCContext, event: Event) -> Result<()> {
let error_data = event.data::<ErrorEventData>()?; let error_data = event.data::<ErrorEventData>()?;
let data = ctx.data.read().await; let data = ctx.data.read().await;
let window = data.get::<WindowKey>().unwrap(); let window = data.get::<WindowKey>().unwrap();
window.emit("error", error_data).expect("Failed to emit error event"); window
.emit("error", error_data)
.expect("Failed to emit error event");
Ok(()) Ok(())
} }
async fn handle_info(ctx: &Context, event: Event) -> Result<()> { async fn handle_info(ctx: &IPCContext, event: Event) -> Result<()> {
let info_data = event.data::<InfoResponse>()?; let info_data = event.data::<InfoResponse>()?;
let data = ctx.data.read().await; let data = ctx.data.read().await;
let window = data.get::<WindowKey>().unwrap(); let window = data.get::<WindowKey>().unwrap();
window.emit("info", info_data).expect("Failed to emit info event"); window
.emit("info", info_data)
.expect("Failed to emit info event");
Ok(()) Ok(())
} }

@ -3,17 +3,20 @@
windows_subsystem = "windows" windows_subsystem = "windows"
)] )]
use crate::commands::repo::{get_repositories, add_repository, select_repository, get_active_repository};
use crate::commands::files::*;
use crate::commands::emit_info; use crate::commands::emit_info;
use crate::commands::files::*;
use crate::commands::repo::{
add_repository, get_active_repository, get_repositories, select_repository,
};
use crate::commands::tags::*;
use crate::context::Context; use crate::context::Context;
use crate::settings::load_settings; use crate::settings::load_settings;
mod commands; mod commands;
pub mod context; pub mod context;
pub mod error; pub mod error;
mod settings;
mod ipc; mod ipc;
mod settings;
fn main() { fn main() {
let settings = load_settings().expect("Failed to load settings"); let settings = load_settings().expect("Failed to load settings");
@ -21,7 +24,19 @@ fn main() {
tauri::Builder::default() tauri::Builder::default()
.manage(context) .manage(context)
.invoke_handler(tauri::generate_handler![get_repositories, add_repository, select_repository, get_active_repository, emit_info, get_all_files, read_file_by_hash, get_thumbnails, read_thumbnail]) .invoke_handler(tauri::generate_handler![
get_repositories,
add_repository,
select_repository,
get_active_repository,
emit_info,
get_all_files,
read_file_by_hash,
get_thumbnails,
read_thumbnail,
get_tags_for_file,
find_files,
])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

@ -24,6 +24,8 @@ import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatPaginatorModule} from "@angular/material/paginator"; import {MatPaginatorModule} from "@angular/material/paginator";
import {ScrollingModule} from "@angular/cdk/scrolling"; import {ScrollingModule} from "@angular/cdk/scrolling";
import {LightboxModule} from "ngx-lightbox"; import {LightboxModule} from "ngx-lightbox";
import {MatChipsModule} from "@angular/material/chips";
import {MatIconModule} from "@angular/material/icon";
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -35,25 +37,27 @@ import {LightboxModule} from "ngx-lightbox";
FileGridComponent, FileGridComponent,
FileGridEntryComponent, FileGridEntryComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
AppRoutingModule, AppRoutingModule,
BrowserAnimationsModule, BrowserAnimationsModule,
MatCardModule, MatCardModule,
MatListModule, MatListModule,
MatButtonModule, MatButtonModule,
MatToolbarModule, MatToolbarModule,
MatSnackBarModule, MatSnackBarModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
ReactiveFormsModule, ReactiveFormsModule,
MatSidenavModule, MatSidenavModule,
MatGridListModule, MatGridListModule,
MatProgressBarModule, MatProgressBarModule,
MatPaginatorModule, MatPaginatorModule,
ScrollingModule, ScrollingModule,
LightboxModule LightboxModule,
], MatChipsModule,
MatIconModule
],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

@ -21,7 +21,8 @@ export class FileGridComponent implements OnChanges {
@Input() files: File[] = []; @Input() files: File[] = [];
@Input() columns: number = 6; @Input() columns: number = 6;
@Output() fileDblClickEvent = new EventEmitter<File>(); @Output() fileDblClickEvent = new EventEmitter<File>();
@Output() filesSelectEvent = new EventEmitter<File[]>(); @Output() fileMultiselectEvent = new EventEmitter<File[]>();
@Output() fileSelectEvent = new EventEmitter<File | undefined>();
selectedEntries: GridEntry[] = []; selectedEntries: GridEntry[] = [];
@ -51,6 +52,8 @@ export class FileGridComponent implements OnChanges {
* @param {FileGridEntryComponent} clickedEntry * @param {FileGridEntryComponent} clickedEntry
*/ */
setSelectedFile(clickedEntry: GridEntry) { setSelectedFile(clickedEntry: GridEntry) {
const previousSelectionSize = this.selectedEntries.length;
if (!(this.shiftClicked || this.ctrlClicked) && this.selectedEntries.length > 0) { if (!(this.shiftClicked || this.ctrlClicked) && this.selectedEntries.length > 0) {
this.selectedEntries.forEach(entry => {if (entry !== clickedEntry) entry.selected = false}); this.selectedEntries.forEach(entry => {if (entry !== clickedEntry) entry.selected = false});
this.selectedEntries = []; this.selectedEntries = [];
@ -61,7 +64,13 @@ export class FileGridComponent implements OnChanges {
clickedEntry.selected = !clickedEntry.selected; clickedEntry.selected = !clickedEntry.selected;
this.selectedEntries.push(clickedEntry); this.selectedEntries.push(clickedEntry);
} }
this.filesSelectEvent.emit(this.selectedEntries.map(entry => entry.file)); if (this.selectedEntries.length == 1) {
this.fileSelectEvent.emit(this.selectedEntries.map(entry => entry.file)[0]);
} else if (this.selectedEntries.length == 0 && previousSelectionSize == 1){
this.fileSelectEvent.emit(undefined);
} else {
this.fileMultiselectEvent.emit(this.selectedEntries.map(entry => entry.file));
}
} }
private handleShiftSelect(clickedEntry: GridEntry): void { private handleShiftSelect(clickedEntry: GridEntry): void {
@ -85,7 +94,6 @@ export class FileGridComponent implements OnChanges {
this.selectedEntries.push(gridEntry); this.selectedEntries.push(gridEntry);
} }
} }
} }
} }

@ -0,0 +1,5 @@
export type Tag = {
id: number,
name: string,
namespace: string | undefined,
};

@ -4,10 +4,31 @@
</mat-toolbar> </mat-toolbar>
<mat-drawer-container> <mat-drawer-container>
<mat-drawer mode="side" opened> <mat-drawer mode="side" opened>
<p>Drawer</p> <mat-form-field id="tag-search" appearance="fill">
<mat-chip-list #chipList>
<mat-chip *ngFor="let tag of searchTags" (removed)="removeSearchTag(tag)" [removable]="true">
{{tag}}
<button matChipRemove>
<mat-icon>cancel</mat-icon>
</button>
</mat-chip>
</mat-chip-list>
<input matInput
[matChipInputFor]="chipList"
[matChipInputAddOnBlur]="true"
[matChipInputSeparatorKeyCodes]="searchInputSeparators"
(matChipInputTokenEnd)="addSearchTag($event)"/>
</mat-form-field>
<h1>Tags</h1>
<mat-selection-list [multiple]="false">
<mat-list-option
*ngFor="let tag of tags">{{tag.namespace ? tag.namespace + ':' + tag.name : tag.name}}</mat-list-option>
</mat-selection-list>
</mat-drawer> </mat-drawer>
<mat-drawer-content> <mat-drawer-content>
<app-file-grid (fileDblClickEvent)="openFile($event)" [files]="files"></app-file-grid> <app-file-grid (fileDblClickEvent)="openFile($event)" [files]="files"
(fileSelectEvent)="onFileSelect($event)"></app-file-grid>
</mat-drawer-content> </mat-drawer-content>
</mat-drawer-container> </mat-drawer-container>

@ -8,6 +8,10 @@
overflow: hidden overflow: hidden
} }
#tag-search {
width: 100%;
}
mat-drawer { mat-drawer {
height: 100%; height: 100%;
width: 25%; width: 25%;

@ -5,6 +5,10 @@ import {PageEvent} from "@angular/material/paginator";
import {Lightbox, LIGHTBOX_EVENT, LightboxEvent} from "ngx-lightbox"; import {Lightbox, LIGHTBOX_EVENT, LightboxEvent} from "ngx-lightbox";
import {SafeResourceUrl} from "@angular/platform-browser"; import {SafeResourceUrl} from "@angular/platform-browser";
import {ErrorBrokerService} from "../../services/error-broker/error-broker.service"; import {ErrorBrokerService} from "../../services/error-broker/error-broker.service";
import {TagService} from "../../services/tag/tag.service";
import {Tag} from "../../models/Tag";
import {MatChipInputEvent} from "@angular/material/chips";
import {COMMA, ENTER} from "@angular/cdk/keycodes";
@Component({ @Component({
selector: 'app-home', selector: 'app-home',
@ -14,15 +18,56 @@ import {ErrorBrokerService} from "../../services/error-broker/error-broker.servi
export class HomeComponent implements OnInit { export class HomeComponent implements OnInit {
files: File[] = []; files: File[] = [];
tags: Tag[] = [];
searchTags: string[] = [];
private openingLightbox = false; private openingLightbox = false;
searchInputSeparators = [ENTER, COMMA];
constructor(private errorBroker: ErrorBrokerService, private fileService: FileService, private lightbox: Lightbox, private lightboxEvent: LightboxEvent) { } constructor(
private errorBroker: ErrorBrokerService,
private fileService: FileService,
private tagService: TagService,
private lightbox: Lightbox,
private lightboxEvent: LightboxEvent) { }
async ngOnInit() { async ngOnInit() {
this.fileService.displayedFiles.subscribe((files) => this.files = files); this.fileService.displayedFiles.subscribe((files) => this.files = files);
await this.fileService.getFiles(); await this.fileService.getFiles();
} }
async onFileSelect(file: File | undefined) {
if (file) {
await this.showFileDetails(file);
} else {
this.clearFileDetails();
}
}
clearFileDetails() {
this.tags = [];
}
async showFileDetails(file: File) {
this.tags = await this.tagService.getTagsForFile(file.hash);
}
async removeSearchTag(tag: string) {
const index = this.searchTags.indexOf(tag);
if (index >= 0) {
this.searchTags.splice(index, 1);
}
await this.fileService.findFiles(this.searchTags);
}
async addSearchTag(event: MatChipInputEvent) {
const tag = event.value.trim();
if (tag.length > 0) {
this.searchTags.push(tag);
event.chipInput?.clear();
await this.fileService.findFiles(this.searchTags);
}
}
async openFile(file: File) { async openFile(file: File) {
if (this.openingLightbox) { if (this.openingLightbox) {
return; return;

@ -21,6 +21,11 @@ export class FileService {
this.displayedFiles.next(all_files); this.displayedFiles.next(all_files);
} }
public async findFiles(tags: string[]) {
let files = await invoke<File[]>("find_files", {tags});
this.displayedFiles.next(files);
}
public async readFile(hash: string, mime_type: string): Promise<SafeResourceUrl> { public async readFile(hash: string, mime_type: string): Promise<SafeResourceUrl> {
const data = await invoke<number[]>("read_file_by_hash", {hash}); const data = await invoke<number[]>("read_file_by_hash", {hash});
const blob = new Blob([new Uint8Array(data)], {type: mime_type}); const blob = new Blob([new Uint8Array(data)], {type: mime_type});

@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { TagService } from './tag.service';
describe('TagService', () => {
let service: TagService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(TagService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

@ -0,0 +1,15 @@
import { Injectable } from '@angular/core';
import {invoke} from "@tauri-apps/api/tauri";
import {Tag} from "../../models/Tag";
@Injectable({
providedIn: 'root'
})
export class TagService {
constructor() { }
public async getTagsForFile(hash: string): Promise<Tag[]> {
return await invoke<Tag[]>("get_tags_for_file", {hash});
}
}
Loading…
Cancel
Save