;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [CopyDialogComponent],
12 | });
13 | fixture = TestBed.createComponent(CopyDialogComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/copy-dialog/copy-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Inject } from '@angular/core';
2 |
3 | import { MAT_DIALOG_DATA } from '@angular/material/dialog';
4 |
5 | @Component({
6 | selector: 'app-copy-dialog',
7 | templateUrl: './copy-dialog.component.html',
8 | styleUrls: ['./copy-dialog.component.scss'],
9 | })
10 | export class CopyDialogComponent {
11 | constructor(
12 | @Inject(MAT_DIALOG_DATA)
13 | public data: {
14 | content: string;
15 | },
16 | ) {}
17 | }
18 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/delete-confirm-dialog/delete-confirm-dialog.component.html:
--------------------------------------------------------------------------------
1 | Delete
2 |
3 | Would you like to delete
4 | {{ data[0].Name }}
5 | 1" i18n>
6 | these
7 | {data.length, plural,
8 | one {{{data.length}} item}
9 | other {{{data.length}} items}
10 | }
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/delete-confirm-dialog/delete-confirm-dialog.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuudi/rclone-webui-angular/1407be6d51636b25be232900caea15243bc892b7/src/app/features/functions/explorer/explorer-viewer/delete-confirm-dialog/delete-confirm-dialog.component.scss
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/delete-confirm-dialog/delete-confirm-dialog.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { DeleteConfirmDialogComponent } from './delete-confirm-dialog.component';
4 |
5 | describe('DeleteConfirmDialogComponent', () => {
6 | let component: DeleteConfirmDialogComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [DeleteConfirmDialogComponent],
12 | });
13 | fixture = TestBed.createComponent(DeleteConfirmDialogComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/delete-confirm-dialog/delete-confirm-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Inject } from '@angular/core';
2 |
3 | import { MAT_DIALOG_DATA } from '@angular/material/dialog';
4 |
5 | import { DirectoryItem } from '../../explorer.model';
6 |
7 | @Component({
8 | selector: 'app-delete-confirm-dialog',
9 | templateUrl: './delete-confirm-dialog.component.html',
10 | styleUrls: ['./delete-confirm-dialog.component.scss'],
11 | })
12 | export class DeleteConfirmDialogComponent {
13 | constructor(@Inject(MAT_DIALOG_DATA) public data: DirectoryItem[]) {
14 | // const length = data.length;
15 | // if (length == 0) {
16 | // this.description = 'No files selected';
17 | // console.error('No files selected when delete confirm dialog is opened');
18 | // } else if (length === 1) {
19 | // this.description = data[0].Name;
20 | // } else {
21 | // this.description = $localize`${length, plural, few {{{length}} items} other {{{length}} items}}`;
22 | // }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/explorer-viewer.component.html:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 | Empty
10 |
11 |
12 |
13 |
22 |
23 |
24 | {{ item.Name }}
25 | {{ item.ModTime | date }}
26 |
27 |
28 |
29 |
37 |
38 |
39 |
40 |
44 |
48 |
52 |
56 |
60 |
68 |
76 |
77 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/explorer-viewer.component.scss:
--------------------------------------------------------------------------------
1 | .explorer-view__container {
2 | height: 65vh;
3 | overflow-y: scroll;
4 | }
5 |
6 | .explorer-view__list-option--hide-checkbox {
7 | ::ng-deep .mdc-list-item__end {
8 | display: none;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/explorer-viewer.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ExplorerViewerComponent } from './explorer-viewer.component';
4 |
5 | describe('ExplorerViewerComponent', () => {
6 | let component: ExplorerViewerComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ExplorerViewerComponent],
12 | });
13 | fixture = TestBed.createComponent(ExplorerViewerComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/file-icon.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { FileIconPipe } from './file-icon.pipe';
2 |
3 | describe('FileIconPipe', () => {
4 | it('create an instance', () => {
5 | const pipe = new FileIconPipe();
6 | expect(pipe).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/file-icon.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import { DirectoryItem } from '../explorer.model';
3 |
4 | @Pipe({
5 | name: 'fileIcon',
6 | })
7 | export class FileIconPipe implements PipeTransform {
8 | transform(value: DirectoryItem): string {
9 | if (value.IsDir) {
10 | return 'folder';
11 | }
12 | const mimeParts = value.MimeType.split('/');
13 | switch (mimeParts[0]) {
14 | case 'text':
15 | return 'description';
16 | case 'image':
17 | return 'image';
18 | case 'audio':
19 | return 'audio_file';
20 | case 'video':
21 | return 'video_file';
22 | case 'application':
23 | switch (mimeParts[1]) {
24 | case 'msword':
25 | case 'vnd.openxmlformats-officedocument.wordprocessingml.document':
26 | case 'vnd.ms-powerpoint':
27 | case 'vnd.openxmlformats-officedocument.presentationml.presentation':
28 | case 'vnd.ms-excel':
29 | case 'vnd.openxmlformats-officedocument.spreadsheetml.sheet':
30 | case 'pdf':
31 | return 'docs';
32 | case 'zip':
33 | case 'x-bzip':
34 | case 'x-bzip2':
35 | case 'x-tar':
36 | case 'gzip':
37 | case 'x-gzip':
38 | case 'vnd.rar':
39 | case 'x-7z-compressed':
40 | return 'folder_zip';
41 | case 'octet-stream':
42 | return 'insert_drive_file';
43 | default:
44 | return 'insert_drive_file';
45 | }
46 | default:
47 | return 'insert_drive_file';
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/path-splitter/path-splitter.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 | /
7 |
8 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/path-splitter/path-splitter.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuudi/rclone-webui-angular/1407be6d51636b25be232900caea15243bc892b7/src/app/features/functions/explorer/explorer-viewer/path-splitter/path-splitter.component.scss
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/path-splitter/path-splitter.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { PathSplitterComponent } from './path-splitter.component';
4 |
5 | describe('PathSplitterComponent', () => {
6 | let component: PathSplitterComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [PathSplitterComponent],
12 | });
13 | fixture = TestBed.createComponent(PathSplitterComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/path-splitter/path-splitter.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | EventEmitter,
4 | Input,
5 | OnChanges,
6 | OnInit,
7 | Output,
8 | } from '@angular/core';
9 |
10 | @Component({
11 | selector: 'app-path-splitter[path]',
12 | templateUrl: './path-splitter.component.html',
13 | styleUrls: ['./path-splitter.component.scss'],
14 | })
15 | export class PathSplitterComponent implements OnInit, OnChanges {
16 | @Input() path!: string;
17 | @Output() pathChange = new EventEmitter();
18 |
19 | pathParts: {
20 | name: string;
21 | fullPath: string;
22 | }[] = [];
23 |
24 | ngOnInit() {
25 | this.updatePath();
26 | }
27 |
28 | ngOnChanges() {
29 | this.updatePath();
30 | }
31 |
32 | updatePath() {
33 | this.pathParts = [];
34 | if (this.path === '') {
35 | return;
36 | }
37 | const paths = this.path.split('/');
38 | const fullPathList = [];
39 | for (const path of paths) {
40 | fullPathList.push(path);
41 | this.pathParts.push({
42 | name: path,
43 | fullPath: fullPathList.join('/'),
44 | });
45 | }
46 | }
47 |
48 | pathClicked(fullPath: string) {
49 | this.pathChange.emit(fullPath);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/rename-dialog/rename-dialog.component.html:
--------------------------------------------------------------------------------
1 | {{ data.title }}
2 |
3 |
4 | New name
5 |
6 |
7 | Name already exists
8 |
9 |
10 |
11 |
12 |
13 |
14 |
22 |
23 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/rename-dialog/rename-dialog.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuudi/rclone-webui-angular/1407be6d51636b25be232900caea15243bc892b7/src/app/features/functions/explorer/explorer-viewer/rename-dialog/rename-dialog.component.scss
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/rename-dialog/rename-dialog.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { RenameDialogComponent } from './rename-dialog.component';
4 |
5 | describe('RenameDialogComponent', () => {
6 | let component: RenameDialogComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [RenameDialogComponent],
12 | });
13 | fixture = TestBed.createComponent(RenameDialogComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer-viewer/rename-dialog/rename-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Inject } from '@angular/core';
2 |
3 | import { MAT_DIALOG_DATA } from '@angular/material/dialog';
4 |
5 | @Component({
6 | selector: 'app-rename-dialog',
7 | templateUrl: './rename-dialog.component.html',
8 | styleUrls: ['./rename-dialog.component.scss'],
9 | })
10 | export class RenameDialogComponent {
11 | constructor(
12 | @Inject(MAT_DIALOG_DATA)
13 | public data: {
14 | title: string;
15 | name: string;
16 | existNames: string[];
17 | },
18 | ) {}
19 | }
20 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer.component.scss:
--------------------------------------------------------------------------------
1 | .group-container {
2 | display: flex;
3 | flex-direction: row;
4 | > * {
5 | flex: 1;
6 | }
7 | }
8 |
9 | .group-card {
10 | margin: 1em;
11 | }
12 |
13 | .new-group-hint {
14 | display: flex;
15 | align-items: center;
16 | margin: 1em;
17 |
18 | .mat-icon {
19 | margin: 0.5em;
20 | }
21 | }
22 |
23 | .spacer {
24 | flex: 1 1 auto;
25 | }
26 |
27 | .icon--rotate90 {
28 | transform: rotate(90deg);
29 | }
30 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { ExplorerComponent } from './explorer.component';
4 |
5 | describe('ExplorerComponent', () => {
6 | let component: ExplorerComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ExplorerComponent],
12 | });
13 | fixture = TestBed.createComponent(ExplorerComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer.model.ts:
--------------------------------------------------------------------------------
1 | import { FsInfo } from '../backend/backend.model';
2 |
3 | export interface ExplorerView {
4 | backend: string;
5 | path: string;
6 | info: Promise;
7 | actions: {
8 | refresh?: () => void;
9 | getPath?: () => string;
10 | getChildren?: () => DirectoryItem[] | undefined;
11 | addChild?: (name: string, isFolder: boolean) => void;
12 | };
13 | }
14 |
15 | export interface AppClipboard {
16 | type: 'copy' | 'move';
17 | backend: string;
18 | items: DirectoryItem[];
19 | }
20 |
21 | export interface SyncClipboard {
22 | type: 'sync' | 'bisync';
23 | backend: string;
24 | dirPath: string;
25 | }
26 |
27 | export interface FileItem extends DirectoryItem {
28 | IsDir: false;
29 | }
30 |
31 | export interface DirItem extends DirectoryItem {
32 | IsDir: true;
33 | }
34 |
35 | export interface DirectoryItem {
36 | IsDir: boolean;
37 | Path: string;
38 | Name: string;
39 | Size: number;
40 | MimeType: string;
41 | ModTime: Date;
42 | }
43 |
44 | export type EmptyObj = Record;
45 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer.module.ts:
--------------------------------------------------------------------------------
1 | import { ClipboardModule } from '@angular/cdk/clipboard';
2 | import { TextFieldModule } from '@angular/cdk/text-field';
3 | import { CommonModule } from '@angular/common';
4 | import { NgModule } from '@angular/core';
5 | import { FormsModule } from '@angular/forms';
6 |
7 | import { MatBadgeModule } from '@angular/material/badge';
8 | import { MatButtonModule } from '@angular/material/button';
9 | import { MatCardModule } from '@angular/material/card';
10 | import { MatDialogModule } from '@angular/material/dialog';
11 | import { MatFormFieldModule } from '@angular/material/form-field';
12 | import { MatIconModule } from '@angular/material/icon';
13 | import { MatInputModule } from '@angular/material/input';
14 | import { MatListModule } from '@angular/material/list';
15 | import { MatMenuModule } from '@angular/material/menu';
16 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
17 | import { MatSnackBarModule } from '@angular/material/snack-bar';
18 | import { MatTabsModule } from '@angular/material/tabs';
19 | import { MatToolbarModule } from '@angular/material/toolbar';
20 | import { MatTooltipModule } from '@angular/material/tooltip';
21 |
22 | import { ExplorerRoutingModule } from './explorer-routing.module';
23 | import { CopyDialogComponent } from './explorer-viewer/copy-dialog/copy-dialog.component';
24 | import { DeleteConfirmDialogComponent } from './explorer-viewer/delete-confirm-dialog/delete-confirm-dialog.component';
25 | import { ExplorerViewerComponent } from './explorer-viewer/explorer-viewer.component';
26 | import { FileIconPipe } from './explorer-viewer/file-icon.pipe';
27 | import { PathSplitterComponent } from './explorer-viewer/path-splitter/path-splitter.component';
28 | import { RenameDialogComponent } from './explorer-viewer/rename-dialog/rename-dialog.component';
29 | import { ExplorerComponent } from './explorer.component';
30 |
31 | @NgModule({
32 | declarations: [
33 | ExplorerComponent,
34 | ExplorerViewerComponent,
35 | DeleteConfirmDialogComponent,
36 | PathSplitterComponent,
37 | RenameDialogComponent,
38 | CopyDialogComponent,
39 | FileIconPipe,
40 | ],
41 | imports: [
42 | CommonModule,
43 | FormsModule,
44 | ClipboardModule,
45 | TextFieldModule,
46 | ExplorerRoutingModule,
47 | MatIconModule,
48 | MatButtonModule,
49 | MatTabsModule,
50 | MatListModule,
51 | MatMenuModule,
52 | MatSnackBarModule,
53 | MatCardModule,
54 | MatToolbarModule,
55 | MatTooltipModule,
56 | MatProgressSpinnerModule,
57 | MatDialogModule,
58 | MatFormFieldModule,
59 | MatInputModule,
60 | MatBadgeModule,
61 | ],
62 | })
63 | export class ExplorerModule {}
64 |
--------------------------------------------------------------------------------
/src/app/features/functions/explorer/explorer.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { ExplorerService } from './explorer.service';
4 |
5 | describe('ExplorerService', () => {
6 | let service: ExplorerService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(ExplorerService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/features/functions/functions-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 |
4 | import { FunctionsComponent } from './functions.component';
5 | import { isElectronGuard } from './is-electron.guard';
6 |
7 | const routes: Routes = [
8 | { path: '', component: FunctionsComponent, pathMatch: 'full' },
9 | {
10 | path: 'drive',
11 | loadChildren: () =>
12 | import('./backend/backend.module').then((m) => m.BackendModule),
13 | },
14 | {
15 | path: 'explore',
16 | loadChildren: () =>
17 | import('./explorer/explorer.module').then((m) => m.ExplorerModule),
18 | },
19 | {
20 | path: 'mount',
21 | loadChildren: () =>
22 | import('./mount/mount.module').then((m) => m.MountModule),
23 | },
24 | {
25 | path: 'job',
26 | loadChildren: () => import('./job/job.module').then((m) => m.JobModule),
27 | },
28 | {
29 | path: 'cron',
30 | loadChildren: () => import('./cron/cron.module').then((m) => m.CronModule),
31 | canActivate: [isElectronGuard],
32 | },
33 | ];
34 |
35 | @NgModule({
36 | imports: [RouterModule.forChild(routes)],
37 | exports: [RouterModule],
38 | })
39 | export class FunctionsRoutingModule {}
40 |
--------------------------------------------------------------------------------
/src/app/features/functions/functions.component.html:
--------------------------------------------------------------------------------
1 | Manage Drives
2 | Explore Drives
3 | Mount Manager
4 | Job Manager
5 |
--------------------------------------------------------------------------------
/src/app/features/functions/functions.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuudi/rclone-webui-angular/1407be6d51636b25be232900caea15243bc892b7/src/app/features/functions/functions.component.scss
--------------------------------------------------------------------------------
/src/app/features/functions/functions.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { FunctionsComponent } from './functions.component';
4 |
5 | describe('FunctionsComponent', () => {
6 | let component: FunctionsComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [FunctionsComponent],
12 | });
13 | fixture = TestBed.createComponent(FunctionsComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/features/functions/functions.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-functions',
5 | templateUrl: './functions.component.html',
6 | styleUrls: ['./functions.component.scss'],
7 | })
8 | export class FunctionsComponent {}
9 |
--------------------------------------------------------------------------------
/src/app/features/functions/functions.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { FunctionsRoutingModule } from './functions-routing.module';
5 | import { FunctionsComponent } from './functions.component';
6 |
7 | @NgModule({
8 | declarations: [FunctionsComponent],
9 | imports: [CommonModule, FunctionsRoutingModule],
10 | })
11 | export class FunctionsModule {}
12 |
--------------------------------------------------------------------------------
/src/app/features/functions/is-electron.guard.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 | import { CanActivateFn } from '@angular/router';
3 |
4 | import { isElectronGuard } from './is-electron.guard';
5 |
6 | describe('isElectronGuard', () => {
7 | const executeGuard: CanActivateFn = (...guardParameters) =>
8 | TestBed.runInInjectionContext(() => isElectronGuard(...guardParameters));
9 |
10 | beforeEach(() => {
11 | TestBed.configureTestingModule({});
12 | });
13 |
14 | it('should be created', () => {
15 | expect(executeGuard).toBeTruthy();
16 | });
17 | });
18 |
--------------------------------------------------------------------------------
/src/app/features/functions/is-electron.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivateFn } from '@angular/router';
2 |
3 | export const isElectronGuard: CanActivateFn = () => {
4 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
5 | // @ts-ignore
6 | return globalThis['RWA_DESKTOP'] !== undefined;
7 | };
8 |
--------------------------------------------------------------------------------
/src/app/features/functions/job/job-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 | import { JobComponent } from './job.component';
4 |
5 | const routes: Routes = [{ path: '', component: JobComponent }];
6 |
7 | @NgModule({
8 | imports: [RouterModule.forChild(routes)],
9 | exports: [RouterModule],
10 | })
11 | export class JobRoutingModule {}
12 |
--------------------------------------------------------------------------------
/src/app/features/functions/job/job.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0; else nothing">
4 |
5 |
6 |
7 |
8 |
13 |
17 |
21 |
22 | {{ job.id }}:
23 | {{ job.summary }}
24 |
25 |
26 |
27 |
28 |
36 |
37 |
41 |
42 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | No jobs
59 |
60 |
61 |
--------------------------------------------------------------------------------
/src/app/features/functions/job/job.component.scss:
--------------------------------------------------------------------------------
1 | .list-container {
2 | margin: 3em auto;
3 | min-height: 20em;
4 | width: 80%;
5 | max-width: 50em;
6 | }
7 |
8 | .list-line {
9 | display: flex;
10 |
11 | .job-info {
12 | display: flex;
13 | align-items: center;
14 |
15 | .mat-icon {
16 | margin: 0.5em;
17 | }
18 | }
19 | }
20 | .spacer {
21 | margin: 0 auto;
22 | }
23 |
24 | .icon--spinning {
25 | animation: spin 2s linear infinite;
26 | }
27 |
28 | .empty-hint {
29 | margin: 2em auto;
30 | text-align: center;
31 | }
32 |
--------------------------------------------------------------------------------
/src/app/features/functions/job/job.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { JobComponent } from './job.component';
4 |
5 | describe('JobComponent', () => {
6 | let component: JobComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [JobComponent],
12 | });
13 | fixture = TestBed.createComponent(JobComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/features/functions/job/job.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 |
3 | import { JobService } from './job.service';
4 |
5 | @Component({
6 | selector: 'app-job',
7 | templateUrl: './job.component.html',
8 | styleUrls: ['./job.component.scss'],
9 | })
10 | export class JobComponent {
11 | jobs$ = this.jobService.getJobs();
12 | constructor(private jobService: JobService) {}
13 |
14 | removeJob(jobId: number) {
15 | this.jobService.removeJob(jobId);
16 | }
17 |
18 | killJob(jobId: number) {
19 | this.jobService.killJob(jobId);
20 | }
21 |
22 | removeFinishedJobs() {
23 | this.jobService.removeFinishedJobs();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/features/functions/job/job.model.ts:
--------------------------------------------------------------------------------
1 | interface BaseJobInfo {
2 | id: number;
3 | finished: boolean;
4 | success: boolean; // true for success false otherwise
5 | error: string; // empty string if no error
6 | duration: number; // in seconds
7 | startTime: Date;
8 | }
9 |
10 | interface PendingJobInfo extends BaseJobInfo {
11 | finished: false;
12 | success: false;
13 | error: '';
14 | progress: unknown;
15 | }
16 |
17 | interface SuccessJobInfo extends BaseJobInfo {
18 | finished: true;
19 | success: true;
20 | error: '';
21 | output: R;
22 | endTime: Date;
23 | }
24 |
25 | interface ErrorJobInfo extends BaseJobInfo {
26 | finished: true;
27 | success: false;
28 | error: string;
29 | duration: number;
30 | endTime: Date;
31 | }
32 |
33 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
34 | export type JobInfo =
35 | | PendingJobInfo
36 | | SuccessJobInfo
37 | | ErrorJobInfo;
38 |
--------------------------------------------------------------------------------
/src/app/features/functions/job/job.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 |
4 | import { MatButtonModule } from '@angular/material/button';
5 | import { MatCardModule } from '@angular/material/card';
6 | import { MatDividerModule } from '@angular/material/divider';
7 | import { MatIconModule } from '@angular/material/icon';
8 | import { MatListModule } from '@angular/material/list';
9 | import { MatMenuModule } from '@angular/material/menu';
10 | import { MatTooltipModule } from '@angular/material/tooltip';
11 |
12 | import { JobRoutingModule } from './job-routing.module';
13 | import { JobComponent } from './job.component';
14 |
15 | @NgModule({
16 | declarations: [JobComponent],
17 | imports: [
18 | CommonModule,
19 | JobRoutingModule,
20 | MatCardModule,
21 | MatListModule,
22 | MatDividerModule,
23 | MatButtonModule,
24 | MatIconModule,
25 | MatMenuModule,
26 | MatTooltipModule,
27 | ],
28 | })
29 | export class JobModule {}
30 |
--------------------------------------------------------------------------------
/src/app/features/functions/job/job.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { JobService } from './job.service';
4 |
5 | describe('JobService', () => {
6 | let service: JobService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(JobService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/features/functions/mount/mount-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule } from '@angular/core';
2 | import { RouterModule, Routes } from '@angular/router';
3 |
4 | import { MountComponent } from './mount.component';
5 |
6 | const routes: Routes = [
7 | {
8 | path: '',
9 | component: MountComponent,
10 | },
11 | ];
12 |
13 | @NgModule({
14 | imports: [RouterModule.forChild(routes)],
15 | exports: [RouterModule],
16 | })
17 | export class MountRoutingModule {}
18 |
--------------------------------------------------------------------------------
/src/app/features/functions/mount/mount.component.html:
--------------------------------------------------------------------------------
1 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {{ setting.Fs }}:
24 |
28 | {{ setting.MountPoint }}
29 |
30 |
31 |
32 |
37 |
40 |
41 |
45 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/src/app/features/functions/mount/mount.component.scss:
--------------------------------------------------------------------------------
1 | .button-group {
2 | display: flex;
3 | margin: 3em auto;
4 | width: 90%;
5 | max-width: 65em;
6 |
7 | button {
8 | margin: 0 1em;
9 | }
10 | }
11 |
12 | .spacer {
13 | margin: 0 auto;
14 | }
15 |
16 | .list-container {
17 | margin: 3em auto;
18 | min-height: 20em;
19 | width: 80%;
20 | max-width: 50em;
21 | }
22 |
23 | .list-line {
24 | display: flex;
25 |
26 | .mount-info {
27 | display: flex;
28 | align-items: center;
29 |
30 | .mat-icon {
31 | margin: 0.5em;
32 | }
33 | }
34 |
35 | .mount-action {
36 | display: flex;
37 | align-items: center;
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/app/features/functions/mount/mount.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { MountComponent } from './mount.component';
4 |
5 | describe('MountComponent', () => {
6 | let component: MountComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [MountComponent],
12 | });
13 | fixture = TestBed.createComponent(MountComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/features/functions/mount/mount.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 | import { ReactiveFormsModule } from '@angular/forms';
4 |
5 | import { MatButtonModule } from '@angular/material/button';
6 | import { MatCardModule } from '@angular/material/card';
7 | import { MatCheckboxModule } from '@angular/material/checkbox';
8 | import { MatDialogModule } from '@angular/material/dialog';
9 | import { MatDividerModule } from '@angular/material/divider';
10 | import { MatFormFieldModule } from '@angular/material/form-field';
11 | import { MatIconModule } from '@angular/material/icon';
12 | import { MatInputModule } from '@angular/material/input';
13 | import { MatListModule } from '@angular/material/list';
14 | import { MatMenuModule } from '@angular/material/menu';
15 | import { MatSelectModule } from '@angular/material/select';
16 | import { MatSlideToggleModule } from '@angular/material/slide-toggle';
17 | import { MatSnackBarModule } from '@angular/material/snack-bar';
18 | import { MatTooltipModule } from '@angular/material/tooltip';
19 |
20 | import { MountRoutingModule } from './mount-routing.module';
21 | import { MountComponent } from './mount.component';
22 | import { NewMountDialogComponent } from './new-mount-dialog/new-mount-dialog.component';
23 |
24 | @NgModule({
25 | declarations: [MountComponent, NewMountDialogComponent],
26 | imports: [
27 | CommonModule,
28 | ReactiveFormsModule,
29 | MountRoutingModule,
30 | MatTooltipModule,
31 | MatListModule,
32 | MatButtonModule,
33 | MatIconModule,
34 | MatCardModule,
35 | MatSnackBarModule,
36 | MatSlideToggleModule,
37 | MatDialogModule,
38 | MatFormFieldModule,
39 | MatInputModule,
40 | MatCheckboxModule,
41 | MatSelectModule,
42 | MatDividerModule,
43 | MatMenuModule,
44 | ],
45 | })
46 | export class MountModule {}
47 |
--------------------------------------------------------------------------------
/src/app/features/functions/mount/mount.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed } from '@angular/core/testing';
2 |
3 | import { MountService } from './mount.service';
4 |
5 | describe('MountService', () => {
6 | let service: MountService;
7 |
8 | beforeEach(() => {
9 | TestBed.configureTestingModule({});
10 | service = TestBed.inject(MountService);
11 | });
12 |
13 | it('should be created', () => {
14 | expect(service).toBeTruthy();
15 | });
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/features/functions/mount/new-mount-dialog/new-mount-dialog.component.html:
--------------------------------------------------------------------------------
1 | Create MountPoint
2 |
104 |
105 |
106 |
115 |
116 |
--------------------------------------------------------------------------------
/src/app/features/functions/mount/new-mount-dialog/new-mount-dialog.component.scss:
--------------------------------------------------------------------------------
1 | .mount-form {
2 | display: flex;
3 | flex-direction: column;
4 | }
5 |
6 | .button-advanced {
7 | display: flex;
8 | justify-content: center;
9 | }
10 |
11 | mat-slide-toggle {
12 | margin: 1em;
13 | }
14 |
--------------------------------------------------------------------------------
/src/app/features/functions/mount/new-mount-dialog/new-mount-dialog.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { NewMountDialogComponent } from './new-mount-dialog.component';
4 |
5 | describe('NewMountDialogComponent', () => {
6 | let component: NewMountDialogComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [NewMountDialogComponent],
12 | });
13 | fixture = TestBed.createComponent(NewMountDialogComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/features/functions/mount/new-mount-dialog/new-mount-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Inject } from '@angular/core';
2 | import { FormBuilder, Validators } from '@angular/forms';
3 |
4 | import { MAT_DIALOG_DATA, MatDialog } from '@angular/material/dialog';
5 |
6 | import { jsonStringValidator } from 'src/app/shared/json-string-validator.directive';
7 | import { SimpleDialogComponent } from 'src/app/shared/simple-dialog/simple-dialog.component';
8 | import { environment } from 'src/environments/environment';
9 |
10 | @Component({
11 | selector: 'app-new-mount-dialog',
12 | templateUrl: './new-mount-dialog.component.html',
13 | styleUrls: ['./new-mount-dialog.component.scss'],
14 | })
15 | export class NewMountDialogComponent {
16 | mountForm = this.fb.nonNullable.group({
17 | Fs: ['', Validators.required],
18 | AutoMountPoint: [true], // Only for Windows
19 | MountPoint: ['', Validators.required],
20 | enabled: [true],
21 | autoMount: [false], // Scheduled task
22 | readonly: [false],
23 | windowsNetworkMode: [true],
24 | filePerms: [
25 | '0666',
26 | [Validators.required, Validators.pattern(/^0?[1-7][0-7]{2}$/)], // although something like 077 is valid, we don't want to allow it
27 | ],
28 | dirPerms: [
29 | '0777',
30 | [Validators.required, Validators.pattern(/^0?[1-7][0-7]{2}$/)],
31 | ],
32 | noModTime: [false],
33 | vfsCacheMode: ['minimal'],
34 | vfsCacheMaxAge: [
35 | '1h',
36 | [Validators.required, Validators.pattern(/^\d+[smhd]$/)],
37 | ],
38 | customMountOpt: ['{\n}', jsonStringValidator()],
39 | customVfsOpt: ['{\n}', jsonStringValidator()],
40 | });
41 | showAdvancedOptions = false;
42 | hasCron = environment.electron;
43 |
44 | constructor(
45 | private dialog: MatDialog,
46 | private fb: FormBuilder,
47 | @Inject(MAT_DIALOG_DATA)
48 | public data: {
49 | osType: string;
50 | fsOptions: string[];
51 | },
52 | ) {
53 | if (data.osType === 'windows') {
54 | this.mountForm.controls.MountPoint.setValue('Z:');
55 | } else {
56 | this.mountForm.controls.AutoMountPoint.setValue(false);
57 | this.mountForm.controls.MountPoint.setValue('/mnt/rclone');
58 | }
59 | }
60 |
61 | getMountOptHelp() {
62 | this.dialog.open(SimpleDialogComponent, {
63 | data: {
64 | title: $localize`Information`,
65 | message: $localize`This is options for advanced user only!\nPlease input options in JSON format, keys are in PascalCase.\nAvailable options: please refer to https://github.com/rclone/rclone/blob/master/cmd/mountlib/mount.go\nfind "type Options struct" part`,
66 | actions: [{ label: $localize`Close`, value: 0 }],
67 | },
68 | });
69 | }
70 | getVfsOptHelp() {
71 | this.dialog.open(SimpleDialogComponent, {
72 | data: {
73 | title: $localize`Information`,
74 | message: $localize`This is options for advanced user only!\nPlease input options in JSON format, keys are in PascalCase.\nAvailable options: please refer to https://github.com/rclone/rclone/blob/master/vfs/vfscommon/options.go\nfind "type Options struct" part`,
75 | actions: [{ label: $localize`Close`, value: 0 }],
76 | },
77 | });
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/app/features/functions/serve/serve.module.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { NgModule } from '@angular/core';
3 |
4 | @NgModule({
5 | declarations: [],
6 | imports: [CommonModule],
7 | })
8 | export class ServeModule {}
9 |
--------------------------------------------------------------------------------
/src/app/shared/bytes.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { BytesPipe } from './bytes.pipe';
2 |
3 | describe('BytesPipe', () => {
4 | it('create an instance', () => {
5 | const pipe = new BytesPipe();
6 | expect(pipe).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/shared/bytes.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'bytes',
5 | })
6 | export class BytesPipe implements PipeTransform {
7 | transform(
8 | value: number,
9 | fractionDigits = 2,
10 | base: 1024 | 1000 = 1024,
11 | IEC = true,
12 | ): string {
13 | const units =
14 | IEC && base === 1024
15 | ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
16 | : ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
17 | let unit = 0;
18 | while (value >= base && unit < units.length - 1) {
19 | value /= base;
20 | unit++;
21 | }
22 | return `${value.toFixed(fractionDigits)} ${units[unit]}`;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/shared/json-string-validator.directive.ts:
--------------------------------------------------------------------------------
1 | import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
2 |
3 | export function jsonStringValidator(): ValidatorFn {
4 | return (control: AbstractControl): ValidationErrors | null => {
5 | try {
6 | JSON.parse(control.value);
7 | return null;
8 | } catch {
9 | return { invalidJsonString: true };
10 | }
11 | };
12 | }
13 |
--------------------------------------------------------------------------------
/src/app/shared/result.ts:
--------------------------------------------------------------------------------
1 | abstract class BaseResult {
2 | abstract readonly ok: boolean;
3 |
4 | abstract map(fn: (t: T) => U): Result;
5 | abstract or(t: T): T;
6 | abstract orElse(fn: (e: E) => T): T;
7 | abstract orThrow(): T;
8 | }
9 |
10 | class OkResult extends BaseResult implements Ok {
11 | readonly ok = true;
12 | constructor(readonly value: T) {
13 | super();
14 | }
15 | map(fn: (t: T) => U): Result {
16 | return Ok(fn(this.value));
17 | }
18 | or() {
19 | return this.value;
20 | }
21 | orElse() {
22 | return this.value;
23 | }
24 | orThrow() {
25 | return this.value;
26 | }
27 | }
28 |
29 | class ErrResult extends BaseResult implements Err {
30 | readonly ok = false;
31 | constructor(readonly error: E) {
32 | super();
33 | }
34 | map() {
35 | return this;
36 | }
37 | or(t: T) {
38 | return t;
39 | }
40 | orElse(fn: (e: E) => T): T {
41 | return fn(this.error);
42 | }
43 | orThrow(): never {
44 | throw new Error(String(this.error));
45 | }
46 | }
47 |
48 | interface Ok extends BaseResult {
49 | readonly ok: true;
50 | value: T;
51 | }
52 |
53 | function Ok(): Ok;
54 | function Ok(value: T): Ok;
55 | function Ok(value?: T) {
56 | return new OkResult(value);
57 | }
58 |
59 | interface Err extends BaseResult {
60 | readonly ok: false;
61 | error: E;
62 | }
63 |
64 | function Err(error: E): Err {
65 | return new ErrResult(error);
66 | }
67 |
68 | type Result = Ok | Err;
69 |
70 | export { Err, Ok, Result };
71 |
--------------------------------------------------------------------------------
/src/app/shared/search.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { SearchPipe } from './search.pipe';
2 |
3 | describe('SearchPipe', () => {
4 | it('create an instance', () => {
5 | const pipe = new SearchPipe();
6 | expect(pipe).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/shared/search.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({
4 | name: 'search',
5 | })
6 | export class SearchPipe implements PipeTransform {
7 | transform(
8 | values: V[],
9 | searchString: string,
10 | searchKeys: K[],
11 | caseSensitive = false,
12 | ): V[] {
13 | if (!searchString) {
14 | return values;
15 | }
16 | if (searchKeys.length === 0) {
17 | return values;
18 | }
19 | return values.filter((v) =>
20 | searchKeys.some((key) =>
21 | SearchPipe.isSubSequence(searchString, v[key], caseSensitive),
22 | ),
23 | );
24 | }
25 |
26 | private static isSubSequence(
27 | sub: string,
28 | str: string,
29 | caseSensitive = false,
30 | ): boolean {
31 | if (!caseSensitive) {
32 | sub = sub.toLowerCase();
33 | str = str.toLowerCase();
34 | }
35 | let j = 0;
36 | for (let i = 0; i < str.length && j < sub.length; i++) {
37 | if (sub[j] === str[i]) {
38 | j++;
39 | }
40 | }
41 | return j === sub.length;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/app/shared/simple-dialog/simple-dialog.component.html:
--------------------------------------------------------------------------------
1 | {{ data.title }}
2 | {{ data.message }}
3 |
4 |
11 |
12 |
--------------------------------------------------------------------------------
/src/app/shared/simple-dialog/simple-dialog.component.scss:
--------------------------------------------------------------------------------
1 | .message {
2 | white-space: pre-wrap;
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/shared/simple-dialog/simple-dialog.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { SimpleDialogComponent } from './simple-dialog.component';
4 |
5 | describe('SimpleDialogComponent', () => {
6 | let component: SimpleDialogComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | imports: [SimpleDialogComponent],
12 | });
13 | fixture = TestBed.createComponent(SimpleDialogComponent);
14 | component = fixture.componentInstance;
15 | fixture.detectChanges();
16 | });
17 |
18 | it('should create', () => {
19 | expect(component).toBeTruthy();
20 | });
21 | });
22 |
--------------------------------------------------------------------------------
/src/app/shared/simple-dialog/simple-dialog.component.ts:
--------------------------------------------------------------------------------
1 | import { CommonModule } from '@angular/common';
2 | import { Component, Inject } from '@angular/core';
3 |
4 | import { MatButtonModule } from '@angular/material/button';
5 | import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
6 |
7 | @Component({
8 | standalone: true,
9 | imports: [CommonModule, MatButtonModule, MatDialogModule],
10 | templateUrl: './simple-dialog.component.html',
11 | styleUrls: ['./simple-dialog.component.scss'],
12 | })
13 | export class SimpleDialogComponent {
14 | constructor(
15 | @Inject(MAT_DIALOG_DATA)
16 | public data: {
17 | title: string;
18 | message: string;
19 | actions: {
20 | value: T;
21 | label: string;
22 | }[];
23 | },
24 | ) {}
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/shared/single-click.directive.spec.ts:
--------------------------------------------------------------------------------
1 | import { SingleClickDirective } from './single-click.directive';
2 |
3 | describe('SingleClickDirective', () => {
4 | it('should create an instance', () => {
5 | const directive = new SingleClickDirective();
6 | expect(directive).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/shared/single-click.directive.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Directive,
3 | EventEmitter,
4 | HostListener,
5 | Input,
6 | OnDestroy,
7 | OnInit,
8 | Output,
9 | } from '@angular/core';
10 |
11 | import { Subject, bufferTime, filter } from 'rxjs';
12 |
13 | @Directive({
14 | selector: '[appSingleClick]',
15 | })
16 | export class SingleClickDirective implements OnInit, OnDestroy {
17 | @Input() appSingleClickDelay = 500;
18 | @Output() appSingleClick = new EventEmitter();
19 | click$ = new Subject();
20 |
21 | @HostListener('click', ['$event'])
22 | onClick(event: MouseEvent) {
23 | event.stopPropagation();
24 | event.preventDefault();
25 | this.click$.next(event);
26 | }
27 |
28 | ngOnInit() {
29 | this.click$
30 | .pipe(
31 | bufferTime(this.appSingleClickDelay),
32 | filter((clicks) => clicks.length === 1),
33 | )
34 | .subscribe((clicks) => this.appSingleClick.emit(clicks[0]));
35 | }
36 |
37 | ngOnDestroy() {
38 | this.click$.unsubscribe();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/shared/utils.ts:
--------------------------------------------------------------------------------
1 | export const trimEnding = (s: string, ending: string): string => {
2 | if (s.endsWith(ending)) {
3 | return s.slice(0, -ending.length);
4 | }
5 | return s;
6 | };
7 |
--------------------------------------------------------------------------------
/src/assets/icons/github-mark-white.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/icons/icon-rclone.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/environments/environment.embed.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | environment: 'embed',
3 | explorerCanDownload: true,
4 | connectSelf: true,
5 | useServiceWorker: false,
6 | showRemoteSetting: false,
7 | prefetch: false,
8 | reuseMissingExecuteId: false,
9 | electron: false,
10 | };
11 |
--------------------------------------------------------------------------------
/src/environments/environment.native.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | environment: 'native',
3 | explorerCanDownload: false,
4 | connectSelf: true,
5 | useServiceWorker: false,
6 | showRemoteSetting: false,
7 | prefetch: false,
8 | reuseMissingExecuteId: true,
9 | electron: true,
10 | };
11 |
--------------------------------------------------------------------------------
/src/environments/environment.standalone.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | environment: 'standalone',
3 | explorerCanDownload: false,
4 | connectSelf: false,
5 | useServiceWorker: true,
6 | showRemoteSetting: true,
7 | prefetch: true,
8 | reuseMissingExecuteId: false,
9 | electron: false,
10 | };
11 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // this is debug environment
2 | export const environment = {
3 | environment: 'development',
4 | explorerCanDownload: true,
5 | connectSelf: false,
6 | useServiceWorker: false,
7 | showRemoteSetting: true,
8 | prefetch: false,
9 | reuseMissingExecuteId: true,
10 | electron: false,
11 | };
12 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yuudi/rclone-webui-angular/1407be6d51636b25be232900caea15243bc892b7/src/favicon.ico
--------------------------------------------------------------------------------
/src/i18n-index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Rclone Webui
7 |
8 |
31 |
32 |
33 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Rclone
6 |
7 |
8 |
9 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
4 |
5 | import { AppModule } from './app/app.module';
6 |
7 | platformBrowserDynamic()
8 | .bootstrapModule(AppModule)
9 | .catch((err) => console.error(err));
10 |
--------------------------------------------------------------------------------
/src/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "rclone-webui-angular",
3 | "short_name": "rclone-webui-angular",
4 | "theme_color": "#1976d2",
5 | "background_color": "#fafafa",
6 | "display": "standalone",
7 | "scope": "./",
8 | "start_url": "./",
9 | "icons": [
10 | {
11 | "src": "assets/icons/icon-rclone.svg",
12 | "sizes": "150x150",
13 | "type": "image/svg+xml",
14 | "purpose": "any"
15 | }
16 | ]
17 | }
18 |
--------------------------------------------------------------------------------
/src/material.scss:
--------------------------------------------------------------------------------
1 | @use "@angular/material" as mat;
2 |
3 | @import "@angular/material/theming";
4 |
5 | @include mat.core();
6 |
7 | $angular-primary: mat.define-palette(mat.$indigo-palette, 500, 100, 900);
8 | $angular-accent: mat.define-palette(mat.$pink-palette, A200, A100, A400);
9 | $angular-warn: mat.define-palette(mat.$red-palette);
10 |
11 | $angular-default-theme: mat.define-light-theme(
12 | (
13 | color: (
14 | primary: $angular-primary,
15 | accent: $angular-accent,
16 | warn: $angular-warn,
17 | ),
18 | typography: mat.define-typography-config(),
19 | density: 0,
20 | )
21 | );
22 |
23 | $angular-dark-theme: mat.define-dark-theme(
24 | (
25 | color: (
26 | primary: $angular-primary,
27 | accent: $angular-accent,
28 | warn: $angular-warn,
29 | ),
30 | typography: mat.define-typography-config(),
31 | density: 0,
32 | )
33 | );
34 |
35 | @include mat.all-component-themes($angular-default-theme);
36 |
37 | .dark-theme-basic {
38 | background-color: #424242;
39 | color: aliceblue;
40 | }
41 |
42 | .dark-theme {
43 | @include mat.all-component-colors($angular-dark-theme);
44 | }
45 |
46 | // @media (prefers-color-scheme: dark) {
47 | // .auto-theme {
48 | // @extend .dark-theme;
49 | // }
50 | // }
51 |
--------------------------------------------------------------------------------
/src/proxy.conf.mjs:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | context: [
4 | "/job",
5 | "/cache",
6 | "/options",
7 | "/core",
8 | "/fscache",
9 | "/debug",
10 | "/config",
11 | "/operations",
12 | "/backend",
13 | "/mount",
14 | "/vfs",
15 | "/sync",
16 | "/rc",
17 | "/pluginsctl",
18 | ],
19 | target: "http://127.0.0.1:5572",
20 | secure: false,
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @import "./material.scss";
2 |
3 | html,
4 | body {
5 | height: 100%;
6 | }
7 | body {
8 | margin: 0;
9 | font-family: Roboto, "Helvetica Neue", sans-serif;
10 | }
11 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": [
7 | "@angular/localize"
8 | ]
9 | },
10 | "files": [
11 | "src/main.ts"
12 | ],
13 | "include": [
14 | "src/**/*.d.ts"
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "strict": true,
9 | "resolveJsonModule": true,
10 | "allowSyntheticDefaultImports": true,
11 | "noImplicitOverride": true,
12 | "noPropertyAccessFromIndexSignature": true,
13 | "noImplicitReturns": true,
14 | "noFallthroughCasesInSwitch": true,
15 | "sourceMap": true,
16 | "declaration": false,
17 | "downlevelIteration": true,
18 | "experimentalDecorators": true,
19 | "moduleResolution": "node",
20 | "importHelpers": true,
21 | "target": "ES2022",
22 | "module": "ES2022",
23 | "useDefineForClassFields": false,
24 | "lib": [
25 | "ES2022",
26 | "dom"
27 | ]
28 | },
29 | "angularCompilerOptions": {
30 | "enableI18nLegacyMessageIdFormat": false,
31 | "strictInjectionParameters": true,
32 | "strictInputAccessModifiers": true,
33 | "strictTemplates": true
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": [
7 | "jasmine",
8 | "@angular/localize"
9 | ]
10 | },
11 | "include": [
12 | "src/**/*.spec.ts",
13 | "src/**/*.d.ts"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------