commit
eb2062e410
@ -1,210 +0,0 @@
|
||||
#!/bin/env python3
|
||||
import shutil as shut
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
tauri_cli_version = '1.0.0-rc.5'
|
||||
build_output = 'out'
|
||||
verbose = False
|
||||
ffmpeg = False
|
||||
|
||||
windows = os.name == 'nt'
|
||||
|
||||
|
||||
def exec(cmd: str, dir: str = None) -> str:
|
||||
print('Running: {}'.format(cmd))
|
||||
child = subprocess.run(cmd, shell=True, cwd=dir)
|
||||
child.check_returncode()
|
||||
|
||||
|
||||
def check_exec(name: str):
|
||||
print('Checking {}...'.format(name))
|
||||
|
||||
if shut.which(name) is None:
|
||||
raise Exception('{} not found'.format(name))
|
||||
exec(name + ' --version')
|
||||
|
||||
|
||||
def check_yarn():
|
||||
print('Checking yarn...')
|
||||
|
||||
if shut.which('yarn') is None:
|
||||
print('installing yarn...')
|
||||
npm('install -g yarn')
|
||||
check_exec('yarn')
|
||||
exec('yarn --version')
|
||||
|
||||
|
||||
def check_ng():
|
||||
print('Checking ng...')
|
||||
|
||||
if shut.which('ng') is None:
|
||||
print('installing ng...')
|
||||
npm('install -g @angular/cli')
|
||||
check_exec('ng')
|
||||
exec('ng --version')
|
||||
|
||||
|
||||
def store_artifact(path: str):
|
||||
print('Storing {}'.format(path))
|
||||
if os.path.isdir(path):
|
||||
shut.copytree(path, os.path.join(
|
||||
build_output, os.path.basename(path)), dirs_exist_ok=True)
|
||||
else:
|
||||
shut.copy(path, build_output)
|
||||
|
||||
|
||||
def cargo(cmd: str, dir: str = None):
|
||||
if verbose:
|
||||
exec('cargo {} --verbose'.format(cmd), dir)
|
||||
else:
|
||||
exec('cargo {}'.format(cmd), dir)
|
||||
|
||||
|
||||
def npm(cmd: str, dir: str = None):
|
||||
exec('npm {}'.format(cmd), dir)
|
||||
|
||||
|
||||
def yarn(cmd: str, dir: str = None):
|
||||
exec('yarn {}'.format(cmd), dir)
|
||||
|
||||
|
||||
def build_daemon():
|
||||
'''Builds daemon'''
|
||||
cargo('fetch', 'mediarepo-daemon')
|
||||
|
||||
if not ffmpeg:
|
||||
cargo('build --release --frozen --no-default-features', 'mediarepo-daemon')
|
||||
else:
|
||||
cargo('build --release --frozen', 'mediarepo-daemon')
|
||||
|
||||
if windows:
|
||||
store_artifact('mediarepo-daemon/target/release/mediarepo-daemon.exe')
|
||||
else:
|
||||
store_artifact('mediarepo-daemon/target/release/mediarepo-daemon')
|
||||
|
||||
|
||||
def build_ui():
|
||||
'''Builds UI'''
|
||||
cargo('install tauri-cli --version ^{}'.format(tauri_cli_version))
|
||||
yarn('install', 'mediarepo-ui')
|
||||
cargo('tauri build', 'mediarepo-ui')
|
||||
|
||||
if windows:
|
||||
store_artifact(
|
||||
'mediarepo-ui/src-tauri/target/release/mediarepo-ui.exe')
|
||||
else:
|
||||
store_artifact('mediarepo-ui/src-tauri/target/release/mediarepo-ui')
|
||||
|
||||
store_artifact('mediarepo-ui/src-tauri/target/release/bundle/')
|
||||
|
||||
|
||||
def check_daemon():
|
||||
'''Checks dependencies for daemon'''
|
||||
check_exec('clang')
|
||||
check_exec('cargo')
|
||||
|
||||
|
||||
def check_ui():
|
||||
'''Checks dependencies for UI'''
|
||||
|
||||
if not windows:
|
||||
check_exec('wget')
|
||||
check_exec('curl')
|
||||
check_exec('file')
|
||||
|
||||
check_exec('clang')
|
||||
check_exec('cargo')
|
||||
check_exec('node')
|
||||
check_exec('npm')
|
||||
check_yarn()
|
||||
check_ng()
|
||||
|
||||
|
||||
def check():
|
||||
'''Checks dependencies'''
|
||||
check_daemon()
|
||||
check_ui()
|
||||
print('All checks passed')
|
||||
|
||||
|
||||
def create_output_dir():
|
||||
'''Creates build output directory'''
|
||||
if not os.path.exists(build_output):
|
||||
os.mkdir(build_output)
|
||||
|
||||
|
||||
def clean():
|
||||
'''Removes build output'''
|
||||
if os.path.exists(build_output):
|
||||
shut.rmtree(build_output)
|
||||
print('Cleaned')
|
||||
|
||||
|
||||
def build(daemon=True, ui=True):
|
||||
'''Builds both daemon and UI'''
|
||||
clean()
|
||||
create_output_dir()
|
||||
|
||||
if daemon:
|
||||
check_daemon()
|
||||
build_daemon()
|
||||
|
||||
if ui:
|
||||
check_ui()
|
||||
build_ui()
|
||||
|
||||
print('Build complete')
|
||||
|
||||
|
||||
def parse_args():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='Build mediarepo')
|
||||
subparsers = parser.add_subparsers(dest='command')
|
||||
subparsers.required = True
|
||||
|
||||
subparsers.add_parser('check')
|
||||
|
||||
build_parser = subparsers.add_parser('build')
|
||||
build_parser.add_argument(
|
||||
'--daemon', action='store_true', help='Build daemon')
|
||||
build_parser.add_argument('--ui', action='store_true', help='Build UI')
|
||||
build_parser.add_argument(
|
||||
'--verbose', action='store_true', help='Verbose build')
|
||||
build_parser.add_argument(
|
||||
'--output', action='store', help='Build output directory')
|
||||
build_parser.add_argument(
|
||||
'--ffmpeg', action='store_true', help='Build with ffmpeg')
|
||||
|
||||
subparsers.add_parser('clean')
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def main():
|
||||
opts = parse_args()
|
||||
|
||||
if opts.command == 'build':
|
||||
global build_output
|
||||
build_output = opts.output if opts.output else build_output
|
||||
|
||||
global verbose
|
||||
verbose = opts.verbose
|
||||
|
||||
global ffmpeg
|
||||
ffmpeg = opts.ffmpeg
|
||||
|
||||
if opts.daemon:
|
||||
build(True, False)
|
||||
elif opts.ui:
|
||||
build(False, True)
|
||||
else:
|
||||
build()
|
||||
elif opts.command == 'check':
|
||||
check()
|
||||
elif opts.command == 'clean':
|
||||
clean()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -1,18 +1,6 @@
|
||||
<div *ngIf="!selectedRepository" class="repo-page-content">
|
||||
<div class="add-repo-tools">
|
||||
<button (click)="openAddRepositoryDialog()" color="primary" mat-flat-button>Add Repository</button>
|
||||
</div>
|
||||
<div class="repository-list">
|
||||
<div *ngFor="let repository of repositories" class="repository-container">
|
||||
<app-repository-card (openEvent)="this.onOpenRepository($event)"
|
||||
[repository]="repository"></app-repository-card>
|
||||
</div>
|
||||
<app-middle-centered *ngIf="this.repositories.length === 0" class="add-repository-prompt">
|
||||
<h1>There are no repositories yet. You can create a repository or add an existing one.</h1>
|
||||
<button (click)="this.openAddRepositoryDialog()" color="primary" mat-flat-button>Add Repository</button>
|
||||
</app-middle-centered>
|
||||
</div>
|
||||
<div *ngIf="!this.selectedRepository">
|
||||
<app-repository-overview></app-repository-overview>
|
||||
</div>
|
||||
<div *ngIf="selectedRepository" class="repo-details">
|
||||
<app-repository-details-view [repository]="selectedRepository"></app-repository-details-view>
|
||||
<div *ngIf="this.selectedRepository" class="repo-details">
|
||||
<app-repository-details-view [repository]="this.selectedRepository"></app-repository-details-view>
|
||||
</div>
|
||||
|
@ -1,164 +1,18 @@
|
||||
import {AfterViewInit, Component, OnInit} from "@angular/core";
|
||||
import {Repository} from "../../../../api/models/Repository";
|
||||
import {Component} from "@angular/core";
|
||||
import {RepositoryService} from "../../../services/repository/repository.service";
|
||||
import {MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {DownloadDaemonDialogComponent} from "./download-daemon-dialog/download-daemon-dialog.component";
|
||||
import {
|
||||
AddRepositoryDialogComponent
|
||||
} from "../../shared/repository/repository/add-repository-dialog/add-repository-dialog.component";
|
||||
import {LoggingService} from "../../../services/logging/logging.service";
|
||||
import {BehaviorSubject} from "rxjs";
|
||||
import {BusyDialogComponent} from "../../shared/app-common/busy-dialog/busy-dialog.component";
|
||||
import {JobService} from "../../../services/job/job.service";
|
||||
import {StateService} from "../../../services/state/state.service";
|
||||
import {Repository} from "../../../../api/models/Repository";
|
||||
|
||||
type BusyDialogContext = { message: BehaviorSubject<string>, dialog: MatDialogRef<BusyDialogComponent> };
|
||||
|
||||
@Component({
|
||||
selector: "app-repositories-tab",
|
||||
templateUrl: "./repositories-tab.component.html",
|
||||
styleUrls: ["./repositories-tab.component.scss"]
|
||||
})
|
||||
export class RepositoriesTabComponent implements OnInit, AfterViewInit {
|
||||
public repositories: Repository[] = [];
|
||||
public selectedRepository?: Repository;
|
||||
|
||||
constructor(
|
||||
private logger: LoggingService,
|
||||
private repoService: RepositoryService,
|
||||
private jobService: JobService,
|
||||
private stateService: StateService,
|
||||
public dialog: MatDialog
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.repoService.repositories.subscribe({
|
||||
next: (repos) => {
|
||||
this.repositories = repos;
|
||||
}
|
||||
});
|
||||
this.repoService.selectedRepository.subscribe(
|
||||
repo => this.selectedRepository = repo);
|
||||
}
|
||||
export class RepositoriesTabComponent {
|
||||
|
||||
public async ngAfterViewInit() {
|
||||
await this.checkAndPromptDaemonExecutable();
|
||||
}
|
||||
|
||||
public async startDaemonAndSelectRepository(repository: Repository) {
|
||||
try {
|
||||
let dialogContext = this.openStartupDialog(repository);
|
||||
let daemonRunning = await this.repoService.checkDaemonRunning(
|
||||
repository.path!);
|
||||
if (!daemonRunning) {
|
||||
dialogContext.message.next("Starting repository daemon...");
|
||||
await this.repoService.startDaemon(repository.path!);
|
||||
|
||||
await new Promise((res, _) => {
|
||||
setTimeout(res, 2000); // wait for the daemon to start
|
||||
});
|
||||
}
|
||||
await this.selectRepository(repository, dialogContext);
|
||||
} catch (err: any) {
|
||||
this.logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
public async selectRepository(repository: Repository, dialogContext?: BusyDialogContext) {
|
||||
dialogContext = dialogContext ?? this.openStartupDialog(repository);
|
||||
try {
|
||||
dialogContext.message.next("Opening repository...");
|
||||
await this.repoService.setRepository(repository);
|
||||
await this.runRepositoryStartupTasks(dialogContext);
|
||||
dialogContext.message.next("Restoring previous tabs...");
|
||||
await this.repoService.loadRepositories();
|
||||
dialogContext.dialog.close(true);
|
||||
} catch (err: any) {
|
||||
this.logger.error(err);
|
||||
dialogContext.message.next(
|
||||
"Failed to open repository: " + err.toString());
|
||||
await this.forceCloseRepository();
|
||||
setTimeout(() => dialogContext!.dialog.close(true), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
public openAddRepositoryDialog() {
|
||||
this.dialog.open(AddRepositoryDialogComponent, {
|
||||
disableClose: true,
|
||||
minWidth: "30%",
|
||||
minHeight: "30%",
|
||||
});
|
||||
}
|
||||
|
||||
public async onOpenRepository(repository: Repository) {
|
||||
if (!repository.local) {
|
||||
await this.selectRepository(repository);
|
||||
} else {
|
||||
await this.startDaemonAndSelectRepository(repository);
|
||||
}
|
||||
}
|
||||
|
||||
private async forceCloseRepository() {
|
||||
try {
|
||||
await this.repoService.closeSelectedRepository();
|
||||
} catch {
|
||||
}
|
||||
try {
|
||||
await this.repoService.disconnectSelectedRepository();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
private async runRepositoryStartupTasks(dialogContext: BusyDialogContext): Promise<void> {
|
||||
dialogContext.message.next("Checking integrity...");
|
||||
await this.jobService.runJob("CheckIntegrity");
|
||||
dialogContext.message.next("Running a vacuum on the database...");
|
||||
await this.jobService.runJob("Vacuum");
|
||||
dialogContext.message.next(
|
||||
"Migrating content descriptors to new format...");
|
||||
await this.jobService.runJob("MigrateContentDescriptors");
|
||||
dialogContext.message.next("Calculating repository sizes...");
|
||||
await this.jobService.runJob("CalculateSizes", false);
|
||||
dialogContext.message.next("Generating missing thumbnails...");
|
||||
await this.jobService.runJob("GenerateThumbnails");
|
||||
dialogContext.message.next("Finished repository startup");
|
||||
}
|
||||
|
||||
private openStartupDialog(repository: Repository): BusyDialogContext {
|
||||
const dialogMessage = new BehaviorSubject<string>(
|
||||
"Opening repository...");
|
||||
let dialog = this.dialog.open(BusyDialogComponent, {
|
||||
data: {
|
||||
title: `Opening repository '${repository.name}'`,
|
||||
message: dialogMessage,
|
||||
allowCancel: true,
|
||||
}, disableClose: true,
|
||||
minWidth: "30%",
|
||||
minHeight: "30%",
|
||||
});
|
||||
dialog.afterClosed().subscribe(async (result) => {
|
||||
if (!result) {
|
||||
await this.forceCloseRepository();
|
||||
}
|
||||
});
|
||||
|
||||
return { message: dialogMessage, dialog };
|
||||
}
|
||||
public selectedRepository?: Repository;
|
||||
|
||||
private async checkAndPromptDaemonExecutable() {
|
||||
if (!await this.repoService.checkDameonConfigured()) {
|
||||
const result = await this.dialog.open(
|
||||
DownloadDaemonDialogComponent,
|
||||
{
|
||||
disableClose: true,
|
||||
}
|
||||
).afterClosed().toPromise();
|
||||
if (result) {
|
||||
// recursion avoidance
|
||||
setTimeout(
|
||||
async () => await this.checkAndPromptDaemonExecutable(), 0);
|
||||
}
|
||||
}
|
||||
constructor(private repositoryService: RepositoryService) {
|
||||
const sub = this.repositoryService.selectedRepository.subscribe(repo => this.selectedRepository = repo);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,24 @@
|
||||
<h1 class="title" mat-dialog-title>
|
||||
About mediarepo
|
||||
</h1>
|
||||
<div class="content" mat-dialog-content>
|
||||
<app-metadata-entry attributeName="Version" display="line">
|
||||
{{version | async}}
|
||||
</app-metadata-entry>
|
||||
<app-metadata-entry attributeName="Tauri Version" display="line">
|
||||
{{tauriVersion | async}}
|
||||
</app-metadata-entry>
|
||||
<br>
|
||||
<mat-divider></mat-divider>
|
||||
<h2>Built with</h2>
|
||||
<div class="lib-list">
|
||||
<b *ngFor="let lib of usedLibs">
|
||||
<app-external-url [href]="lib[1]">{{lib[0]}}</app-external-url>
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions" mat-dialog-actions>
|
||||
<button (click)="this.dialogRef.close(true)" color="primary" mat-stroked-button>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
@ -0,0 +1,20 @@
|
||||
.title {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: block;
|
||||
|
||||
button {
|
||||
float: right;
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.lib-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
|
||||
import {AboutDialogComponent} from "./about-dialog.component";
|
||||
|
||||
describe("AboutDialogComponent", () => {
|
||||
let component: AboutDialogComponent;
|
||||
let fixture: ComponentFixture<AboutDialogComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AboutDialogComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AboutDialogComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
import {ChangeDetectionStrategy, Component, OnInit} from "@angular/core";
|
||||
import {MatDialogRef} from "@angular/material/dialog";
|
||||
import {app} from "@tauri-apps/api";
|
||||
import {Subject} from "rxjs";
|
||||
|
||||
@Component({
|
||||
selector: "app-about-dialog",
|
||||
templateUrl: "./about-dialog.component.html",
|
||||
styleUrls: ["./about-dialog.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.Default
|
||||
})
|
||||
export class AboutDialogComponent implements OnInit {
|
||||
|
||||
public version = new Subject<string>();
|
||||
public tauriVersion = new Subject<string>();
|
||||
public usedLibs = [
|
||||
["Tauri", "https://tauri.studio"],
|
||||
["Angular", "https://angular.io/"],
|
||||
["SeaORM", "https://www.sea-ql.org"],
|
||||
["Tokio", "https://tokio.rs/"],
|
||||
["bromine", "https://github.com/Trivernis/bromine"]
|
||||
];
|
||||
|
||||
constructor(public dialogRef: MatDialogRef<AboutDialogComponent>) {
|
||||
}
|
||||
|
||||
public async ngOnInit() {
|
||||
this.version.next(await app.getVersion());
|
||||
this.tauriVersion.next(await app.getTauriVersion());
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<mat-toolbar>
|
||||
<h1 class="page-title">All Repositories</h1>
|
||||
<button (click)="openAddRepositoryDialog()" class="add-repo-button" color="primary" mat-flat-button>Add Repository
|
||||
</button>
|
||||
<button (click)="openAboutDialog()" class="about-button" mat-stroked-button>About</button>
|
||||
</mat-toolbar>
|
||||
<div class="repo-page-content">
|
||||
<div class="repository-list">
|
||||
<div *ngFor="let repository of repositories" class="repository-container">
|
||||
<app-repository-card (openEvent)="this.onOpenRepository($event)"
|
||||
[repository]="repository"></app-repository-card>
|
||||
</div>
|
||||
<app-middle-centered *ngIf="this.repositories.length === 0" class="add-repository-prompt">
|
||||
<h1>There are no repositories yet. You can create a repository or add an existing one.</h1>
|
||||
<button (click)="this.openAddRepositoryDialog()" color="primary" mat-flat-button>Add Repository</button>
|
||||
</app-middle-centered>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,63 @@
|
||||
.repository-container {
|
||||
padding: 1em;
|
||||
margin: auto;
|
||||
display: block;
|
||||
width: calc(600px - 2em);
|
||||
float: left;
|
||||
|
||||
app-repository-card {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.repo-page-content {
|
||||
margin: 0 10%;
|
||||
height: calc(100% - 2em);
|
||||
}
|
||||
|
||||
.add-repo-tools {
|
||||
height: 5em;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
button {
|
||||
margin: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.repository-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: calc(100% - 5em);
|
||||
}
|
||||
|
||||
|
||||
.add-repository-prompt {
|
||||
button {
|
||||
font-size: 1.5em;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 0.5em;
|
||||
}
|
||||
}
|
||||
|
||||
mat-toolbar {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
.add-repo-button {
|
||||
margin: 0 auto 0 0;
|
||||
}
|
||||
|
||||
.about-button {
|
||||
margin: 0 0 0 auto;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
position: absolute;
|
||||
width: calc(100% - 32px);
|
||||
text-align: center;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
|
||||
import {RepositoryOverviewComponent} from "./repository-overview.component";
|
||||
|
||||
describe("RepositoryOverviewComponent", () => {
|
||||
let component: RepositoryOverviewComponent;
|
||||
let fixture: ComponentFixture<RepositoryOverviewComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RepositoryOverviewComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(RepositoryOverviewComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,167 @@
|
||||
import {AfterViewInit, ChangeDetectionStrategy, Component, OnInit} from "@angular/core";
|
||||
import {Repository} from "../../../../../api/models/Repository";
|
||||
import {LoggingService} from "../../../../services/logging/logging.service";
|
||||
import {RepositoryService} from "../../../../services/repository/repository.service";
|
||||
import {JobService} from "../../../../services/job/job.service";
|
||||
import {StateService} from "../../../../services/state/state.service";
|
||||
import {MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {
|
||||
AddRepositoryDialogComponent
|
||||
} from "../../../shared/repository/repository/add-repository-dialog/add-repository-dialog.component";
|
||||
import {BehaviorSubject} from "rxjs";
|
||||
import {BusyDialogComponent} from "../../../shared/app-common/busy-dialog/busy-dialog.component";
|
||||
import {DownloadDaemonDialogComponent} from "../download-daemon-dialog/download-daemon-dialog.component";
|
||||
import {AboutDialogComponent} from "./about-dialog/about-dialog.component";
|
||||
|
||||
type BusyDialogContext = { message: BehaviorSubject<string>, dialog: MatDialogRef<BusyDialogComponent> };
|
||||
|
||||
@Component({
|
||||
selector: "app-repository-overview",
|
||||
templateUrl: "./repository-overview.component.html",
|
||||
styleUrls: ["./repository-overview.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.Default
|
||||
})
|
||||
export class RepositoryOverviewComponent implements OnInit, AfterViewInit {
|
||||
|
||||
public repositories: Repository[] = [];
|
||||
|
||||
constructor(
|
||||
private logger: LoggingService,
|
||||
private repoService: RepositoryService,
|
||||
private jobService: JobService,
|
||||
private stateService: StateService,
|
||||
public dialog: MatDialog
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.repoService.repositories.subscribe(repos => this.repositories = repos);
|
||||
}
|
||||
|
||||
public async ngAfterViewInit() {
|
||||
await this.checkAndPromptDaemonExecutable();
|
||||
}
|
||||
|
||||
public async startDaemonAndSelectRepository(repository: Repository) {
|
||||
try {
|
||||
let dialogContext = this.openStartupDialog(repository);
|
||||
let daemonRunning = await this.repoService.checkDaemonRunning(
|
||||
repository.path!);
|
||||
if (!daemonRunning) {
|
||||
dialogContext.message.next("Starting repository daemon...");
|
||||
await this.repoService.startDaemon(repository.path!);
|
||||
|
||||
await new Promise((res, _) => {
|
||||
setTimeout(res, 2000); // wait for the daemon to start
|
||||
});
|
||||
}
|
||||
await this.selectRepository(repository, dialogContext);
|
||||
} catch (err: any) {
|
||||
this.logger.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
public async selectRepository(repository: Repository, dialogContext?: BusyDialogContext) {
|
||||
dialogContext = dialogContext ?? this.openStartupDialog(repository);
|
||||
try {
|
||||
dialogContext.message.next("Opening repository...");
|
||||
await this.repoService.setRepository(repository);
|
||||
await this.runRepositoryStartupTasks(dialogContext);
|
||||
dialogContext.message.next("Restoring previous tabs...");
|
||||
await this.repoService.loadRepositories();
|
||||
dialogContext.dialog.close(true);
|
||||
} catch (err: any) {
|
||||
this.logger.error(err);
|
||||
dialogContext.message.next(
|
||||
"Failed to open repository: " + err.toString());
|
||||
await this.forceCloseRepository();
|
||||
setTimeout(() => dialogContext!.dialog.close(true), 1000);
|
||||
}
|
||||
}
|
||||
|
||||
public openAddRepositoryDialog() {
|
||||
this.dialog.open(AddRepositoryDialogComponent, {
|
||||
disableClose: true,
|
||||
minWidth: "30%",
|
||||
minHeight: "30%",
|
||||
});
|
||||
}
|
||||
|
||||
public async onOpenRepository(repository: Repository) {
|
||||
if (!repository.local) {
|
||||
await this.selectRepository(repository);
|
||||
} else {
|
||||
await this.startDaemonAndSelectRepository(repository);
|
||||
}
|
||||
}
|
||||
|
||||
public openAboutDialog(): void {
|
||||
this.dialog.open(AboutDialogComponent, {
|
||||
minWidth: "30%",
|
||||
minHeight: "50%",
|
||||
});
|
||||
}
|
||||
|
||||
private async forceCloseRepository() {
|
||||
try {
|
||||
await this.repoService.closeSelectedRepository();
|
||||
} catch {
|
||||
}
|
||||
try {
|
||||
await this.repoService.disconnectSelectedRepository();
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
private async runRepositoryStartupTasks(dialogContext: BusyDialogContext): Promise<void> {
|
||||
dialogContext.message.next("Checking integrity...");
|
||||
await this.jobService.runJob("CheckIntegrity");
|
||||
dialogContext.message.next("Running a vacuum on the database...");
|
||||
await this.jobService.runJob("Vacuum");
|
||||
dialogContext.message.next(
|
||||
"Migrating content descriptors to new format...");
|
||||
await this.jobService.runJob("MigrateContentDescriptors");
|
||||
dialogContext.message.next("Calculating repository sizes...");
|
||||
await this.jobService.runJob("CalculateSizes", false);
|
||||
dialogContext.message.next("Generating missing thumbnails...");
|
||||
await this.jobService.runJob("GenerateThumbnails");
|
||||
dialogContext.message.next("Finished repository startup");
|
||||
}
|
||||
|
||||
private openStartupDialog(repository: Repository): BusyDialogContext {
|
||||
const dialogMessage = new BehaviorSubject<string>(
|
||||
"Opening repository...");
|
||||
let dialog = this.dialog.open(BusyDialogComponent, {
|
||||
data: {
|
||||
title: `Opening repository '${repository.name}'`,
|
||||
message: dialogMessage,
|
||||
allowCancel: true,
|
||||
}, disableClose: true,
|
||||
minWidth: "30%",
|
||||
minHeight: "30%",
|
||||
});
|
||||
dialog.afterClosed().subscribe(async (result) => {
|
||||
if (!result) {
|
||||
await this.forceCloseRepository();
|
||||
}
|
||||
});
|
||||
|
||||
return { message: dialogMessage, dialog };
|
||||
}
|
||||
|
||||
private async checkAndPromptDaemonExecutable() {
|
||||
if (!await this.repoService.checkDameonConfigured()) {
|
||||
const result = await this.dialog.open(
|
||||
DownloadDaemonDialogComponent,
|
||||
{
|
||||
disableClose: true,
|
||||
}
|
||||
).afterClosed().toPromise();
|
||||
if (result) {
|
||||
// recursion avoidance
|
||||
setTimeout(
|
||||
async () => await this.checkAndPromptDaemonExecutable(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
<span (click)="this.openUrl()" class="highlight">
|
||||
<ng-content></ng-content>
|
||||
</span>
|
@ -0,0 +1,15 @@
|
||||
@import "src/colors";
|
||||
|
||||
.highlight {
|
||||
cursor: pointer;
|
||||
color: $primary-lighter-20;
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
color: $primary-lighter-30;
|
||||
}
|
||||
|
||||
&:active {
|
||||
color: $accent;
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
import {ComponentFixture, TestBed} from "@angular/core/testing";
|
||||
|
||||
import {ExternalUrlComponent} from "./external-url.component";
|
||||
|
||||
describe("ExternalUrlComponent", () => {
|
||||
let component: ExternalUrlComponent;
|
||||
let fixture: ComponentFixture<ExternalUrlComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ExternalUrlComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(ExternalUrlComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it("should create", () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
@ -0,0 +1,31 @@
|
||||
import {ChangeDetectionStrategy, Component, Input} from "@angular/core";
|
||||
import {shell} from "@tauri-apps/api";
|
||||
|
||||
@Component({
|
||||
selector: "app-external-url",
|
||||
templateUrl: "./external-url.component.html",
|
||||
styleUrls: ["./external-url.component.scss"],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class ExternalUrlComponent {
|
||||
|
||||
@Input() href!: string;
|
||||
private opening = false;
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public async openUrl() {
|
||||
if (this.opening) {
|
||||
return;
|
||||
}
|
||||
this.opening = true;
|
||||
try {
|
||||
await shell.open(this.href);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
this.opening = false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
#!/bin/env python3
|
||||
import shutil as shut
|
||||
import os
|
||||
from lib import *
|
||||
from clean import clean
|
||||
from check import check, check_daemon_tooling, check_ui_tooling
|
||||
from typing import List
|
||||
|
||||
|
||||
build_output = 'out'
|
||||
verbose = False
|
||||
ffmpeg = False
|
||||
install_tooling = False
|
||||
|
||||
windows = os.name == 'nt'
|
||||
|
||||
|
||||
def main():
|
||||
opts = parse_args()
|
||||
|
||||
global install_tooling
|
||||
global build_output
|
||||
global verbose
|
||||
global ffmpeg
|
||||
global install_tooling
|
||||
|
||||
build_output = opts.output if opts.output else build_output
|
||||
verbose = opts.verbose
|
||||
ffmpeg = opts.ffmpeg
|
||||
install_tooling = opts.install_tooling
|
||||
|
||||
build(opts.component, opts.bundles)
|
||||
|
||||
|
||||
def parse_args():
|
||||
import argparse
|
||||
parser = argparse.ArgumentParser(description='Build mediarepo')
|
||||
parser.add_argument(
|
||||
'component', type=str, nargs='?', default='all', choices=['daemon', 'ui', 'all'])
|
||||
parser.add_argument(
|
||||
'--verbose', action='store_true', help='Verbose build')
|
||||
parser.add_argument(
|
||||
'--output', action='store', help='Build output directory')
|
||||
parser.add_argument(
|
||||
'--ffmpeg', action='store_true', help='Build with ffmpeg')
|
||||
parser.add_argument('--install-tooling',
|
||||
action='store_true', help='Install tooling')
|
||||
parser.add_argument('--bundles', nargs='+',
|
||||
help='UI bundles to build')
|
||||
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def build(component: str, bundles: List[str] = None):
|
||||
'''Builds the selected component'''
|
||||
clean()
|
||||
create_output_dir()
|
||||
|
||||
if component == 'all':
|
||||
check(install_tooling)
|
||||
build_daemon()
|
||||
build_ui(bundles)
|
||||
elif component == 'daemon':
|
||||
check_daemon_tooling()
|
||||
build_daemon()
|
||||
elif component == 'ui':
|
||||
check_ui_tooling(install_tooling)
|
||||
build_ui(bundles)
|
||||
|
||||
print('Build complete')
|
||||
|
||||
|
||||
def build_daemon():
|
||||
'''Builds daemon'''
|
||||
cargo('fetch', 'mediarepo-daemon')
|
||||
|
||||
if not ffmpeg:
|
||||
cargo('build --release --frozen --no-default-features', 'mediarepo-daemon')
|
||||
else:
|
||||
cargo('build --release --frozen', 'mediarepo-daemon')
|
||||
|
||||
if windows:
|
||||
store_artifact('mediarepo-daemon/target/release/mediarepo-daemon.exe')
|
||||
else:
|
||||
store_artifact('mediarepo-daemon/target/release/mediarepo-daemon')
|
||||
|
||||
|
||||
def build_ui(bundles: List[str] = None):
|
||||
'''Builds UI'''
|
||||
yarn('install', 'mediarepo-ui')
|
||||
|
||||
if bundles is not None:
|
||||
cargo('tauri build --bundles ' + ' '.join(bundles), 'mediarepo-ui')
|
||||
else:
|
||||
cargo('tauri build ', 'mediarepo-ui')
|
||||
|
||||
if windows:
|
||||
store_artifact(
|
||||
'mediarepo-ui/src-tauri/target/release/mediarepo-ui.exe')
|
||||
else:
|
||||
store_artifact('mediarepo-ui/src-tauri/target/release/mediarepo-ui')
|
||||
|
||||
store_artifact('mediarepo-ui/src-tauri/target/release/bundle/')
|
||||
|
||||
|
||||
def create_output_dir():
|
||||
'''Creates build output directory'''
|
||||
if not os.path.exists(build_output):
|
||||
os.mkdir(build_output)
|
||||
|
||||
|
||||
def store_artifact(path: str):
|
||||
'''Stores a build artifact'''
|
||||
print('Storing {}'.format(path))
|
||||
if os.path.isdir(path):
|
||||
shut.copytree(path, os.path.join(
|
||||
build_output, os.path.basename(path)), dirs_exist_ok=True)
|
||||
elif os.path.isfile(path):
|
||||
shut.copy(path, build_output)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,57 @@
|
||||
#!/bin/env python3
|
||||
from lib import *
|
||||
import argparse
|
||||
import os
|
||||
|
||||
|
||||
tauri_cli_version = '1.0.0-rc.5'
|
||||
windows = os.name == 'nt'
|
||||
|
||||
|
||||
def main():
|
||||
opts = parse_args()
|
||||
check(opts.install)
|
||||
|
||||
|
||||
def parse_args():
|
||||
'''Parses command line arguments'''
|
||||
args = argparse.ArgumentParser(description='Build mediarepo')
|
||||
args.add_argument('--install', action='store_true',
|
||||
help='Install tools that can be installed automatically')
|
||||
return args.parse_args()
|
||||
|
||||
|
||||
def check(install_tooling: bool = False):
|
||||
'''Checks dependencies'''
|
||||
check_daemon_tooling()
|
||||
check_ui_tooling(install_tooling)
|
||||
print('All checks passed')
|
||||
|
||||
|
||||
def check_daemon_tooling():
|
||||
'''Checks dependencies for daemon'''
|
||||
check_exec('clang')
|
||||
check_exec('cargo')
|
||||
|
||||
|
||||
def check_ui_tooling(install_tooling: bool = False):
|
||||
'''Checks dependencies for UI'''
|
||||
|
||||
if not windows:
|
||||
check_exec('wget')
|
||||
check_exec('curl')
|
||||
check_exec('file')
|
||||
|
||||
check_exec('clang')
|
||||
check_exec('cargo')
|
||||
check_exec('node')
|
||||
check_exec('npm')
|
||||
check_yarn(install_tooling)
|
||||
check_ng(install_tooling)
|
||||
|
||||
if install_tooling:
|
||||
install_tauri_cli(tauri_cli_version)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,27 @@
|
||||
#!/bin/env python3
|
||||
import os
|
||||
import shutil as shut
|
||||
import argparse
|
||||
|
||||
|
||||
def main():
|
||||
opts = parse_args()
|
||||
clean(opts.output if opts.output else 'out')
|
||||
|
||||
|
||||
def parse_args():
|
||||
'''Parses command line arguments'''
|
||||
args = argparse.ArgumentParser(description='Build mediarepo')
|
||||
args.add_argument('--output', action='store', help='Build output directory')
|
||||
return args.parse_args()
|
||||
|
||||
|
||||
def clean(build_output: str = 'out'):
|
||||
'''Removes build output'''
|
||||
if os.path.exists(build_output):
|
||||
shut.rmtree(build_output)
|
||||
print('Cleaned')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,57 @@
|
||||
import subprocess
|
||||
import shutil as shut
|
||||
|
||||
|
||||
def install_tauri_cli(version: str):
|
||||
cargo('install tauri-cli --version ^{}'.format(version))
|
||||
|
||||
|
||||
def check_ng(install: bool = False):
|
||||
'''Checks if ng is available and installs it
|
||||
if the install flag is set'''
|
||||
if not check_exec('ng'):
|
||||
if install:
|
||||
npm('install -g @angular/cli')
|
||||
else:
|
||||
raise Exception('ng not found')
|
||||
|
||||
|
||||
def check_yarn(install: bool = False):
|
||||
'''Checks if yarn is available and installs it
|
||||
if the install flag is set'''
|
||||
if not check_exec('yarn'):
|
||||
if install:
|
||||
npm('install -g yarn')
|
||||
else:
|
||||
raise Exception('yarn not found')
|
||||
|
||||
|
||||
def yarn(cmd: str, dir: str = None) -> str:
|
||||
'''Executes yarn in a given directory'''
|
||||
exec('yarn {}'.format(cmd), dir)
|
||||
|
||||
|
||||
def cargo(cmd: str, dir: str = None):
|
||||
'''Executes cargo in a given directory'''
|
||||
exec('cargo {}'.format(cmd), dir)
|
||||
|
||||
|
||||
def npm(cmd: str, dir: str = None) -> str:
|
||||
'''Executes npm in a given directory'''
|
||||
exec('npm {}'.format(cmd), dir)
|
||||
|
||||
|
||||
def check_exec(name: str) -> bool:
|
||||
'''Checks if a command is available'''
|
||||
if shut.which(name) is None:
|
||||
print('{} not found'.format(name))
|
||||
return False
|
||||
exec('{} --version'.format(name))
|
||||
return True
|
||||
|
||||
|
||||
def exec(cmd: str, dir: str = None) -> str:
|
||||
'''Executes a command in a given directory'''
|
||||
print('Running: {}'.format(cmd))
|
||||
child = subprocess.run(cmd, shell=True, cwd=dir)
|
||||
child.check_returncode()
|
Loading…
Reference in New Issue