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(
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
all(not(debug_assertions), target_os = "windows"),
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use tracing_subscriber::fmt::format::FmtSpan;
|
||||
use tauri::{LogicalSize, Size};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use tracing_subscriber::fmt::format::FmtSpan;
|
||||
|
||||
fn main() {
|
||||
tracing_subscriber::fmt::SubscriberBuilder::default()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stdout)
|
||||
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
|
||||
.compact()
|
||||
.init();
|
||||
mediarepo_api::tauri_plugin::register_plugin(tauri::Builder::default())
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
tracing_subscriber::fmt::SubscriberBuilder::default()
|
||||
.with_env_filter(EnvFilter::from_default_env())
|
||||
.with_writer(std::io::stdout)
|
||||
.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE)
|
||||
.compact()
|
||||
.init();
|
||||
mediarepo_api::tauri_plugin::register_plugin(tauri::Builder::default())
|
||||
.on_page_load(|window, _| {
|
||||
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"
|
||||
| "CalculateSizes"
|
||||
| "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>
|
||||
<mat-drawer disableClose="true" mode="side" opened>
|
||||
<app-import-tab-sidebar (fileImported)="this.addFileFromImport($event)"
|
||||
(importFinished)="this.refreshFileView()"
|
||||
[selectedFiles]="selectedFiles"></app-import-tab-sidebar>
|
||||
</mat-drawer>
|
||||
<mat-drawer-content>
|
||||
<app-drawer-page>
|
||||
<app-drawer-page-side>
|
||||
<app-import-tab-sidebar
|
||||
[selectedFiles]="selectedFiles"
|
||||
[state]="this.state"></app-import-tab-sidebar>
|
||||
</app-drawer-page-side>
|
||||
<app-drawer-page-content>
|
||||
<app-file-multiview (fileSelectEvent)="this.onFileSelect($event)"
|
||||
(modeChangeEvent)="this.state.mode.next($event)"
|
||||
[files]="this.files"
|
||||
[mode]="this.state.mode.value"
|
||||
[preselectedFile]="this.getSelectedFileFromState()"></app-file-multiview>
|
||||
</mat-drawer-content>
|
||||
</mat-drawer-container>
|
||||
</app-drawer-page-content>
|
||||
</app-drawer-page>
|
||||
|
@ -1,5 +1,14 @@
|
||||
<ng-content></ng-content>
|
||||
<div *ngIf="this.busy" [class.blur]="this.blurBackground" [class.darken]="this.darkenBackground"
|
||||
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>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<div #imageContainer (window:resize)="this.adjustSize(image, imageContainer)" class="image-container">
|
||||
<img #image (load)="this.adjustSize(image, imageContainer)" [class.scale-height]="(!scaleWidth) && maximizeHeight"
|
||||
[class.scale-width]="scaleWidth && maximizeWidth" [src]="this.imageSrc"
|
||||
<img #image (error)="this.onImageLoadError($event, image)" (load)="this.onImageLoad(image, imageContainer)"
|
||||
[class.scale-height]="(!scaleWidth) && maximizeHeight" [class.scale-width]="scaleWidth && maximizeWidth"
|
||||
[src]="this.imageSrc"
|
||||
[style]="{borderRadius: this.borderRadius}" alt="">
|
||||
</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