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 = [
"directories",
"mediarepo",
"rmp-ipc 0.4.3",
"rmp-ipc",
"serde",
"serde_json",
"tauri",
@ -1840,7 +1840,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "mediarepo"
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 = [
"log",
"mediarepo-core",
@ -1851,14 +1851,14 @@ dependencies = [
[[package]]
name = "mediarepo-core"
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 = [
"base64",
"futures",
"image",
"multibase",
"multihash",
"rmp-ipc 0.6.0",
"rmp-ipc",
"sea-orm",
"serde",
"sqlx",
@ -1871,7 +1871,7 @@ dependencies = [
[[package]]
name = "mediarepo-database"
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 = [
"chrono",
"mediarepo-core",
@ -1882,7 +1882,7 @@ dependencies = [
[[package]]
name = "mediarepo-model"
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 = [
"chrono",
"mediarepo-core",
@ -1898,7 +1898,7 @@ dependencies = [
[[package]]
name = "mediarepo-socket"
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 = [
"chrono",
"mediarepo-core",
@ -2927,24 +2927,9 @@ dependencies = [
[[package]]
name = "rmp-ipc"
version = "0.4.3"
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"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbd5082cefa9a4407327087d75dfadb5bf2d6f07168d0b609a2509f54fa3b243"
checksum = "17765aa8bd4d19dd81c53c0707192115f36ec200aea9f4557526932ac1f418e0"
dependencies = [
"lazy_static",
"log",

@ -18,7 +18,7 @@ tauri-build = { version = "1.0.0-beta.4" }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.0.0-beta.8", features = ["api-all"] }
rmp-ipc = "0.4.3"
rmp-ipc = "0.7.0"
thiserror = "1.0.30"
directories = "4.0.1"
toml = "0.5.8"
@ -30,7 +30,7 @@ features = ["fs", "io-std", "io-util"]
[dependencies.mediarepo]
git = "https://github.com/Trivernis/mediarepo-daemon"
rev = "57308089d58706ee1438f6b55d53691e9e08ce6b"
rev = "ce93a5793348a05052b324562730d97f5c6e2128"
features = ["library"]
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 crate::context::Context;
@ -6,47 +7,83 @@ use crate::error::{AppError, AppResult};
#[tauri::command]
pub async fn get_all_files(context: tauri::State<'_, Context>) -> AppResult<Vec<FileResponse>> {
let ipc = context.ipc.read().await;
if let Some(ipc) = &*ipc {
let response = ipc.emitter.emit_to("files", "all_files", ()).await?.await_reply(&ipc).await?;
let ipc = get_ipc(context).await?;
let response = ipc
.emitter
.emit_to("files", "all_files", ())
.await?
.await_reply(&ipc)
.await?;
Ok(response.data::<Vec<FileResponse>>()?)
} else {
Err(AppError::new("No ipc connection."))
}
}
#[tauri::command]
pub async fn read_file_by_hash(hash: String, context: tauri::State<'_, Context>) -> AppResult<Vec<u8>> {
let ipc = context.ipc.read().await;
if let Some(ipc) = &*ipc {
let response = ipc.emitter.emit_to("files", "read_file", ReadFileRequest::Hash(hash)).await?.await_reply(&ipc).await?;
pub async fn find_files(
tags: Vec<String>,
context: tauri::State<'_, Context>,
) -> 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>>()?)
} else {
Err(AppError::new("No ipc connection."))
}
#[tauri::command]
pub async fn read_file_by_hash(
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]
pub async fn get_thumbnails(hash: String, context: tauri::State<'_, Context>) -> AppResult<Vec<ThumbnailResponse>> {
let ipc = context.ipc.read().await;
if let Some(ipc) = &*ipc {
let response = ipc.emitter.emit_to("files", "get_thumbnails", GetFileThumbnailsRequest::Hash(hash)).await?.await_reply(&ipc).await?;
pub async fn get_thumbnails(
hash: String,
context: tauri::State<'_, Context>,
) -> 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>>()?)
} else {
Err(AppError::new("No ipc connection."))
}
}
#[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;
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 {
Err(AppError::new("No ipc connection."))
}

@ -1,8 +1,11 @@
use rmp_ipc::ipc::context::Context as IPCContext;
use crate::context::Context;
use crate::error::AppResult;
use crate::error::{AppError, AppResult};
pub mod repo;
pub mod files;
pub mod repo;
pub mod tags;
#[tauri::command]
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(())
}
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::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::settings::save_settings;
use rmp_ipc::ipc::context::Context as IPCContext;
use serde::{Deserialize, Serialize};
use std::mem;
use std::path::PathBuf;
use tauri::Window;
use tokio::fs;
static REPO_CONFIG_FILE: &str = "repo.toml";
@ -33,13 +33,19 @@ pub async fn get_repositories(context: tauri::State<'_, Context>) -> AppResult<V
}
#[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;
Ok(repo.clone())
}
#[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 path = PathBuf::from(path);
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]
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 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 mut ipc_ctx = context.ipc.write().await;
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::settings::Settings;
use rmp_ipc::ipc::context::Context as IPCContext;
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone)]
pub struct Context {
pub active_repository: Arc<RwLock<Option<Repository>>>,
pub ipc: Arc<RwLock<Option<IPCContext>>>,
pub settings: Arc<RwLock<Settings>>
pub settings: Arc<RwLock<Settings>>,
}
impl Context {
@ -16,7 +16,7 @@ impl Context {
Self {
ipc: 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;
use crate::error::AppResult;
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_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 typemap_rev::TypeMapKey;
use crate::error::AppResult;
pub struct WindowKey;
@ -19,27 +20,32 @@ pub async fn build_ipc_context(window: Window, address: &str) -> AppResult<IPCCo
let ctx = IPCBuilder::new()
.address(address)
.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)))
.build_client().await?;
.build_client()
.await?;
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 data = ctx.data.read().await;
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(())
}
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 data = ctx.data.read().await;
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(())
}

@ -3,17 +3,20 @@
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::files::*;
use crate::commands::repo::{
add_repository, get_active_repository, get_repositories, select_repository,
};
use crate::commands::tags::*;
use crate::context::Context;
use crate::settings::load_settings;
mod commands;
pub mod context;
pub mod error;
mod settings;
mod ipc;
mod settings;
fn main() {
let settings = load_settings().expect("Failed to load settings");
@ -21,7 +24,19 @@ fn main() {
tauri::Builder::default()
.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!())
.expect("error while running tauri application");
}

@ -24,6 +24,8 @@ import {MatProgressBarModule} from "@angular/material/progress-bar";
import {MatPaginatorModule} from "@angular/material/paginator";
import {ScrollingModule} from "@angular/cdk/scrolling";
import {LightboxModule} from "ngx-lightbox";
import {MatChipsModule} from "@angular/material/chips";
import {MatIconModule} from "@angular/material/icon";
@NgModule({
declarations: [
@ -52,7 +54,9 @@ import {LightboxModule} from "ngx-lightbox";
MatProgressBarModule,
MatPaginatorModule,
ScrollingModule,
LightboxModule
LightboxModule,
MatChipsModule,
MatIconModule
],
providers: [],
bootstrap: [AppComponent]

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

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

@ -5,6 +5,10 @@ import {PageEvent} from "@angular/material/paginator";
import {Lightbox, LIGHTBOX_EVENT, LightboxEvent} from "ngx-lightbox";
import {SafeResourceUrl} from "@angular/platform-browser";
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({
selector: 'app-home',
@ -14,15 +18,56 @@ import {ErrorBrokerService} from "../../services/error-broker/error-broker.servi
export class HomeComponent implements OnInit {
files: File[] = [];
tags: Tag[] = [];
searchTags: string[] = [];
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() {
this.fileService.displayedFiles.subscribe((files) => this.files = files);
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) {
if (this.openingLightbox) {
return;

@ -21,6 +21,11 @@ export class FileService {
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> {
const data = await invoke<number[]>("read_file_by_hash", {hash});
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