Merge pull request #15 from Trivernis/develop

Release Candidate 2
main v1.0.0-rc.2
Julius Riegel 2 years ago committed by GitHub
commit 9180eb6efb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,39 @@
# compiled output
out
# IDEs and editors
mediarepo-api/.idea
mediarepo-daemon/.idea
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# System Files
.DS_Store
Thumbs.db
# other
*.zip
.vscode
# ui
mediarepo-ui/.angular
mediarepo-ui/.idea
mediarepo-ui/dist
mediarepo-ui/node_modules
mediarepo-ui/src-tauri/target
# daemon
mediarepo-daemon/.idea
mediarepo-daemon/target
mediarepo-daemon/*.svg
mediarepo-daemon/*.folded
# api
mediarepo-api/.idea
mediarepo-api/target

@ -48,13 +48,13 @@ jobs:
DEBIAN_FRONTEND=noninteractive sudo apt-get install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev -y
- name: Build
run: cargo build
run: cargo build --verbose
- name: Build API
run: cargo build --features=client-api
run: cargo build --features=client-api --verbose
- name: Build Plugin
run: cargo build --features=tauri-plugin
run: cargo build --features=tauri-plugin --verbose
- name: Test
run: cargo test --all-features
@ -64,11 +64,6 @@ jobs:
# to run fewer steps in parallel
needs: build-api
defaults:
run:
shell: bash
working-directory: mediarepo-daemon
strategy:
fail-fast: false
matrix:
@ -90,8 +85,13 @@ jobs:
restore-keys: |
${{ runner.os }}-cargo-
- name: setup python
uses: actions/setup-python@v2
with:
python-version: '^3.7'
- name: Build
run: cargo build --release --no-default-features
run: python build.py build --daemon --verbose
- name: Upload artifacts
if: ${{ !env.ACT }}
@ -99,18 +99,13 @@ jobs:
with:
name: mediarepo-daemon-${{ runner.os }}
path: |
target/release/mediarepo-daemon*
out/*
build-ui:
# to run fewer steps in parallel
needs: build-api
defaults:
run:
shell: bash
working-directory: mediarepo-ui
runs-on: ${{ matrix.os }}
strategy:
@ -142,17 +137,10 @@ jobs:
with:
node-version: 16
- name: Install Tauri
run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri
- name: Install Angular CLI
run: npm install -g @angular/cli
- name: Install yarn
run: npm install -g yarn
- name: Install dependencies
run: yarn install
- name: setup python
uses: actions/setup-python@v2
with:
python-version: '^3.7'
- name: Install OS-specific dependencies
uses: knicknic/os-specific-run@v1.0.3
@ -162,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: cargo tauri build
run: python build.py build --ui --verbose
- name: Upload artifacts
if: ${{ !env.ACT }}
@ -170,5 +158,4 @@ jobs:
with:
name: mediarepo-ui-${{ runner.os }}-release
path: |
src-tauri/target/release/bundle
src-tauri/target/release/mediarepo-ui*
out/*

@ -1,9 +1,8 @@
name: Create pre-release
on:
push:
branches:
- main
workflow_dispatch:
jobs:
create-release-draft:
name: pre-release
@ -31,10 +30,6 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
working-directory: mediarepo-daemon
steps:
- uses: actions/checkout@v2
if: ${{ !env.ACT }}
@ -50,12 +45,17 @@ jobs:
restore-keys: |
${{ runner.os }}-cargo-
- name: setup python
uses: actions/setup-python@v2
with:
python-version: '^3.7'
- name: Build Daemon
run: cargo build --release --no-default-features
run: python build.py build --daemon --verbose
- uses: vimtor/action-zip@v1
with:
files: mediarepo-daemon/target/release/mediarepo-daemon mediarepo-daemon/target/release/mediarepo-daemon.exe
files: out/
dest: mediarepo-daemon-${{ runner.os }}.zip
- name: Upload Release Asset
@ -76,10 +76,6 @@ jobs:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash
working-directory: mediarepo-ui
steps:
- uses: actions/checkout@v2
if: ${{ !env.ACT }}
@ -99,23 +95,16 @@ jobs:
${{ runner.os }}-release-dependencies-
${{ runner.os }}-dependencies-
- name: setup python
uses: actions/setup-python@v2
with:
python-version: '^3.7'
- name: Use Node.js 16
uses: actions/setup-node@v1
with:
node-version: 16
- name: Install Tauri
run: cargo install tauri-cli --git https://github.com/tauri-apps/tauri
- name: Install Angular CLI
run: npm install -g @angular/cli
- name: Install yarn
run: npm install -g yarn
- name: Install dependencies
run: yarn install
- name: Install OS-specific dependencies
uses: knicknic/os-specific-run@v1.0.3
with:
@ -124,11 +113,11 @@ jobs:
DEBIAN_FRONTEND=noninteractive sudo apt-get install libwebkit2gtk-4.0-dev libgtk-3-dev libappindicator3-dev -y
- name: Build project
run: cargo tauri build
run: python build.py build --ui --verbose
- uses: vimtor/action-zip@v1
with:
files: mediarepo-ui/src-tauri/target/release/bundle mediarepo-ui/src-tauri/target/release/mediarepo-ui mediarepo-ui/src-tauri/target/release/mediarepo-ui.exe
files: out/
dest: mediarepo-ui-${{ runner.os }}.zip
- name: Upload Release Asset

4
.gitignore vendored

@ -3,6 +3,7 @@
/tmp
/out-tsc
/target
/out
# IDEs and editors
mediarepo-api/.idea
@ -29,4 +30,5 @@ mediarepo-ui/.idea
Thumbs.db
# other
*.zip
*.zip
.vscode

@ -0,0 +1,87 @@
# Contributing
Before contributing to this repository, please discuss the changes you're willing to make via an issue or on discord before
making those changes.
## Pull Request Process
1. Always submit Pull Requests to the develop branch unless it contains a hotfix
2. Increase the API version according to the [SemVer](https://semver.org/) rules if you've done changes on the API
3. Make sure that the project can be built and passes all automatic tests
4. Your Pull Request will be merged after approval
# Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
me@trivernis.net.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).

@ -0,0 +1,40 @@
ARG DEBIAN_RELEASE=bullseye
FROM bitnami/minideb:${DEBIAN_RELEASE} AS builder
WORKDIR /usr/src
COPY mediarepo-api ./mediarepo-api
COPY mediarepo-daemon ./mediarepo-daemon
COPY mediarepo-ui ./mediarepo-ui
COPY build.py .
RUN apt-get update
RUN apt-get install -y \
build-essential \
libssl-dev \
libgtk-3-dev \
libappindicator3-0.1-cil-dev \
patchelf \
librsvg2-dev \
curl \
wget \
pkg-config \
libavutil-dev \
libavformat-dev \
libavcodec-dev \
libavfilter-dev \
libavdevice-dev \
clang \
nodejs \
npm \
libsoup2.4-dev \
libwebkit2gtk-4.0-dev \
file \
python
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

@ -2,9 +2,8 @@
mediarepo
</h1>
<p align="center">
<img src="https://github.com/Trivernis/mediarepo/raw/main/mediarepo-ui/src-tauri/icons/64x64.png"/>
<img src="https://github.com/Trivernis/mediarepo/raw/main/mediarepo-ui/src-tauri/icons/64x64.png?raw=true"/>
</p>
<h3 align="center" style="color:red">mediarepo is a work in progress</h3>
<p align="center">
<a href="https://github.com/Trivernis/mediarepo/actions/workflows/build.yml">
<img src="https://img.shields.io/github/workflow/status/trivernis/mediarepo/Build%20and%20test?style=for-the-badge">
@ -20,22 +19,81 @@ mediarepo
- - -
Mediarepo is a tool for managing media files.
> Mediarepo is a tool for managing media files.
It works similar to image boards (boorus) as it allows one to assign tags to media entries and
search for entries by using those tags. It is inspired by [hydrus](https://github.com/hydrusnetwork/hydrus/) with the goal to provide a good looking and fast
way to organize all kinds of media.
search for entries by using those tags.
![](https://mediarepo.trivernis.dev/assets/images/screenshot-1.png)
## Features
### Implemented
- management of multiple repositories
- running repository daemons on startup or in the background
- importing files from the file system
- assigning tags to files
- searching for files using tags and properties
- sorting files by properties and tag namespaces
### Planned
- tag aliases and implications
- file collections
- importing files from URLs
- tag lookup using SauceNao and IQDB
- synchronisation between clients
## Installation
In order to use mediarepo, the mediarepo daemon and UI application need to be installed.
Both can be downloaded from the [Releases](https://github.com/Trivernis/mediarepo/releases) page or the AUR.
Arch Linux:
```sh
yay -S mediarepo-daemon mediarepo
$ yay -S mediarepo-daemon mediarepo
```
When installing manually the `mediarepo-daemon` binary needs to be accessible in the `PATH` variable.
## Building
### Prerequisites
You need to have a working rust toolchain (e.g. via [rustup](https://rustup.rs/)) and [node.js](https://nodejs.org) installed.
For building the UI the required tauri build tooling needs to be installed as well. Please follow [their documentation](https://tauri.studio/docs/getting-started/prerequisites) for setup information.
You also need to have a working [python](https://www.python.org/) installation on your system.
### Building mediarepo
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`.
All Componens:
```sh
$ ./build.py build --ffmpeg
```
Daemon only:
```sh
$ ./build.py build --daemon --ffmpeg
```
If you don't want to build with ffmpeg support omit the `--ffmpeg` flag.
UI only:
```sh
$ ./build.py build --ui
```
After building the `out` directory contains all the built binaries and bundles.
### Test Builds
For test builds the `Dockerfile` in this repository can be used. This way no build dependencies need to be installed on the system. The dockerfile doesn't provide any artifacts and can only be used for validation.
## Usage and Further Information
Please consult the [official website](https://mediarepo.trivernis.dev) for more information.

@ -0,0 +1,210 @@
#!/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,16 +0,0 @@
<h1 align="center">
mediarepo-api
</h1>
<h3 align="center" color="red">
This project is a work in progress
</h3>
- - -
This repository contains common mediarepo API types to implement the API both serverside
and clientside. It also contains a tauri plugin (feature "tauri-plugin") to hook it
into the tauri application lifecycle.
## License
GPL-3

@ -4,7 +4,7 @@ default-members = ["mediarepo-core", "mediarepo-database", "mediarepo-logic", "m
[package]
name = "mediarepo-daemon"
version = "1.0.0-rc.1"
version = "1.0.0-rc.2"
edition = "2018"
license = "gpl-3"
repository = "https://github.com/Trivernis/mediarepo-daemon"

@ -1,35 +0,0 @@
<h1 align="center">
mediarepo-daemon
</h1>
<p align="center">
<img src="https://github.com/Trivernis/mediarepo-ui/raw/main/src-tauri/icons/64x64.png"/>
</p>
<h3 align="center" style="color:red">This repository is a work in progress</h3>
- - -
This repository contains a media repository daemon that allows one to manage their media.
## Usage
1. Initialize an empty repository
```
mediarepo --repo "where/your/repo/should/be" init
```
2. Import some images
```
mediarepo --repo "path/to/your/repo" import "path/to/your/files/as/**/glob/*.png"
```
3. Start the daemon
```
mediarepo --repo "path/to/your/repo start
```
4. Open the mediarepo-ui and connect to the repository
## License
GPL-3

@ -43,6 +43,9 @@ yarn-error.log
testem.log
/typings
# only using yarn
package-lock.json
# System Files
.DS_Store
Thumbs.db

@ -1,33 +0,0 @@
<h1 align="center">
mediarepo-ui
</h1>
<p align="center">
<img src="https://github.com/Trivernis/mediarepo-ui/raw/main/src-tauri/icons/64x64.png"/>
</p>
<h3 align="center" style="color:red">This repository is a work in progress</h3>
- - -
This repository contains a frontend client to connect to
the [mediarepo-daemon](../mediarepo-daemon). It is written in tauri (yay).
## Usage
Refer to [the tauri documentation](https://tauri.studio/en/docs/getting-started) for information about setting up your
environment to build this project. With the `cargo-tauri` tooling installed you can run
```
cargo tauri dev
```
to start the application in development mode or
```
cargo tauri build
```
to bundle the application.
## License
GPL-3

@ -1,6 +1,6 @@
{
"name": "mediarepo-ui",
"version": "1.0.0-rc.1",
"version": "1.0.0-rc.2",
"scripts": {
"ng": "ng",
"start": "ng serve",

@ -1,6 +1,6 @@
[package]
name = "app"
version = "1.0.0-rc.1"
version = "1.0.0-rc.2"
description = "The UI for the mediarepo media management tool"
authors = ["you"]
license = ""
@ -10,7 +10,7 @@ edition = "2018"
build = "src/build.rs"
[build-dependencies]
tauri-build = "1.0.0-beta.4"
tauri-build = { version = "1.0.0-beta.4", features = [] }
[dependencies]
serde_json = "1.0.78"

@ -1,7 +1,7 @@
{
"package": {
"productName": "mediarepo-ui",
"version": "1.0.0-rc.1"
"version": "1.0.0"
},
"build": {
"distDir": "../dist/mediarepo-ui",
@ -73,4 +73,4 @@
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self' once: thumb: content:"
}
}
}
}

@ -1,5 +1,5 @@
<div class="sidebar-inner">
<mat-tab-group headerPosition="below">
<mat-tab-group (selectedTabChange)="this.onTabChange()" headerPosition="below">
<mat-tab label="Search">
<app-file-search (searchEndEvent)="this.onDisplayedFilesChange(); this.searchEndEvent.emit($event);"
(searchStartEvent)="this.searchStartEvent.emit($event)"

@ -1,4 +1,14 @@
import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild} from "@angular/core";
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild
} from "@angular/core";
import {Tag} from "../../../../../api/models/Tag";
import {TagService} from "../../../../services/tag/tag.service";
import {File} from "../../../../../api/models/File";
@ -29,7 +39,11 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges {
public tagsOfSelection: Tag[] = [];
public tagsLoading = false;
constructor(private repoService: RepositoryService, private tagService: TagService) {
constructor(
private changeDetector: ChangeDetectorRef,
private repoService: RepositoryService,
private tagService: TagService
) {
this.repoService.selectedRepository.subscribe(
async (repo) => repo && this.fileSearch && await this.fileSearch.searchForFiles());
this.tagService.tags.subscribe(t => this.allTags = t);
@ -79,6 +93,10 @@ export class FilesTabSidebarComponent implements OnInit, OnChanges {
this.tagsLoading = false;
}
public onTabChange(): void {
this.changeDetector.markForCheck();
}
private async refreshFileSelection() {
const filteredSelection = this.selectedFiles.filter(
file => this.files.findIndex(f => f.id === file.id) >= 0);

@ -4,6 +4,7 @@ import {FilesTabState} from "../../../models/state/FilesTabState";
import {RepositoryMetadata} from "../../../../api/api-types/repo";
import {RepositoryService} from "../../../services/repository/repository.service";
import {TabCategory} from "../../../models/state/TabCategory";
import {take} from "rxjs";
@Component({
selector: "app-files-tab",
@ -19,6 +20,8 @@ export class FilesTabComponent implements OnInit {
selectedFiles: File[] = [];
public metadata?: RepositoryMetadata;
private preselectedCd?: string;
constructor(
repoService: RepositoryService,
) {
@ -28,6 +31,12 @@ export class FilesTabComponent implements OnInit {
async ngOnInit() {
this.state.files.subscribe(files => this.files = files);
this.state.loading.subscribe(loading => this.contentLoading = loading);
this.state.files.pipe(take(2)).subscribe(async files => {
await this.handlePreselection(this.preselectedCd, files);
});
this.state.selectedCD.pipe(take(2)).subscribe(async (cd) => {
await this.handlePreselection(cd, this.files);
});
}
async onFileSelect(files: File[]) {
@ -37,7 +46,6 @@ export class FilesTabComponent implements OnInit {
} else {
this.state.selectedCD.next(undefined);
}
console.debug(this.selectedFiles);
}
public getStateSelectedFile(): File | undefined {
@ -61,4 +69,19 @@ export class FilesTabComponent implements OnInit {
public onImportFiles(): void {
this.state.category = TabCategory.Import;
}
private async handlePreselection(cd: string | undefined, files: File[]) {
console.log(cd, files);
this.preselectedCd = cd;
if (cd && files.length > 0) {
const file = files.find(f => f.cd === cd);
if (file) {
console.debug("firing select");
this.preselectedCd = undefined;
await this.onFileSelect([file]);
}
}
}
}

@ -1,9 +1,9 @@
<div (mouseenter)="this.mouseInImageView = true" (mouseleave)="this.mouseInImageView = false"
class="image-full-view-inner">
<div class="zoom-slider">
<mat-slider (input)="this.imageZoom=$event.value ?? 1" [value]="this.imageZoom" max="4"
min="0.5" step="0.1" vertical></mat-slider>
<button (click)="this.resetImage()" mat-icon-button>
<mat-slider (input)="this.imageZoom=$event.value ?? 1" [max]="this.maxZoom" [min]="this.minZoom"
[value]="this.imageZoom" step="0.1" vertical></mat-slider>
<button (click)="this.resetImage()" class="reset-image-zoom-button" mat-icon-button>
<ng-icon name="mat-refresh"></ng-icon>
</button>
</div>

@ -23,15 +23,14 @@
opacity: 0.5;
padding: 1em 0.5em;
transition-duration: 0.2s;
}
.zoom-slider:hover {
opacity: 1;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 1em;
&:hover {
opacity: 1;
background-color: rgba(0, 0, 0, 0.3);
border-radius: 1em;
}
}
.image-full-view-inner {
height: 100%;
width: 100%;
@ -42,3 +41,8 @@
.hidden {
display: none;
}
.reset-image-zoom-button {
height: 3em;
margin: auto
}

@ -9,6 +9,9 @@ import {SafeResourceUrl} from "@angular/platform-browser";
})
export class ImageViewerComponent implements OnChanges {
@Input() imageUrl!: SafeResourceUrl | string;
@Input() maxZoom = 10;
@Input() minZoom = 0.5;
public imageZoom = 1;
public imagePosition = { x: 0, y: 0 };
public mouseInImageView = false;
@ -52,13 +55,13 @@ export class ImageViewerComponent implements OnChanges {
if (delta > 0) {
this.imageZoom += 0.2;
if (this.imageZoom > 4) {
this.imageZoom = 4;
if (this.imageZoom > this.maxZoom) {
this.imageZoom = this.maxZoom;
}
} else if (delta < 0) {
this.imageZoom -= 0.2;
if (this.imageZoom < 0.5) {
this.imageZoom = 0.5;
if (this.imageZoom < this.minZoom) {
this.imageZoom = this.minZoom;
}
}
}

@ -81,6 +81,7 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit, Afte
file => new Selectable<File>(file, false));
this.refreshFileSelections();
this.setPartitionedGridEntries();
this.scrollToSelection();
}
}
@ -246,11 +247,23 @@ export class FileGridComponent implements OnChanges, OnInit, AfterViewInit, Afte
selectedEntry.select();
this.selectedEntries.push(selectedEntry);
}
this.changeDetector.markForCheck();
}
}, 0);
}
}
private scrollToSelection() {
const selected = this.selectedEntries[0];
if (this.virtualScroll && selected) {
const index = Math.floor(this.gridEntries.indexOf(selected) / this.columns);
setTimeout(() => {
this.virtualScroll.scrollToIndex(index);
this.changeDetector.markForCheck();
}, 0);
}
}
private refreshFileSelections() {
const newSelection: Selectable<File>[] = this.gridEntries.filter(
entry => this.selectedEntries.findIndex(

@ -3,7 +3,7 @@
<h1>File Metadata</h1>
<mat-divider></mat-divider>
</div>
<app-busy-indicator [blurBackground]="true" [darkenBackground]="false">
<app-busy-indicator [blurBackground]="true" [busy]="this.loading" [darkenBackground]="false">
<div class="file-metadata-entries-scroll-container">
<div class="file-metadata-entries">
<app-editable-metadata-entry (valueChangeEvent)="this.saveFileName($event)"
@ -22,7 +22,6 @@
<app-metadata-entry
attributeName="Changed at">{{fileMetadata.change_time | date: 'dd.MM.yy HH:mm:ss'}}</app-metadata-entry>
</ng-container>
</div>
</div>
</app-busy-indicator>

@ -1,8 +1,15 @@
import {ChangeDetectionStrategy, Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild} from "@angular/core";
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
Input,
OnChanges,
OnInit,
SimpleChanges
} from "@angular/core";
import {File} from "../../../../../api/models/File";
import {FileService} from "../../../../services/file/file.service";
import {FileMetadata} from "../../../../../api/api-types/files";
import {BusyIndicatorComponent} from "../../app-common/busy-indicator/busy-indicator.component";
@Component({
selector: "app-file-metadata",
@ -14,32 +21,34 @@ export class FileMetadataComponent implements OnInit, OnChanges {
@Input() file!: File;
public fileMetadata: FileMetadata | undefined;
public loading: boolean = false;
@ViewChild(BusyIndicatorComponent) busyIndicator!: BusyIndicatorComponent;
constructor(private fileService: FileService) {
constructor(private changeDetector: ChangeDetectorRef, private fileService: FileService) {
}
public async ngOnInit() {
await this.busyIndicator.wrapAsyncOperation(async () => {
this.fileMetadata = await this.fileService.getFileMetadata(this.file.id);
});
this.loading = true;
this.fileMetadata = await this.fileService.getFileMetadata(this.file.id);
this.changeDetector.markForCheck();
this.loading = false;
}
public async ngOnChanges(changes: SimpleChanges) {
if (changes["file"] && (!this.fileMetadata || this.fileMetadata.file_id != this.file.id)) {
await this.busyIndicator.wrapAsyncOperation(async () => {
this.fileMetadata = await this.fileService.getFileMetadata(this.file.id);
});
this.loading = true;
this.fileMetadata = await this.fileService.getFileMetadata(this.file.id);
this.changeDetector.markForCheck();
this.loading = false;
}
}
public async saveFileName(name: string) {
await this.busyIndicator.wrapAsyncOperation(async () => {
const newFile = await this.fileService.updateFileName(this.file.id, name);
if (this.fileMetadata) {
this.fileMetadata.name = newFile.name;
}
});
this.loading = true;
const newFile = await this.fileService.updateFileName(this.file.id, name);
if (this.fileMetadata) {
this.fileMetadata.name = newFile.name;
this.changeDetector.markForCheck();
}
this.loading = false;
}
}

@ -19,8 +19,8 @@
class="filter-input"></app-filter-input>
</div>
<div class="dialog-actions" mat-dialog-actions>
<button (click)="confirmFilter()" color="primary" mat-flat-button>Filter</button>
<button (click)="cancelFilter()" color="accent" mat-stroked-button>Cancel</button>
<button (click)="confirmFilter()" color="primary" mat-flat-button>Filter</button>
</div>
<app-context-menu #contextMenu>
<button (click)="this.removeSelectedFilters()" mat-menu-item>Remove</button>

@ -1,6 +1,7 @@
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
Input,
@ -36,6 +37,7 @@ export class TagEditComponent implements AfterViewInit, OnChanges {
private fileTags: { [key: number]: Tag[] } = {};
constructor(
private changeDetector: ChangeDetectorRef,
private logger: LoggingService,
private tagService: TagService,
) {
@ -63,6 +65,7 @@ export class TagEditComponent implements AfterViewInit, OnChanges {
tagInstance = (await this.tagService.createTags([tag]))[0];
this.allTags.push(tagInstance);
}
this.changeDetector.markForCheck();
switch (this.editMode) {
case "Toggle":
await this.toggleTag(tagInstance);
@ -102,6 +105,7 @@ export class TagEditComponent implements AfterViewInit, OnChanges {
if (index >= 0) {
this.tagScroll.scrollToIndex(index);
}
this.changeDetector.markForCheck();
});
this.tagEditEvent.emit(this);
}
@ -123,6 +127,7 @@ export class TagEditComponent implements AfterViewInit, OnChanges {
}
await this.tagService.loadTags();
await this.tagService.loadNamespaces();
this.changeDetector.markForCheck();
});
this.tagEditEvent.emit(this);
}
@ -169,6 +174,7 @@ export class TagEditComponent implements AfterViewInit, OnChanges {
this.tags = tags.sort(
(a, b) => a.getNormalizedOutput()
.localeCompare(b.getNormalizedOutput()));
this.changeDetector.markForCheck();
}
private async wrapAsyncOperation<T>(cb: () => Promise<T>): Promise<T | undefined> {

@ -52,8 +52,8 @@ export class FilesTabState extends TabState implements SaveState<FilesTabSaveSta
this.filters = new BehaviorSubject(new SearchFilters(state.filters ?? []));
this.sortingPreset = new BehaviorSubject(new SortingPreset(state.sortingPreset));
this.mode = new BehaviorSubject(state.mode ?? "grid");
this.selectedCD = new BehaviorSubject(state.selectedCd);
this.files = new BehaviorSubject((state.files ?? []).map(mapNew(File)));
this.selectedCD = new BehaviorSubject(state.selectedCd);
this.subscribe();
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save