diff --git a/.dockerignore b/.dockerignore
index e5c8b1c..ced1b76 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,8 @@
# compiled output
out
+__pycache__
+target
+dist
# IDEs and editors
mediarepo-api/.idea
@@ -36,4 +39,4 @@ mediarepo-daemon/*.folded
# api
mediarepo-api/.idea
-mediarepo-api/target
+mediarepo-api/target
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7a29a7f..3135cc3 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -91,7 +91,7 @@ jobs:
python-version: '^3.7'
- name: Build
- run: python build.py build --daemon --verbose
+ run: python scripts/build.py daemon --verbose --install-tooling
- name: Upload artifacts
if: ${{ !env.ACT }}
@@ -150,7 +150,7 @@ jobs:
DEBIAN_FRONTEND=noninteractive sudo apt-get install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev -y
- name: Build project
- run: python build.py build --ui --verbose
+ run: python scripts/build.py ui --verbose --install-tooling
- name: Upload artifacts
if: ${{ !env.ACT }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 757ceb7..81f77d9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -51,7 +51,7 @@ jobs:
python-version: '^3.7'
- name: Build Daemon
- run: python build.py build --daemon --verbose
+ run: python scripts/build.py daemon --verbose --install-tooling
- uses: vimtor/action-zip@v1
with:
@@ -113,7 +113,7 @@ jobs:
DEBIAN_FRONTEND=noninteractive sudo apt-get install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev -y
- name: Build project
- run: python build.py build --ui --verbose
+ run: python scripts/build.py ui --verbose --install-tooling
- uses: vimtor/action-zip@v1
with:
diff --git a/.gitignore b/.gitignore
index 2ca27a3..df4f7a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
/out-tsc
/target
/out
+__pycache__
# IDEs and editors
mediarepo-api/.idea
diff --git a/Dockerfile b/Dockerfile
index e9caab0..d1dc609 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,7 +6,7 @@ WORKDIR /usr/src
COPY mediarepo-api ./mediarepo-api
COPY mediarepo-daemon ./mediarepo-daemon
COPY mediarepo-ui ./mediarepo-ui
-COPY build.py .
+COPY scripts ./scripts
RUN apt-get update
RUN apt-get install -y \
@@ -37,4 +37,6 @@ RUN apt remove cmdtest -y
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y
ENV PATH="/root/.cargo/bin:${PATH}"
-RUN python3 build.py build
+RUN python3 scripts/clean.py
+RUN python3 scripts/check.py --install
+RUN python3 scripts/build.py all --verbose --ffmpeg
diff --git a/README.md b/README.md
index dd07f19..a837fd3 100644
--- a/README.md
+++ b/README.md
@@ -69,23 +69,32 @@ You also need to have a working [python](https://www.python.org/) installation o
After all required dependencies are installed and tools are accessible in the `PATH`, you can build the project like follows:
-> Note: You might need to make the `build.py` file executable with `chmod +x build.py`.
+Check (and install) required tooling:
+```sh
+$ ./scripts/check.py --install
+```
+> Note: this only installs tools that are installable via cargo or npm
All Componens:
```sh
-$ ./build.py build --ffmpeg
+$ ./scripts/build.py all --ffmpeg
```
Daemon only:
```sh
-$ ./build.py build --daemon --ffmpeg
+$ ./scripts/build.py daemon --ffmpeg
```
If you don't want to build with ffmpeg support omit the `--ffmpeg` flag.
UI only:
```sh
-$ ./build.py build --ui
+$ ./scripts/build.py ui
+```
+
+Clean the output directory:
+```sh
+$ ./scripts/clean.py
```
After building the `out` directory contains all the built binaries and bundles.
diff --git a/build.py b/build.py
deleted file mode 100755
index fc6be03..0000000
--- a/build.py
+++ /dev/null
@@ -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()
diff --git a/mediarepo-daemon/Cargo.lock b/mediarepo-daemon/Cargo.lock
index 09f9992..a9e5bc4 100644
--- a/mediarepo-daemon/Cargo.lock
+++ b/mediarepo-daemon/Cargo.lock
@@ -1398,7 +1398,7 @@ dependencies = [
[[package]]
name = "mediarepo-daemon"
-version = "1.0.0-rc.1"
+version = "1.0.0-rc.3"
dependencies = [
"console-subscriber",
"glob",
diff --git a/mediarepo-daemon/Cargo.toml b/mediarepo-daemon/Cargo.toml
index 6cf4e00..5268dfc 100644
--- a/mediarepo-daemon/Cargo.toml
+++ b/mediarepo-daemon/Cargo.toml
@@ -4,7 +4,7 @@ default-members = ["mediarepo-core", "mediarepo-database", "mediarepo-logic", "m
[package]
name = "mediarepo-daemon"
-version = "1.0.0-rc.2"
+version = "1.0.0-rc.3"
edition = "2018"
license = "gpl-3"
repository = "https://github.com/Trivernis/mediarepo-daemon"
diff --git a/mediarepo-ui/package.json b/mediarepo-ui/package.json
index 2097b81..4bec025 100644
--- a/mediarepo-ui/package.json
+++ b/mediarepo-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "mediarepo-ui",
- "version": "1.0.0-rc.2",
+ "version": "1.0.0-rc.3",
"scripts": {
"ng": "ng",
"start": "ng serve",
diff --git a/mediarepo-ui/src-tauri/Cargo.lock b/mediarepo-ui/src-tauri/Cargo.lock
index 40b6e09..939030c 100644
--- a/mediarepo-ui/src-tauri/Cargo.lock
+++ b/mediarepo-ui/src-tauri/Cargo.lock
@@ -40,7 +40,7 @@ checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
[[package]]
name = "app"
-version = "1.0.0-rc.1"
+version = "1.0.0-rc.3"
dependencies = [
"mediarepo-api",
"serde",
diff --git a/mediarepo-ui/src-tauri/Cargo.toml b/mediarepo-ui/src-tauri/Cargo.toml
index a84e54c..950b214 100644
--- a/mediarepo-ui/src-tauri/Cargo.toml
+++ b/mediarepo-ui/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "app"
-version = "1.0.0-rc.2"
+version = "1.0.0-rc.3"
description = "The UI for the mediarepo media management tool"
authors = ["you"]
license = ""
diff --git a/mediarepo-ui/src/app/components/core/core.component.html b/mediarepo-ui/src/app/components/core/core.component.html
index def7489..94f56f3 100644
--- a/mediarepo-ui/src/app/components/core/core.component.html
+++ b/mediarepo-ui/src/app/components/core/core.component.html
@@ -1,7 +1,7 @@
-
+
diff --git a/mediarepo-ui/src/app/components/core/core.module.ts b/mediarepo-ui/src/app/components/core/core.module.ts
index a514aba..bec4607 100644
--- a/mediarepo-ui/src/app/components/core/core.module.ts
+++ b/mediarepo-ui/src/app/components/core/core.module.ts
@@ -39,7 +39,9 @@ import {MatToolbarModule} from "@angular/material/toolbar";
import {
RepositoryDetailsViewComponent
} from "./repositories-tab/repository-details-view/repository-details-view.component";
-import { EmptyTabComponent } from './empty-tab/empty-tab.component';
+import {EmptyTabComponent} from "./empty-tab/empty-tab.component";
+import {RepositoryOverviewComponent} from "./repositories-tab/repository-overview/repository-overview.component";
+import {AboutDialogComponent} from "./repositories-tab/repository-overview/about-dialog/about-dialog.component";
@NgModule({
@@ -54,6 +56,8 @@ import { EmptyTabComponent } from './empty-tab/empty-tab.component';
DownloadDaemonDialogComponent,
RepositoryDetailsViewComponent,
EmptyTabComponent,
+ RepositoryOverviewComponent,
+ AboutDialogComponent,
],
exports: [
CoreComponent,
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.html b/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.html
index 48622cd..4093b2a 100644
--- a/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.html
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.html
@@ -1,18 +1,6 @@
-
-
-
-
-
-
-
- There are no repositories yet. You can create a repository or add an existing one.
-
-
-
+
-
-
+
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.scss b/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.scss
index 4e2ab92..2e3c1d6 100644
--- a/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.scss
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.scss
@@ -1,41 +1,3 @@
-.repository-container {
- margin: 1em;
-}
-
-.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-direction: column;
- overflow-y: auto;
- height: calc(100% - 5em);
-}
-
-app-repository-card {
- position: relative;
-}
-
-app-repository-details-view, .repo-details {
+app-repository-details-view {
height: 100%;
}
-
-.add-repository-prompt {
- button {
- font-size: 1.5em;
- padding: 0.5em 1em;
- border-radius: 0.5em;
- }
-}
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.ts b/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.ts
index 3c4f421..4b17e68 100644
--- a/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.ts
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repositories-tab.component.ts
@@ -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
, dialog: MatDialogRef };
@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 {
- 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(
- "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);
}
}
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.html b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.html
new file mode 100644
index 0000000..9b57198
--- /dev/null
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.html
@@ -0,0 +1,24 @@
+
+ About mediarepo
+
+
+
+ {{version | async}}
+
+
+ {{tauriVersion | async}}
+
+
+
+
Built with
+
+
+
+
+
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.scss b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.scss
new file mode 100644
index 0000000..607eefd
--- /dev/null
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.scss
@@ -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;
+}
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.spec.ts b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.spec.ts
new file mode 100644
index 0000000..e95648e
--- /dev/null
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [AboutDialogComponent]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(AboutDialogComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.ts b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.ts
new file mode 100644
index 0000000..e4c49ed
--- /dev/null
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/about-dialog/about-dialog.component.ts
@@ -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();
+ public tauriVersion = new Subject();
+ 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) {
+ }
+
+ public async ngOnInit() {
+ this.version.next(await app.getVersion());
+ this.tauriVersion.next(await app.getTauriVersion());
+ }
+}
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.html b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.html
new file mode 100644
index 0000000..8220df4
--- /dev/null
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.html
@@ -0,0 +1,18 @@
+
+ All Repositories
+
+
+
+
+
+
+
+ There are no repositories yet. You can create a repository or add an existing one.
+
+
+
+
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.scss b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.scss
new file mode 100644
index 0000000..e16282d
--- /dev/null
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.scss
@@ -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;
+ }
+}
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.spec.ts b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.spec.ts
new file mode 100644
index 0000000..48e0d55
--- /dev/null
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [RepositoryOverviewComponent]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(RepositoryOverviewComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.ts b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.ts
new file mode 100644
index 0000000..966a479
--- /dev/null
+++ b/mediarepo-ui/src/app/components/core/repositories-tab/repository-overview/repository-overview.component.ts
@@ -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, dialog: MatDialogRef };
+
+@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 {
+ 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(
+ "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);
+ }
+ }
+ }
+}
diff --git a/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts b/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts
index a556c14..0a25e48 100644
--- a/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts
+++ b/mediarepo-ui/src/app/components/shared/app-common/app-common.module.ts
@@ -25,6 +25,7 @@ import {MatRippleModule} from "@angular/material/core";
import {FlapButtonComponent} from "./flap-button/flap-button.component";
import {MiddleCenteredComponent} from "./middle-centered/middle-centered.component";
import {FormatBytesPipe} from "./pipes/format-bytes.pipe";
+import {ExternalUrlComponent} from "./external-url/external-url.component";
@NgModule({
@@ -44,6 +45,7 @@ import {FormatBytesPipe} from "./pipes/format-bytes.pipe";
FlapButtonComponent,
MiddleCenteredComponent,
FormatBytesPipe,
+ ExternalUrlComponent,
],
exports: [
ConfirmDialogComponent,
@@ -60,6 +62,7 @@ import {FormatBytesPipe} from "./pipes/format-bytes.pipe";
FlapButtonComponent,
MiddleCenteredComponent,
FormatBytesPipe,
+ ExternalUrlComponent,
],
imports: [
CommonModule,
diff --git a/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.html b/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.html
new file mode 100644
index 0000000..7148987
--- /dev/null
+++ b/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.html
@@ -0,0 +1,3 @@
+
+
+
diff --git a/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.scss b/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.scss
new file mode 100644
index 0000000..3fccf25
--- /dev/null
+++ b/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.scss
@@ -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;
+ }
+}
diff --git a/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.spec.ts b/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.spec.ts
new file mode 100644
index 0000000..77018c4
--- /dev/null
+++ b/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.spec.ts
@@ -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;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [ExternalUrlComponent]
+ })
+ .compileComponents();
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(ExternalUrlComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it("should create", () => {
+ expect(component).toBeTruthy();
+ });
+});
diff --git a/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.ts b/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.ts
new file mode 100644
index 0000000..7d3c10b
--- /dev/null
+++ b/mediarepo-ui/src/app/components/shared/app-common/external-url/external-url.component.ts
@@ -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;
+ }
+ }
+}
diff --git a/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.html b/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.html
index 9edc4cb..22dfb2f 100644
--- a/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.html
+++ b/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.html
@@ -1,4 +1,6 @@
-
+
{{attributeName}}:
diff --git a/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.scss b/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.scss
index eb64cef..e6e3180 100644
--- a/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.scss
+++ b/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.scss
@@ -1,9 +1,30 @@
.metadata-entry-inner {
width: 100%;
display: flex;
- flex-direction: column;
+
+ &.display-line {
+ display: flex;
+ flex-direction: row;
+
+ .metadata-attribute, .metadata-value {
+ width: 50%;
+ }
+
+ .metadata-attribute {
+ text-align: right;
+ }
+
+ .metadata-value {
+ text-align: left;
+ }
+ }
+
+ &.display-multiline {
+ flex-direction: column;
+ }
}
+
.metadata-attribute {
opacity: 0.8;
text-overflow: ellipsis;
@@ -15,3 +36,4 @@
word-break: break-all;
user-select: text;
}
+
diff --git a/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.ts b/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.ts
index 66bc84d..8a585b4 100644
--- a/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.ts
+++ b/mediarepo-ui/src/app/components/shared/app-common/metadata-entry/metadata-entry.component.ts
@@ -1,14 +1,15 @@
-import {Component, Input} from "@angular/core";
+import {ChangeDetectionStrategy, Component, Input} from "@angular/core";
@Component({
selector: "app-metadata-entry",
templateUrl: "./metadata-entry.component.html",
- styleUrls: ["./metadata-entry.component.scss"]
+ styleUrls: ["./metadata-entry.component.scss"],
+ changeDetection: ChangeDetectionStrategy.OnPush
})
export class MetadataEntryComponent {
+ @Input() display: "multiline" | "line" = "multiline";
@Input() attributeName!: string;
- @Input() value!: string;
constructor() {
}
diff --git a/scripts/build.py b/scripts/build.py
new file mode 100755
index 0000000..d5fe6c0
--- /dev/null
+++ b/scripts/build.py
@@ -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()
diff --git a/scripts/check.py b/scripts/check.py
new file mode 100755
index 0000000..340dde9
--- /dev/null
+++ b/scripts/check.py
@@ -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()
diff --git a/scripts/clean.py b/scripts/clean.py
new file mode 100755
index 0000000..6e87d7a
--- /dev/null
+++ b/scripts/clean.py
@@ -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()
\ No newline at end of file
diff --git a/scripts/lib.py b/scripts/lib.py
new file mode 100644
index 0000000..8721381
--- /dev/null
+++ b/scripts/lib.py
@@ -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()