commit
6f39780430
@ -0,0 +1,15 @@
|
|||||||
|
version = 1
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "javascript"
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
plugins = ["angular"]
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "rust"
|
||||||
|
enabled = true
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
msrv = "1.30.0"
|
@ -0,0 +1,158 @@
|
|||||||
|
use crate::tauri_plugin::error::{PluginError, PluginResult};
|
||||||
|
use futures::future;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::{Debug, Formatter};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
use std::{mem, thread};
|
||||||
|
use tokio::sync::{Mutex, RwLock};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct TaskContext {
|
||||||
|
tasks: Arc<RwLock<HashMap<String, AsyncTask>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaskContext {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
tasks: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn add_task<S: ToString, F: 'static + Future<Output = PluginResult<()>>>(
|
||||||
|
&self,
|
||||||
|
name: S,
|
||||||
|
task: F,
|
||||||
|
) {
|
||||||
|
self.tasks
|
||||||
|
.write()
|
||||||
|
.await
|
||||||
|
.insert(name.to_string(), AsyncTask::new(task));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn task_state<S: AsRef<str>>(&self, name: S) -> Option<TaskState> {
|
||||||
|
let state = {
|
||||||
|
let tasks = self.tasks.read().await;
|
||||||
|
|
||||||
|
if let Some(task) = tasks.get(name.as_ref()) {
|
||||||
|
Some(task.state().await)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if let Some(TaskState::Finished) = state {
|
||||||
|
self.tasks.write().await.remove(name.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all tasks queued for execution
|
||||||
|
async fn queued_tasks(&self) -> Vec<AsyncTask> {
|
||||||
|
let task_map = self.tasks.read().await;
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
|
||||||
|
for task in task_map.values() {
|
||||||
|
if task.state().await == TaskState::Queued {
|
||||||
|
tasks.push(task.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
|
||||||
|
pub enum TaskState {
|
||||||
|
Queued,
|
||||||
|
Running,
|
||||||
|
Finished,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TaskState {
|
||||||
|
pub fn error(&self) -> bool {
|
||||||
|
*self == TaskState::Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct AsyncTask {
|
||||||
|
state: Arc<RwLock<TaskState>>,
|
||||||
|
inner: Arc<Mutex<Option<Pin<Box<dyn Future<Output = PluginResult<()>>>>>>>,
|
||||||
|
error: Arc<RwLock<Option<PluginError>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for AsyncTask {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "AsyncTask (state: {:?})", self.state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsyncTask {
|
||||||
|
pub fn new<F: 'static + Future<Output = PluginResult<()>>>(inner: F) -> Self {
|
||||||
|
Self {
|
||||||
|
state: Arc::new(RwLock::new(TaskState::Queued)),
|
||||||
|
inner: Arc::new(Mutex::new(Some(Box::pin(inner)))),
|
||||||
|
error: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn exec(&self) {
|
||||||
|
self.set_state(TaskState::Running).await;
|
||||||
|
|
||||||
|
let inner = self.inner.lock().await.take();
|
||||||
|
if let Some(task) = inner {
|
||||||
|
if let Err(e) = task.await {
|
||||||
|
let _ = mem::replace(&mut *self.error.write().await, Some(e));
|
||||||
|
self.set_state(TaskState::Error).await;
|
||||||
|
} else {
|
||||||
|
self.set_state(TaskState::Finished).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.set_state(TaskState::Finished).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn state(&self) -> TaskState {
|
||||||
|
self.state.read().await.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn set_state(&self, state: TaskState) {
|
||||||
|
let _ = mem::replace(&mut *self.state.write().await, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl Send for AsyncTask {}
|
||||||
|
unsafe impl Sync for AsyncTask {}
|
||||||
|
|
||||||
|
pub fn start_background_task_runtime(ctx: TaskContext) {
|
||||||
|
thread::spawn(move || {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.thread_name("background_tasks")
|
||||||
|
.enable_time()
|
||||||
|
.build()
|
||||||
|
.expect("failed to build background task runtime")
|
||||||
|
.block_on(async move {
|
||||||
|
tracing::debug!("background task listener ready");
|
||||||
|
loop {
|
||||||
|
let tasks = ctx.queued_tasks().await;
|
||||||
|
|
||||||
|
if tasks.len() > 0 {
|
||||||
|
tracing::debug!("executing {} async background tasks", tasks.len());
|
||||||
|
let start = SystemTime::now();
|
||||||
|
future::join_all(tasks.iter().map(|t| t.exec())).await;
|
||||||
|
tracing::debug!(
|
||||||
|
"background tasks executed in {} ms",
|
||||||
|
start.elapsed().unwrap().as_millis()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tracing::error!("background task executor exited!");
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
use crate::dao::job::JobDao;
|
||||||
|
use crate::dao::DaoProvider;
|
||||||
|
use mediarepo_core::error::RepoResult;
|
||||||
|
use mediarepo_core::futures;
|
||||||
|
use mediarepo_core::thumbnailer::ThumbnailSize;
|
||||||
|
|
||||||
|
impl JobDao {
|
||||||
|
/// Generates thumbnails for files that are still missing some
|
||||||
|
#[tracing::instrument(level = "debug", skip(self))]
|
||||||
|
pub async fn generate_missing_thumbnails(&self) -> RepoResult<()> {
|
||||||
|
let file_dao = self.file();
|
||||||
|
let files = file_dao.all().await?;
|
||||||
|
let mut missing_thumbnails = Vec::new();
|
||||||
|
|
||||||
|
for file in files {
|
||||||
|
if file_dao.thumbnails(file.encoded_cd()).await?.is_empty() {
|
||||||
|
missing_thumbnails.push(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
futures::future::join_all(missing_thumbnails.into_iter().map(|f| async {
|
||||||
|
let file = f;
|
||||||
|
file_dao
|
||||||
|
.create_thumbnails(&file, vec![ThumbnailSize::Medium])
|
||||||
|
.await
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -1,19 +1,24 @@
|
|||||||
#![cfg_attr(
|
#![cfg_attr(
|
||||||
all(not(debug_assertions), target_os = "windows"),
|
all(not(debug_assertions), target_os = "windows"),
|
||||||
windows_subsystem = "windows"
|
windows_subsystem = "windows"
|
||||||
)]
|
)]
|
||||||
|
|
||||||
use tracing_subscriber::fmt::format::FmtSpan;
|
use tauri::{LogicalSize, Size};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
use tracing_subscriber::fmt::format::FmtSpan;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
tracing_subscriber::fmt::SubscriberBuilder::default()
|
tracing_subscriber::fmt::SubscriberBuilder::default()
|
||||||
.with_env_filter(EnvFilter::from_default_env())
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
.with_writer(std::io::stdout)
|
.with_writer(std::io::stdout)
|
||||||
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
|
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
|
||||||
.compact()
|
.compact()
|
||||||
.init();
|
.init();
|
||||||
mediarepo_api::tauri_plugin::register_plugin(tauri::Builder::default())
|
mediarepo_api::tauri_plugin::register_plugin(tauri::Builder::default())
|
||||||
.run(tauri::generate_context!())
|
.on_page_load(|window, _| {
|
||||||
.expect("error while running tauri application");
|
window.set_title(format!("mediarepo {}", env!("CARGO_PKG_VERSION")).as_str()).expect("failed to set window title");
|
||||||
|
window.set_min_size(Some(Size::Logical(LogicalSize { width: 1000.0, height: 750.0 }))).expect("failed to set minimal size");
|
||||||
|
})
|
||||||
|
.run(tauri::generate_context!())
|
||||||
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
export type JobType = "MigrateContentDescriptors"
|
export type JobType = "MigrateContentDescriptors"
|
||||||
| "CalculateSizes"
|
| "CalculateSizes"
|
||||||
| "CheckIntegrity"
|
| "CheckIntegrity"
|
||||||
| "Vacuum";
|
| "Vacuum"
|
||||||
|
| "GenerateThumbnails";
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
@use 'sass:map';
|
|
||||||
@use '@angular/material' as mat;
|
|
||||||
|
|
||||||
@mixin color($theme) {
|
|
||||||
$color-config: mat.get-color-config($theme);
|
|
||||||
$primary-palette: map.get($color-config, 'primary');
|
|
||||||
$warn-palette: map.get($color-config, 'warn');
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: darken(#303030, 5);
|
|
||||||
color: white
|
|
||||||
}
|
|
||||||
|
|
||||||
.warn {
|
|
||||||
background-color: mat.get-color-from-palette($warn-palette);
|
|
||||||
color: white
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin typography($theme) {
|
|
||||||
// Get the typography config from the theme.
|
|
||||||
$typography-config: mat.get-typography-config($theme);
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: mat.font-family($typography-config);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@mixin theme($theme) {
|
|
||||||
$color-config: mat.get-color-config($theme);
|
|
||||||
@if $color-config != null {
|
|
||||||
@include color($theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
$typography-config: mat.get-typography-config($theme);
|
|
||||||
@if $typography-config != null {
|
|
||||||
@include typography($theme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,7 @@
|
|||||||
|
<app-middle-centered>
|
||||||
|
<h1>What kind of tab do you want to open?</h1>
|
||||||
|
<div class="button-container">
|
||||||
|
<button (click)="this.addTab('files')" color="primary" mat-flat-button>Files</button>
|
||||||
|
<button (click)="this.addTab('import')" color="primary" mat-flat-button>Import</button>
|
||||||
|
</div>
|
||||||
|
</app-middle-centered>
|
@ -0,0 +1,31 @@
|
|||||||
|
@import "src/colors";
|
||||||
|
|
||||||
|
:host {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
background: radial-gradient(circle, $background-darker-05 80%, $primary 200%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-container {
|
||||||
|
height: 6em;
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin: 1em;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
transition-duration: 0.25s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
scale: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
scale: 1;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||||
|
|
||||||
|
import {EmptyTabComponent} from "./empty-tab.component";
|
||||||
|
|
||||||
|
describe("EmptyTabComponent", () => {
|
||||||
|
let component: EmptyTabComponent;
|
||||||
|
let fixture: ComponentFixture<EmptyTabComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [EmptyTabComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(EmptyTabComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,29 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, EventEmitter, Output} from "@angular/core";
|
||||||
|
import {TabCategory} from "../../../models/state/TabCategory";
|
||||||
|
|
||||||
|
type TabCategoryName = "files" | "import";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-empty-tab",
|
||||||
|
templateUrl: "./empty-tab.component.html",
|
||||||
|
styleUrls: ["./empty-tab.component.scss"],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class EmptyTabComponent {
|
||||||
|
|
||||||
|
@Output() tabCategorySelect = new EventEmitter<TabCategory>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public addTab(category: TabCategoryName) {
|
||||||
|
switch (category) {
|
||||||
|
case "files":
|
||||||
|
this.tabCategorySelect.emit(TabCategory.Files);
|
||||||
|
break;
|
||||||
|
case "import":
|
||||||
|
this.tabCategorySelect.emit(TabCategory.Import);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
<mat-drawer-container autosize>
|
<app-drawer-page>
|
||||||
<mat-drawer disableClose="true" mode="side" opened>
|
<app-drawer-page-side>
|
||||||
<app-import-tab-sidebar (fileImported)="this.addFileFromImport($event)"
|
<app-import-tab-sidebar
|
||||||
(importFinished)="this.refreshFileView()"
|
[selectedFiles]="selectedFiles"
|
||||||
[selectedFiles]="selectedFiles"></app-import-tab-sidebar>
|
[state]="this.state"></app-import-tab-sidebar>
|
||||||
</mat-drawer>
|
</app-drawer-page-side>
|
||||||
<mat-drawer-content>
|
<app-drawer-page-content>
|
||||||
<app-file-multiview (fileSelectEvent)="this.onFileSelect($event)"
|
<app-file-multiview (fileSelectEvent)="this.onFileSelect($event)"
|
||||||
(modeChangeEvent)="this.state.mode.next($event)"
|
(modeChangeEvent)="this.state.mode.next($event)"
|
||||||
[files]="this.files"
|
[files]="this.files"
|
||||||
[mode]="this.state.mode.value"
|
[mode]="this.state.mode.value"
|
||||||
[preselectedFile]="this.getSelectedFileFromState()"></app-file-multiview>
|
[preselectedFile]="this.getSelectedFileFromState()"></app-file-multiview>
|
||||||
</mat-drawer-content>
|
</app-drawer-page-content>
|
||||||
</mat-drawer-container>
|
</app-drawer-page>
|
||||||
|
@ -1,5 +1,14 @@
|
|||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
<div *ngIf="this.busy" [class.blur]="this.blurBackground" [class.darken]="this.darkenBackground"
|
<div *ngIf="this.busy" [class.blur]="this.blurBackground" [class.darken]="this.darkenBackground"
|
||||||
class="busy-indicator-overlay">
|
class="busy-indicator-overlay">
|
||||||
<mat-progress-spinner *ngIf="this.busy" [mode]="mode" [value]="value" color="primary"></mat-progress-spinner>
|
<mat-progress-spinner *ngIf="indicatorType === 'spinner' && this.busy"
|
||||||
|
[mode]="mode"
|
||||||
|
[value]="value"
|
||||||
|
color="primary"></mat-progress-spinner>
|
||||||
|
|
||||||
|
<app-middle-centered *ngIf="indicatorType === 'pulse' && this.busy">
|
||||||
|
<div class="loading-indicator-pulse-outer">
|
||||||
|
<div class="loading-indicator-pulse-inner"></div>
|
||||||
|
</div>
|
||||||
|
</app-middle-centered>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<div #imageContainer (window:resize)="this.adjustSize(image, imageContainer)" class="image-container">
|
<div #imageContainer (window:resize)="this.adjustSize(image, imageContainer)" class="image-container">
|
||||||
<img #image (load)="this.adjustSize(image, imageContainer)" [class.scale-height]="(!scaleWidth) && maximizeHeight"
|
<img #image (error)="this.onImageLoadError($event, image)" (load)="this.onImageLoad(image, imageContainer)"
|
||||||
[class.scale-width]="scaleWidth && maximizeWidth" [src]="this.imageSrc"
|
[class.scale-height]="(!scaleWidth) && maximizeHeight" [class.scale-width]="scaleWidth && maximizeWidth"
|
||||||
|
[src]="this.imageSrc"
|
||||||
[style]="{borderRadius: this.borderRadius}" alt="">
|
[style]="{borderRadius: this.borderRadius}" alt="">
|
||||||
</div>
|
</div>
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
<ng-content></ng-content>
|
@ -0,0 +1,25 @@
|
|||||||
|
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||||
|
|
||||||
|
import {DrawerPageContentComponent} from "./drawer-page-content.component";
|
||||||
|
|
||||||
|
describe("DrawerPageContentComponent", () => {
|
||||||
|
let component: DrawerPageContentComponent;
|
||||||
|
let fixture: ComponentFixture<DrawerPageContentComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [DrawerPageContentComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DrawerPageContentComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,13 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-drawer-page-content",
|
||||||
|
templateUrl: "./drawer-page-content.component.html",
|
||||||
|
styleUrls: ["./drawer-page-content.component.scss"],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class DrawerPageContentComponent {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
<ng-content></ng-content>
|
@ -0,0 +1,25 @@
|
|||||||
|
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||||
|
|
||||||
|
import {DrawerPageSideComponent} from "./drawer-page-side.component";
|
||||||
|
|
||||||
|
describe("DrawerPageSideComponent", () => {
|
||||||
|
let component: DrawerPageSideComponent;
|
||||||
|
let fixture: ComponentFixture<DrawerPageSideComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [DrawerPageSideComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DrawerPageSideComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,13 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-drawer-page-side",
|
||||||
|
templateUrl: "./drawer-page-side.component.html",
|
||||||
|
styleUrls: ["./drawer-page-side.component.scss"],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class DrawerPageSideComponent {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,13 @@
|
|||||||
|
<mat-drawer-container autosize>
|
||||||
|
<mat-drawer [opened]="this.drawerOpened" disableClose mode="side">
|
||||||
|
<ng-content select="app-drawer-page-side"></ng-content>
|
||||||
|
</mat-drawer>
|
||||||
|
<mat-drawer-content>
|
||||||
|
<ng-content select="app-drawer-page-content"></ng-content>
|
||||||
|
|
||||||
|
<app-flap-button (appClick)="this.toggleDrawer()" align="center" attach="left">
|
||||||
|
<ng-icon *ngIf="!this.drawerOpened" name="mat-chevron-right"></ng-icon>
|
||||||
|
<ng-icon *ngIf="this.drawerOpened" name="mat-chevron-left"></ng-icon>
|
||||||
|
</app-flap-button>
|
||||||
|
</mat-drawer-content>
|
||||||
|
</mat-drawer-container>
|
@ -0,0 +1,39 @@
|
|||||||
|
@import "src/colors";
|
||||||
|
|
||||||
|
mat-drawer {
|
||||||
|
height: 100%;
|
||||||
|
width: 25%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-drawer-content {
|
||||||
|
overflow-x: hidden;
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
mat-drawer-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-side-inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drawer-side-top-bar {
|
||||||
|
background: $background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-button {
|
||||||
|
height: 2em;
|
||||||
|
float: right;
|
||||||
|
|
||||||
|
ng-icon {
|
||||||
|
margin-top: -0.5em;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||||
|
|
||||||
|
import {DrawerPageComponent} from "./drawer-page.component";
|
||||||
|
|
||||||
|
describe("DrawerPageComponent", () => {
|
||||||
|
let component: DrawerPageComponent;
|
||||||
|
let fixture: ComponentFixture<DrawerPageComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [DrawerPageComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(DrawerPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,23 @@
|
|||||||
|
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, EventEmitter, Output} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-drawer-page",
|
||||||
|
templateUrl: "./drawer-page.component.html",
|
||||||
|
styleUrls: ["./drawer-page.component.scss"],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class DrawerPageComponent {
|
||||||
|
|
||||||
|
public drawerOpened = true;
|
||||||
|
|
||||||
|
@Output() appSizeChange = new EventEmitter<void>();
|
||||||
|
|
||||||
|
constructor(private changeDetecter: ChangeDetectorRef) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleDrawer(): void {
|
||||||
|
this.drawerOpened = !this.drawerOpened;
|
||||||
|
this.appSizeChange.emit();
|
||||||
|
this.changeDetecter.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
<div (click)="this.appClick.emit($event)"
|
||||||
|
[className]="'flap-' + this.attach + ' flap-' + this.align + ' flap-button'"
|
||||||
|
matRipple>
|
||||||
|
<ng-content></ng-content>
|
||||||
|
</div>
|
@ -0,0 +1,167 @@
|
|||||||
|
@import "src/colors";
|
||||||
|
|
||||||
|
:host {
|
||||||
|
position: absolute;
|
||||||
|
transition-duration: 0.5s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
& > .flap-top, & > .flap-bottom {
|
||||||
|
height: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .flap-left, & > .flap-right {
|
||||||
|
width: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='left'], &[attach='right'] {
|
||||||
|
top: calc(50% - 5em);
|
||||||
|
height: 10em;
|
||||||
|
width: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='top'], &[attach='bottom'] {
|
||||||
|
left: calc(50% - 5em);
|
||||||
|
width: 10em;
|
||||||
|
height: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='left'] {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='right'] {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='top'] {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='bottom'] {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='left'], &[attach='top'][align='start'], &[attach='bottom'][align='start'] {
|
||||||
|
.flap-button {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='right'], &[attach='top'][align='end'], &[attach='bottom'][align='end'] {
|
||||||
|
.flap-button {
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='top'], &[attach='left'][align='start'], &[attach='right'][align='start'] {
|
||||||
|
.flap-button {
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='bottom'], &[attach='left'][align='end'], &[attach='right'][align='end'] {
|
||||||
|
.flap-button {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='left'][align='center'], &[attach='right'][align='center'] {
|
||||||
|
.flap-button {
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&[attach='top'][align='center'], &[attach='bottom'][align='center'] {
|
||||||
|
.flap-button {
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flap-button {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
opacity: 0.7;
|
||||||
|
background: $accent-darker-10;
|
||||||
|
text-align: center;
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $accent;
|
||||||
|
opacity: 0.9;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
::ng-deep ng-icon {
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.flap-top, .flap-bottom {
|
||||||
|
width: 4em;
|
||||||
|
height: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flap-button.flap-top:hover, .flap-button.flap-bottom:hover {
|
||||||
|
width: 10em;
|
||||||
|
height: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flap-left, .flap-right {
|
||||||
|
width: 2px;
|
||||||
|
height: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flap-button.flap-left:hover, .flap-button.flap-right:hover {
|
||||||
|
width: 2em;
|
||||||
|
height: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
$flap-border-radius: 2em;
|
||||||
|
|
||||||
|
.flap-start.flap-left, .flap-start.flap-top {
|
||||||
|
border-bottom-right-radius: $flap-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flap-start.flap-right, .flap-end.flap-top {
|
||||||
|
border-bottom-left-radius: $flap-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flap-end.flap-left, .flap-start.flap-bottom {
|
||||||
|
border-top-right-radius: $flap-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flap-end.flap-right, .flap-end.flap-bottom {
|
||||||
|
border-top-left-radius: $flap-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flap-center {
|
||||||
|
|
||||||
|
&.flap-left {
|
||||||
|
border-top-right-radius: $flap-border-radius;
|
||||||
|
border-bottom-right-radius: $flap-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.flap-right {
|
||||||
|
border-top-left-radius: $flap-border-radius;
|
||||||
|
border-bottom-left-radius: $flap-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.flap-top {
|
||||||
|
border-bottom-right-radius: $flap-border-radius;
|
||||||
|
border-bottom-left-radius: $flap-border-radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.flap-bottom {
|
||||||
|
border-top-right-radius: $flap-border-radius;
|
||||||
|
border-top-left-radius: $flap-border-radius;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
|||||||
|
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||||
|
|
||||||
|
import {FlapButtonComponent} from "./flap-button.component";
|
||||||
|
|
||||||
|
describe("FlapButtonComponent", () => {
|
||||||
|
let component: FlapButtonComponent;
|
||||||
|
let fixture: ComponentFixture<FlapButtonComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [FlapButtonComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(FlapButtonComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,20 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from "@angular/core";
|
||||||
|
|
||||||
|
export type Attachment = "top" | "bottom" | "left" | "right";
|
||||||
|
export type Alignment = "start" | "center" | "end";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-flap-button",
|
||||||
|
templateUrl: "./flap-button.component.html",
|
||||||
|
styleUrls: ["./flap-button.component.scss"],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class FlapButtonComponent {
|
||||||
|
|
||||||
|
@Input() attach: Attachment = "top";
|
||||||
|
@Input() align: Alignment = "center";
|
||||||
|
@Output() appClick = new EventEmitter<MouseEvent>();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
<ng-content></ng-content>
|
@ -0,0 +1,7 @@
|
|||||||
|
:host {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||||
|
|
||||||
|
import {MiddleCenteredComponent} from "./middle-centered.component";
|
||||||
|
|
||||||
|
describe("MiddleCenteredComponent", () => {
|
||||||
|
let component: MiddleCenteredComponent;
|
||||||
|
let fixture: ComponentFixture<MiddleCenteredComponent>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
declarations: [MiddleCenteredComponent]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MiddleCenteredComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create", () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,13 @@
|
|||||||
|
import {ChangeDetectionStrategy, Component} from "@angular/core";
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-middle-centered",
|
||||||
|
templateUrl: "./middle-centered.component.html",
|
||||||
|
styleUrls: ["./middle-centered.component.scss"],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class MiddleCenteredComponent {
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
import {FormatBytesPipe} from "./format-bytes.pipe";
|
||||||
|
|
||||||
|
describe("FormatBytesPipe", () => {
|
||||||
|
it("create an instance", () => {
|
||||||
|
const pipe = new FormatBytesPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,24 @@
|
|||||||
|
import {Pipe, PipeTransform} from "@angular/core";
|
||||||
|
|
||||||
|
@Pipe({
|
||||||
|
name: "formatBytes"
|
||||||
|
})
|
||||||
|
export class FormatBytesPipe implements PipeTransform {
|
||||||
|
|
||||||
|
static round(number: number, decimals: number) {
|
||||||
|
return Math.round(number * (10 ** decimals)) / (10 ** decimals);
|
||||||
|
}
|
||||||
|
|
||||||
|
transform(value: number): string {
|
||||||
|
const units = ["B", "KiB", "MiB", "GiB"];
|
||||||
|
let formattedValue = value;
|
||||||
|
|
||||||
|
for (const unit of units) {
|
||||||
|
if (formattedValue < 1000) {
|
||||||
|
return `${formattedValue} ${unit}`;
|
||||||
|
}
|
||||||
|
formattedValue = FormatBytesPipe.round(formattedValue / 1024, 2);
|
||||||
|
}
|
||||||
|
return formattedValue + " GiB";
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue