Merge pull request #12 from Trivernis/feature/sorting-presets

Feature/sorting presets
pull/13/head
Julius Riegel 3 years ago committed by GitHub
commit 8a2c10a061
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,50 +1,50 @@
[package]
name = "mediarepo-api"
version = "0.28.0"
version = "0.28.1"
edition = "2018"
license = "gpl-3"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tracing = "^0.1.29"
thiserror = "^1.0.30"
async-trait = {version = "^0.1.52", optional=true}
parking_lot = {version="^0.11.2", optional=true}
serde_json = {version="^1.0.73", optional=true}
directories = {version="^4.0.1", optional=true}
mime_guess = {version = "^2.0.3", optional=true}
serde_piecewise_default = "^0.2.0"
futures = {version = "^0.3.19", optional=true}
url = {version = "^2.2.2", optional=true }
pathsearch = {version="^0.2.0", optional=true}
tracing = "0.1.30"
thiserror = "1.0.30"
async-trait = { version = "0.1.52", optional = true }
parking_lot = { version = "0.12.0", optional = true }
serde_json = { version = "1.0.78", optional = true }
directories = { version = "4.0.1", optional = true }
mime_guess = { version = "2.0.3", optional = true }
serde_piecewise_default = "0.2.0"
futures = { version = "0.3.19", optional = true }
url = { version = "2.2.2", optional = true }
pathsearch = { version = "0.2.0", optional = true }
[dependencies.bromine]
version = "^0.17.1"
version = "0.17.1"
optional = true
features = ["serialize_bincode"]
[dependencies.serde]
version = "^1.0.132"
version = "1.0.136"
features = ["serde_derive"]
[dependencies.chrono]
version = "^0.4.19"
version = "0.4.19"
features = ["serde"]
[dependencies.tauri]
version = "^1.0.0-beta.8"
version = "1.0.0-beta.8"
optional=true
default-features = false
features = []
[dependencies.tokio]
version = "^1.15.0"
version = "1.16.1"
optional = true
features = ["sync", "fs", "net", "io-util", "io-std", "time", "rt", "process"]
[dependencies.toml]
version = "^0.5.8"
version = "0.5.8"
optional = true
[features]

@ -4,6 +4,7 @@ pub mod job;
pub mod protocol;
pub mod repo;
pub mod tag;
pub mod preset;
use crate::client_api::error::{ApiError, ApiResult};
use crate::client_api::file::FileApi;
@ -15,6 +16,7 @@ use async_trait::async_trait;
use bromine::ipc::stream_emitter::EmitMetadata;
use bromine::prelude::*;
use tokio::time::Duration;
use crate::client_api::preset::PresetApi;
#[async_trait]
pub trait IPCApi {
@ -48,6 +50,7 @@ pub struct ApiClient {
pub tag: TagApi,
pub repo: RepoApi,
pub job: JobApi,
pub preset: PresetApi,
}
impl Clone for ApiClient {
@ -58,6 +61,7 @@ impl Clone for ApiClient {
tag: self.tag.clone(),
repo: self.repo.clone(),
job: self.job.clone(),
preset: self.preset.clone(),
}
}
}
@ -70,6 +74,7 @@ impl ApiClient {
tag: TagApi::new(ctx.clone()),
repo: RepoApi::new(ctx.clone()),
job: JobApi::new(ctx.clone()),
preset: PresetApi::new(ctx.clone()),
ctx,
}
}

@ -0,0 +1,54 @@
use std::time::Duration;
use bromine::prelude::*;
use crate::client_api::error::ApiResult;
use crate::types::filtering::{SortingPreset, SortKey};
use super::IPCApi;
#[derive(Clone)]
pub struct PresetApi {
ctx: PooledContext,
}
impl IPCApi for PresetApi {
fn namespace() -> &'static str {
"presets"
}
fn ctx(&self) -> PoolGuard<Context> {
self.ctx.acquire()
}
}
impl PresetApi {
pub fn new(ctx: PooledContext) -> Self {
Self { ctx }
}
/// Returns all sorting presets of the repository
#[tracing::instrument(level = "debug", skip(self))]
pub async fn all_sorting_presets(&self) -> ApiResult<Vec<SortingPreset>> {
self.emit_and_get(
"all_sorting_presets",
(),
Some(Duration::from_secs(1))
)
.await
}
/// Adds a new sorting preset with the given keys
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add_sorting_preset(&self, keys: Vec<SortKey>) -> ApiResult<SortingPreset> {
self.emit_and_get(
"add_sorting_preset",
keys,
Some(Duration::from_secs(1))
)
.await
}
/// Deletes a given sorting preset by id
#[tracing::instrument(level = "debug", skip(self))]
pub async fn delete_sorting_preset(&self, id: i32) -> ApiResult<()> {
self.emit_and_get("delete_sorting_preset", id, Some(Duration::from_secs(1))).await
}
}

@ -5,6 +5,7 @@ pub use file::*;
pub use job::*;
pub use repo::*;
pub use tag::*;
pub use preset::*;
use crate::tauri_plugin::state::{ApiState, AppState, BufferState};
@ -13,6 +14,7 @@ pub mod file;
pub mod job;
pub mod repo;
pub mod tag;
pub mod preset;
pub type ApiAccess<'a> = State<'a, ApiState>;
pub type AppAccess<'a> = State<'a, AppState>;

@ -0,0 +1,27 @@
use crate::tauri_plugin::commands::ApiAccess;
use crate::tauri_plugin::error::PluginResult;
use crate::types::filtering::{SortingPreset, SortKey};
#[tauri::command]
pub async fn all_sorting_presets(api_state: ApiAccess<'_>) -> PluginResult<Vec<SortingPreset>> {
let api = api_state.api().await?;
let presets = api.preset.all_sorting_presets().await?;
Ok(presets)
}
#[tauri::command]
pub async fn add_sorting_preset(api_state: ApiAccess<'_>, sort_keys: Vec<SortKey>) -> PluginResult<SortingPreset> {
let api = api_state.api().await?;
let preset = api.preset.add_sorting_preset(sort_keys).await?;
Ok(preset)
}
#[tauri::command]
pub async fn delete_sorting_preset(api_state: ApiAccess<'_>, id: i32) -> PluginResult<()> {
let api = api_state.api().await?;
api.preset.delete_sorting_preset(id).await?;
Ok(())
}

@ -70,7 +70,10 @@ impl<R: Runtime> MediarepoPlugin<R> {
run_job,
update_file_status,
delete_file,
get_file_tag_map
get_file_tag_map,
all_sorting_presets,
add_sorting_preset,
delete_sorting_preset
]),
}
}

@ -71,3 +71,9 @@ pub enum SortDirection {
}
impl Eq for SortDirection {}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SortingPreset {
pub id: i32,
pub keys: Vec<SortKey>,
}

@ -216,23 +216,23 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "blake2b_simd"
version = "0.5.11"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587"
checksum = "72936ee4afc7f8f736d1c38383b56480b5497b4617b4a77bdbf1d2ababc76127"
dependencies = [
"arrayref",
"arrayvec 0.5.2",
"arrayvec 0.7.2",
"constant_time_eq",
]
[[package]]
name = "blake2s_simd"
version = "0.5.11"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e461a7034e85b211a4acb57ee2e6730b32912b06c08cc242243c39fc21ae6a2"
checksum = "db539cc2b5f6003621f1cd9ef92d7ded8ea5232c7de0f9faa2de251cd98730d4"
dependencies = [
"arrayref",
"arrayvec 0.5.2",
"arrayvec 0.7.2",
"constant_time_eq",
]
@ -255,15 +255,17 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
dependencies = [
"block-padding",
"generic-array",
]
[[package]]
name = "block-padding"
version = "0.2.1"
name = "block-buffer"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae"
checksum = "03588e54c62ae6d763e2a80090d50353b785795361b4ff5b3bf0a5097fc31c0b"
dependencies = [
"generic-array",
]
[[package]]
name = "bromine"
@ -407,9 +409,9 @@ dependencies = [
[[package]]
name = "console-api"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14f67643a7d716307ad10b3e3aef02826382acbe349a3e7605ac57556148bc87"
checksum = "cc347c19eb5b940f396ac155822caee6662f850d97306890ac3773ed76c90c5a"
dependencies = [
"prost",
"prost-types",
@ -462,6 +464,15 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc"
[[package]]
name = "core2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
dependencies = [
"memchr",
]
[[package]]
name = "cpufeatures"
version = "0.2.1"
@ -502,7 +513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils 0.8.6",
"crossbeam-utils 0.8.7",
]
[[package]]
@ -513,17 +524,17 @@ checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-epoch",
"crossbeam-utils 0.8.6",
"crossbeam-utils 0.8.7",
]
[[package]]
name = "crossbeam-epoch"
version = "0.9.6"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762"
checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils 0.8.6",
"crossbeam-utils 0.8.7",
"lazy_static",
"memoffset",
"scopeguard",
@ -531,12 +542,12 @@ dependencies = [
[[package]]
name = "crossbeam-queue"
version = "0.3.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b979d76c9fcb84dffc80a73f7290da0f83e4c95773494674cb44b76d13a7a110"
checksum = "4dd435b205a4842da59efd07628f921c096bc1cc0a156835b4fa0bcb9a19bcce"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils 0.8.6",
"crossbeam-utils 0.8.7",
]
[[package]]
@ -552,14 +563,23 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.6"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120"
checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6"
dependencies = [
"cfg-if 1.0.0",
"lazy_static",
]
[[package]]
name = "crypto-common"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d6b536309245c849479fba3da410962a43ed8e51c26b729208ec0ac2798d0"
dependencies = [
"generic-array",
]
[[package]]
name = "data-encoding"
version = "2.3.2"
@ -613,6 +633,17 @@ dependencies = [
"generic-array",
]
[[package]]
name = "digest"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b697d66081d42af4fba142d56918a3cb21dc8eb63372c6b85d14f44fb9c5979b"
dependencies = [
"block-buffer 0.10.1",
"crypto-common",
"generic-array",
]
[[package]]
name = "dotenv"
version = "0.15.0"
@ -1329,7 +1360,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "mediarepo-api"
version = "0.28.0"
version = "0.28.1"
dependencies = [
"bromine",
"chrono",
@ -1513,26 +1544,26 @@ dependencies = [
[[package]]
name = "multihash"
version = "0.15.0"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49e540106213dc639fe2b632a7d9e3525169ef081378a7c2da4457b84fec44c0"
checksum = "7392bffd88bc0c4f8297e36a777ab9f80b7127409c4a1acb8fee99c9f27addcd"
dependencies = [
"blake2b_simd",
"blake2s_simd",
"blake3",
"digest",
"generic-array",
"core2",
"digest 0.10.1",
"multihash-derive",
"sha2",
"sha2 0.10.1",
"sha3",
"unsigned-varint",
]
[[package]]
name = "multihash-derive"
version = "0.7.2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "424f6e86263cd5294cbd7f1e95746b95aca0e0d66bff31e5a40d6baa87b4aa99"
checksum = "fc076939022111618a5026d3be019fd8b366e76314538ff9a1b59ffbcbf98bcd"
dependencies = [
"proc-macro-crate",
"proc-macro-error",
@ -2048,7 +2079,7 @@ checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e"
dependencies = [
"crossbeam-channel",
"crossbeam-deque",
"crossbeam-utils 0.8.6",
"crossbeam-utils 0.8.7",
"lazy_static",
"num_cpus",
]
@ -2251,9 +2282,9 @@ dependencies = [
[[package]]
name = "security-framework"
version = "2.6.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fed7948b6c68acbb6e20c334f55ad635dc0f75506963de4464289fbd3b051ac"
checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
dependencies = [
"bitflags",
"core-foundation",
@ -2264,9 +2295,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.6.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a57321bf8bc2362081b2599912d2961fe899c0efadf1b4b2f8d48b3e253bb96c"
checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
dependencies = [
"core-foundation-sys",
"libc",
@ -2350,23 +2381,32 @@ version = "0.9.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer",
"block-buffer 0.9.0",
"cfg-if 1.0.0",
"cpufeatures",
"digest",
"digest 0.9.0",
"opaque-debug",
]
[[package]]
name = "sha2"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99c3bd8169c58782adad9290a9af5939994036b76187f7b4f0e6de91dbbfc0ec"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
"digest 0.10.1",
]
[[package]]
name = "sha3"
version = "0.9.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809"
checksum = "31f935e31cf406e8c0e96c2815a5516181b7004ae8c5f296293221e9b1e356bd"
dependencies = [
"block-buffer",
"digest",
"digest 0.10.1",
"keccak",
"opaque-debug",
]
[[package]]
@ -2460,7 +2500,7 @@ dependencies = [
"crc",
"crossbeam-channel",
"crossbeam-queue",
"crossbeam-utils 0.8.6",
"crossbeam-utils 0.8.7",
"either",
"flume",
"futures-channel",
@ -2483,7 +2523,7 @@ dependencies = [
"rust_decimal",
"serde 1.0.136",
"serde_json",
"sha2",
"sha2 0.9.9",
"smallvec",
"sqlformat",
"sqlx-rt",
@ -2507,7 +2547,7 @@ dependencies = [
"proc-macro2 1.0.36",
"quote 1.0.15",
"serde_json",
"sha2",
"sha2 0.9.9",
"sqlx-core",
"sqlx-rt",
"syn 1.0.86",
@ -2921,9 +2961,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6"
[[package]]
name = "tracing"
version = "0.1.29"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9"
dependencies = [
"cfg-if 1.0.0",
"log",
@ -2945,9 +2985,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.18"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716"
dependencies = [
"proc-macro2 1.0.36",
"quote 1.0.15",
@ -2956,11 +2996,12 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.21"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23"
dependencies = [
"lazy_static",
"valuable",
]
[[package]]
@ -2998,9 +3039,9 @@ dependencies = [
[[package]]
name = "tracing-serde"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b"
checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1"
dependencies = [
"serde 1.0.136",
"tracing-core",
@ -3008,9 +3049,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.7"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5312f325fe3588e277415f5a6cca1f4ccad0f248c4cd5a4bd33032d7286abc22"
checksum = "74786ce43333fcf51efe947aed9718fbe46d5c7328ec3f1029e818083966d9aa"
dependencies = [
"ansi_term",
"lazy_static",
@ -3127,6 +3168,12 @@ dependencies = [
"serde 1.0.136",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"

@ -16,17 +16,17 @@ name = "mediarepo-daemon"
path = "src/main.rs"
[dependencies]
tracing = "^0.1.29"
toml = "^0.5.8"
structopt ="^0.3.26"
glob = "^0.3.0"
tracing-flame = "^0.2.0"
tracing-appender = "^0.2.0"
tracing-log = "^0.1.2"
rolling-file = "^0.1.0"
num-integer = "^0.1.44"
console-subscriber = "^0.1.1"
log = "^0.4.14"
tracing = "0.1.30"
toml = "0.5.8"
structopt = "0.3.26"
glob = "0.3.0"
tracing-flame = "0.2.0"
tracing-appender = "0.2.0"
tracing-log = "0.1.2"
rolling-file = "0.1.0"
num-integer = "0.1.44"
console-subscriber = "0.1.1"
log = "0.4.14"
[dependencies.mediarepo-core]
path = "./mediarepo-core"
@ -42,7 +42,7 @@ version = "1.16.1"
features = ["macros", "rt-multi-thread", "io-std", "io-util"]
[dependencies.tracing-subscriber]
version="0.3.7"
version= "0.3.8"
features = ["env-filter", "ansi", "json"]
[features]

@ -7,39 +7,39 @@ workspace = ".."
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
thiserror = "^1.0.30"
multihash = "^0.15.0"
multibase = "^0.9.1"
base64 = "^0.13.0"
toml = "^0.5.8"
serde = "^1.0.136"
typemap_rev = "^0.1.5"
futures = "^0.3.19"
itertools = "^0.10.3"
glob = "^0.3.0"
tracing = "^0.1.29"
data-encoding = "^2.3.2"
tokio-graceful-shutdown = "^0.4.3"
thiserror = "1.0.30"
multihash = "0.16.1"
multibase = "0.9.1"
base64 = "0.13.0"
toml = "0.5.8"
serde = "1.0.136"
typemap_rev = "0.1.5"
futures = "0.3.19"
itertools = "0.10.3"
glob = "0.3.0"
tracing = "0.1.30"
data-encoding = "2.3.2"
tokio-graceful-shutdown = "0.4.3"
[dependencies.thumbnailer]
version = "^0.3"
version = "0.3.0"
default-features = false
[dependencies.sea-orm]
version = "^0.5.0"
version = "0.5.0"
default-features = false
[dependencies.sqlx]
version = "^0.5.10"
version = "0.5.10"
default-features = false
features = ["migrate"]
[dependencies.tokio]
version = "^1.16.1"
version = "1.16.1"
features = ["fs", "io-util", "io-std"]
[dependencies.config]
version = "^0.11.0"
version = "0.11.0"
features = ["toml"]
[dependencies.mediarepo-api]

@ -7,17 +7,17 @@ workspace = ".."
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = "^0.4.19"
tracing = "^0.1.29"
chrono = "0.4.19"
tracing = "0.1.30"
[dependencies.mediarepo-core]
path = "../mediarepo-core"
[dependencies.sqlx]
version = "^0.5.10"
version = "0.5.10"
features = ["migrate"]
[dependencies.sea-orm]
version = "^0.5.0"
version = "0.5.0"
features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros", "debug-print"]
default-features = false

@ -0,0 +1,20 @@
-- Add migration script here
CREATE TABLE sorting_presets (
id INTEGER PRIMARY KEY AUTOINCREMENT
);
CREATE TABLE sort_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_type INTEGER NOT NULL DEFAULT 0,
ascending INTEGER NOT NULL CHECK (ascending IN (0, 1)),
value VARCHAR(128)
);
CREATE TABLE sorting_preset_keys (
preset_id INTEGER REFERENCES sorting_presets (id) ON DELETE CASCADE,
key_id INTEGER REFERENCES sort_keys (id),
key_index INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (preset_id, key_id)
);
CREATE INDEX sorting_preset_index ON sorting_preset_keys (preset_id);

@ -0,0 +1,17 @@
PRAGMA foreign_keys= off;
ALTER TABLE sorting_preset_keys
RENAME TO _sorting_preset_keys_old;
CREATE TABLE sorting_preset_keys
(
preset_id INTEGER REFERENCES sorting_presets (id) ON DELETE CASCADE,
key_id INTEGER REFERENCES sort_keys (id),
key_index INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (preset_id, key_id, key_index)
);
INSERT INTO sorting_preset_keys SELECT * FROM _sorting_preset_keys_old;
DROP TABLE _sorting_preset_keys_old;
PRAGMA foreign_keys= on;

@ -4,5 +4,8 @@ pub mod content_descriptor_tag;
pub mod file;
pub mod file_metadata;
pub mod namespace;
pub mod sort_key;
pub mod sorting_preset;
pub mod sorting_preset_key;
pub mod source;
pub mod tag;

@ -0,0 +1,26 @@
use sea_orm::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "sort_keys")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
pub key_type: i32,
pub ascending: bool,
pub value: Option<String>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl Related<super::sorting_preset::Entity> for Entity {
fn to() -> RelationDef {
super::sorting_preset_key::Relation::SortingPreset.def()
}
fn via() -> Option<RelationDef> {
Some(super::sorting_preset_key::Relation::SortingKey.def().rev())
}
}
impl ActiveModelBehavior for ActiveModel {}

@ -0,0 +1,27 @@
use sea_orm::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "sorting_presets")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}
impl Related<super::sort_key::Entity> for Entity {
fn to() -> RelationDef {
super::sorting_preset_key::Relation::SortingKey.def()
}
fn via() -> Option<RelationDef> {
Some(
super::sorting_preset_key::Relation::SortingPreset
.def()
.rev(),
)
}
}
impl ActiveModelBehavior for ActiveModel {}

@ -0,0 +1,45 @@
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
#[sea_orm(table_name = "sorting_preset_keys")]
pub struct Model {
#[sea_orm(primary_key)]
preset_id: i32,
#[sea_orm(primary_key)]
key_id: i32,
#[sea_orm(primary_key)]
key_index: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {
#[sea_orm(
belongs_to = "super::sorting_preset::Entity"
from = "Column::PresetId",
to = "super::sorting_preset::Column::Id"
)]
SortingPreset,
#[sea_orm(
belongs_to = "super::sort_key::Entity",
from = "Column::KeyId",
to = "super::sort_key::Column::Id"
)]
SortingKey,
}
impl Related<super::sorting_preset::Entity> for Entity {
fn to() -> RelationDef {
Relation::SortingPreset.def()
}
}
impl Related<super::sort_key::Entity> for Entity {
fn to() -> RelationDef {
Relation::SortingKey.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

@ -7,13 +7,13 @@ workspace = ".."
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
chrono = "^0.4.19"
typemap_rev = "^0.1.5"
serde = "^1.0.136"
mime_guess = "^2.0.3"
mime = "^0.3.16"
tracing = "^0.1.29"
async-trait = "^0.1.52"
chrono = "0.4.19"
typemap_rev = "0.1.5"
serde = "1.0.136"
mime_guess = "2.0.3"
mime = "0.3.16"
tracing = "0.1.30"
async-trait = "0.1.52"
[dependencies.mediarepo-core]
path = "../mediarepo-core"
@ -22,12 +22,12 @@ path = "../mediarepo-core"
path = "../mediarepo-database"
[dependencies.sea-orm]
version = "^0.5.0-rc.1"
version = "0.5.0"
features = ["runtime-tokio-native-tls", "macros"]
default-features = false
[dependencies.tokio]
version = "^1.15.0"
version = "1.16.1"
features = ["fs", "io-std", "io-util"]

@ -1,10 +1,10 @@
use sea_orm::prelude::*;
use tokio::io::AsyncReadExt;
use crate::dao_provider;
use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::{content_descriptor, file, file_metadata};
use crate::dao::{DaoContext, DaoProvider};
use crate::dto::{FileDto, FileMetadataDto, ThumbnailDto};
pub mod add;
@ -12,21 +12,9 @@ pub mod delete;
pub mod find;
pub mod update;
pub struct FileDao {
ctx: DaoContext,
}
impl DaoProvider for FileDao {
fn dao_ctx(&self) -> DaoContext {
self.ctx.clone()
}
}
dao_provider!(FileDao);
impl FileDao {
pub fn new(ctx: DaoContext) -> Self {
Self { ctx }
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn all(&self) -> RepoResult<Vec<FileDto>> {
let files = file::Entity::find()

@ -1,20 +1,6 @@
use crate::dao_provider;
pub mod migrate_content_descriptors;
pub mod sqlite_operations;
use crate::dao::{DaoContext, DaoProvider};
pub struct JobDao {
ctx: DaoContext,
}
impl DaoProvider for JobDao {
fn dao_ctx(&self) -> DaoContext {
self.ctx.clone()
}
}
impl JobDao {
pub fn new(ctx: DaoContext) -> JobDao {
Self { ctx }
}
}
dao_provider!(JobDao);

@ -5,13 +5,38 @@ use mediarepo_core::fs::thumbnail_store::ThumbnailStore;
use crate::dao::file::FileDao;
use crate::dao::job::JobDao;
use crate::dao::sorting_preset::SortingPresetDao;
use crate::dao::tag::TagDao;
pub mod file;
pub mod job;
pub mod repo;
pub mod sorting_preset;
pub mod tag;
#[macro_export]
macro_rules! dao_provider {
($name:ident) => {
use crate::dao::{DaoContext, DaoProvider};
pub struct $name {
ctx: DaoContext,
}
impl DaoProvider for $name {
fn dao_ctx(&self) -> DaoContext {
self.ctx.clone()
}
}
impl $name {
pub fn new(ctx: DaoContext) -> Self {
Self { ctx }
}
}
};
}
#[derive(Clone)]
pub struct DaoContext {
pub db: DatabaseConnection,
@ -33,6 +58,10 @@ pub trait DaoProvider {
fn job(&self) -> JobDao {
JobDao::new(self.dao_ctx())
}
fn sorting_preset(&self) -> SortingPresetDao {
SortingPresetDao::new(self.dao_ctx())
}
}
fn opt_to_active_val<T: Into<sea_orm::Value>>(opt: Option<T>) -> ActiveValue<T> {

@ -0,0 +1,172 @@
use crate::dao::sorting_preset::SortingPresetDao;
use crate::dto::{AddSortKeyDto, AddSortingPresetDto, SortKeyDto, SortingPresetDto};
use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::{sort_key, sorting_preset, sorting_preset_key};
use sea_orm::prelude::*;
use sea_orm::ActiveValue::Set;
use sea_orm::{
Condition, ConnectionTrait, DatabaseTransaction, DbBackend, FromQueryResult, JoinType,
QuerySelect, Statement,
};
#[allow(unused_imports)]
use sea_orm::TryGetableMany; // otherwise intellijrust hates on me
impl SortingPresetDao {
#[tracing::instrument(level = "debug", skip(self))]
pub async fn add(&self, preset: AddSortingPresetDto) -> RepoResult<SortingPresetDto> {
let trx = self.ctx.db.begin().await?;
let keys = add_keys(&trx, preset.keys).await?;
let key_ids = keys
.iter()
.enumerate()
.map(|(idx, key)| (idx, key.id()))
.collect::<Vec<(usize, i32)>>();
let condition = key_ids
.iter()
.cloned()
.map(create_mapping_condition)
.fold(Condition::any(), |acc, cond| acc.add(cond));
let existing_preset: Option<sorting_preset::Model> = sorting_preset::Entity::find()
.join(
JoinType::InnerJoin,
sorting_preset_key::Relation::SortingPreset.def().rev(),
)
.filter(condition)
.one(&trx)
.await?;
if let Some(model) = existing_preset {
trx.commit().await?;
return Ok(SortingPresetDto::new(model, keys));
}
// sea_orm currently doesn't support all-default-value inserts.
// TODOD: Replace after the change for default inserts has been merged
let preset_model = sorting_preset::Model::find_by_statement(Statement::from_string(
DbBackend::Sqlite,
"INSERT INTO sorting_presets DEFAULT VALUES RETURNING *;".to_string(),
))
.one(&trx)
.await?
.expect("failed to insert new sorting preset");
let mapping_models = key_ids
.into_iter()
.map(|(idx, key)| sorting_preset_key::ActiveModel {
preset_id: Set(preset_model.id),
key_id: Set(key),
key_index: Set(idx as i32),
})
.collect::<Vec<sorting_preset_key::ActiveModel>>();
if !mapping_models.is_empty() {
sorting_preset_key::Entity::insert_many(mapping_models)
.exec(&trx)
.await?;
}
trx.commit().await?;
Ok(SortingPresetDto::new(preset_model, keys))
}
}
async fn add_keys(
trx: &DatabaseTransaction,
keys: Vec<AddSortKeyDto>,
) -> RepoResult<Vec<SortKeyDto>> {
let mut key_dtos = find_sort_keys(trx, &keys).await?;
let mut insert_keys = keys.clone();
key_dtos.iter().for_each(|key| {
insert_keys.retain(|k| {
k.ascending != key.ascending()
|| k.key_type != key.key_type().unwrap()
|| !compare_opts_eq(key.value(), k.value.as_ref())
})
});
if !insert_keys.is_empty() {
let active_models: Vec<sort_key::ActiveModel> = insert_keys
.iter()
.cloned()
.map(|key| sort_key::ActiveModel {
key_type: Set(key.key_type.to_number()),
ascending: Set(key.ascending),
value: Set(key.value),
..Default::default()
})
.collect();
sort_key::Entity::insert_many(active_models)
.exec(trx)
.await?;
let mut new_keys = find_sort_keys(trx, &insert_keys).await?;
key_dtos.append(&mut new_keys);
}
let keys_original_order = keys
.into_iter()
.filter_map(|k| {
key_dtos
.iter()
.find(|key| {
k.ascending == key.ascending()
&& k.key_type == key.key_type().unwrap()
&& compare_opts_eq(key.value(), k.value.as_ref())
})
.cloned()
})
.collect::<Vec<SortKeyDto>>();
Ok(keys_original_order)
}
async fn find_sort_keys(
trx: &DatabaseTransaction,
keys: &Vec<AddSortKeyDto>,
) -> RepoResult<Vec<SortKeyDto>> {
if keys.is_empty() {
return Ok(vec![]);
}
let condition = keys
.iter()
.cloned()
.map(create_sort_key_condition)
.fold(Condition::any(), |acc, cond| acc.add(cond));
let keys = sort_key::Entity::find()
.filter(condition)
.all(trx)
.await?
.into_iter()
.map(SortKeyDto::new)
.collect();
Ok(keys)
}
fn create_sort_key_condition(key: AddSortKeyDto) -> Condition {
let mut condition = Condition::all()
.add(sort_key::Column::KeyType.eq(key.key_type.to_number()))
.add(sort_key::Column::Ascending.eq(key.ascending));
if let Some(value) = key.value {
condition = condition.add(sort_key::Column::Value.eq(value))
} else {
condition = condition.add(sort_key::Column::Value.is_null())
}
condition
}
fn create_mapping_condition(entry: (usize, i32)) -> Condition {
Condition::all()
.add(sorting_preset_key::Column::KeyId.eq(entry.1))
.add(sorting_preset_key::Column::KeyIndex.eq(entry.0 as i32))
}
fn compare_opts_eq<T: Eq>(opt1: Option<T>, opt2: Option<T>) -> bool {
if let (Some(opt1), Some(opt2)) = (&opt1, &opt2) {
opt1 == opt2
} else {
opt1.is_none() && opt2.is_none()
}
}

@ -0,0 +1,42 @@
pub mod add;
use crate::dao_provider;
use crate::dto::{SortKeyDto, SortingPresetDto};
use mediarepo_core::error::RepoResult;
use mediarepo_database::entities::{sort_key, sorting_preset, sorting_preset_key};
use sea_orm::prelude::*;
use sea_orm::QueryOrder;
dao_provider!(SortingPresetDao);
impl SortingPresetDao {
#[tracing::instrument(level = "debug", skip(self))]
pub async fn all(&self) -> RepoResult<Vec<SortingPresetDto>> {
let presets = sorting_preset::Entity::find()
.find_with_related(sort_key::Entity)
.order_by_asc(sorting_preset_key::Column::KeyIndex)
.all(&self.ctx.db)
.await?
.into_iter()
.map(map_sorting_preset_dto)
.collect();
Ok(presets)
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn delete(&self, id: i32) -> RepoResult<()> {
sorting_preset::Entity::delete_many()
.filter(sorting_preset::Column::Id.eq(id))
.exec(&self.ctx.db)
.await?;
Ok(())
}
}
fn map_sorting_preset_dto(
entry: (sorting_preset::Model, Vec<sort_key::Model>),
) -> SortingPresetDto {
SortingPresetDto::new(entry.0, entry.1.into_iter().map(SortKeyDto::new).collect())
}

@ -10,7 +10,7 @@ use mediarepo_core::utils::parse_namespace_and_tag;
use mediarepo_database::entities::{content_descriptor, content_descriptor_tag, namespace, tag};
use crate::dao::tag::by_name::TagByNameQuery;
use crate::dao::{DaoContext, DaoProvider};
use crate::dao_provider;
use crate::dto::{NamespaceDto, TagDto};
pub mod add;
@ -19,21 +19,9 @@ pub mod by_name;
pub mod cdids_with_namespaced_tags;
pub mod mappings;
pub struct TagDao {
ctx: DaoContext,
}
impl DaoProvider for TagDao {
fn dao_ctx(&self) -> DaoContext {
self.ctx.clone()
}
}
dao_provider!(TagDao);
impl TagDao {
pub fn new(ctx: DaoContext) -> Self {
Self { ctx }
}
#[tracing::instrument(level = "debug", skip(self))]
pub async fn all(&self) -> RepoResult<Vec<TagDto>> {
let tags = tag::Entity::find()

@ -1,12 +1,13 @@
pub use file::*;
pub use file_metadata::*;
pub use namespace::*;
pub use sorting_preset::*;
pub use tag::*;
pub use thumbnail::*;
mod file;
mod file_metadata;
mod tag;
mod namespace;
mod sorting_preset;
mod tag;
mod thumbnail;

@ -0,0 +1,101 @@
use crate::dto::KeyType::{
FileChangeTime, FileCreatedTime, FileImportedTime, FileName, FileSize, FileType, Namespace,
NumTags,
};
use mediarepo_database::entities::sort_key;
use mediarepo_database::entities::sorting_preset;
#[derive(Clone, Debug)]
pub struct SortingPresetDto {
model: sorting_preset::Model,
keys: Vec<SortKeyDto>,
}
impl SortingPresetDto {
pub fn new(model: sorting_preset::Model, keys: Vec<SortKeyDto>) -> Self {
Self { model, keys }
}
pub fn id(&self) -> i32 {
self.model.id
}
pub fn keys(&self) -> &Vec<SortKeyDto> {
&self.keys
}
pub fn into_keys(self) -> Vec<SortKeyDto> {
self.keys
}
}
#[derive(Clone, Debug)]
pub struct SortKeyDto {
model: sort_key::Model,
}
impl SortKeyDto {
pub fn new(model: sort_key::Model) -> Self {
Self { model }
}
pub fn id(&self) -> i32 {
self.model.id
}
pub fn key_type(&self) -> Option<KeyType> {
KeyType::from_number(self.model.key_type)
}
pub fn ascending(&self) -> bool {
self.model.ascending
}
pub fn value(&self) -> Option<&String> {
self.model.value.as_ref()
}
}
#[derive(Clone, Copy, Debug, PartialOrd, PartialEq)]
pub enum KeyType {
Namespace = 0,
FileName = 1,
FileSize = 2,
FileImportedTime = 3,
FileCreatedTime = 4,
FileChangeTime = 5,
FileType = 6,
NumTags = 7,
}
impl KeyType {
pub fn from_number(number: i32) -> Option<KeyType> {
match number {
0 => Some(Namespace),
1 => Some(FileName),
2 => Some(FileSize),
3 => Some(FileImportedTime),
4 => Some(FileCreatedTime),
5 => Some(FileChangeTime),
6 => Some(FileType),
7 => Some(NumTags),
_ => None,
}
}
pub fn to_number(&self) -> i32 {
self.clone() as i32
}
}
#[derive(Clone, Debug)]
pub struct AddSortingPresetDto {
pub keys: Vec<AddSortKeyDto>,
}
#[derive(Clone, Debug)]
pub struct AddSortKeyDto {
pub key_type: KeyType,
pub ascending: bool,
pub value: Option<String>,
}

@ -7,11 +7,11 @@ workspace = ".."
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "^1.0.136"
tracing = "^0.1.29"
compare = "^0.1.0"
port_check = "^0.1.5"
rayon = "^1.5.1"
serde = "1.0.136"
tracing = "0.1.30"
compare = "0.1.0"
port_check = "0.1.5"
rayon = "1.5.1"
[dependencies.mediarepo-core]
path = "../mediarepo-core"
@ -23,13 +23,13 @@ path = "../mediarepo-database"
path = "../mediarepo-logic"
[dependencies.tokio]
version = "^1.16.1"
version = "1.16.1"
features = ["net"]
[dependencies.chrono]
version = "^0.4.19"
version = "0.4.19"
features = ["serde"]
[dependencies.tracing-futures]
version = "^0.2.5"
version = "0.2.5"
features = ["tokio-executor"]

@ -1,9 +1,13 @@
use mediarepo_core::mediarepo_api::types::files::{
FileBasicDataResponse, FileMetadataResponse, FileStatus, ThumbnailMetadataResponse,
};
use mediarepo_core::mediarepo_api::types::filtering::{
SortDirection, SortKey, SortNamespace, SortingPreset,
};
use mediarepo_core::mediarepo_api::types::tags::{NamespaceResponse, TagResponse};
use mediarepo_logic::dto::{
FileDto, FileMetadataDto, FileStatus as FileStatusModel, NamespaceDto, TagDto, ThumbnailDto,
FileDto, FileMetadataDto, FileStatus as FileStatusModel, KeyType, NamespaceDto, SortKeyDto,
SortingPresetDto, TagDto, ThumbnailDto,
};
pub trait FromModel<M> {
@ -73,3 +77,42 @@ impl FromModel<NamespaceDto> for NamespaceResponse {
}
}
}
impl FromModel<SortingPresetDto> for SortingPreset {
fn from_model(model: SortingPresetDto) -> Self {
SortingPreset {
id: model.id(),
keys: model
.into_keys()
.into_iter()
.filter_map(map_sort_dto_to_key)
.collect(),
}
}
}
fn map_sort_dto_to_key(dto: SortKeyDto) -> Option<SortKey> {
let direction = map_direction(dto.ascending());
match dto.key_type()? {
KeyType::Namespace => Some(SortKey::Namespace(SortNamespace {
name: dto.value()?.to_owned(),
direction,
})),
KeyType::FileName => Some(SortKey::FileName(direction)),
KeyType::FileSize => Some(SortKey::FileSize(direction)),
KeyType::FileImportedTime => Some(SortKey::FileImportedTime(direction)),
KeyType::FileCreatedTime => Some(SortKey::FileCreatedTime(direction)),
KeyType::FileChangeTime => Some(SortKey::FileChangeTime(direction)),
KeyType::FileType => Some(SortKey::FileType(direction)),
KeyType::NumTags => Some(SortKey::NumTags(direction)),
}
}
fn map_direction(ascending: bool) -> SortDirection {
if ascending {
SortDirection::Ascending
} else {
SortDirection::Descending
}
}

@ -291,10 +291,8 @@ impl FilesNamespace {
let found_thumbnail = thumbnails.into_iter().find(|thumb| {
let Dimensions { height, width } = thumb.size();
*height >= min_size.0
&& *height <= max_size.0
&& *width >= min_size.1
&& *width <= max_size.1
(*height <= max_size.0 && *width <= max_size.1)
&& (*width >= min_size.1 || *height >= min_size.0)
});
let thumbnail = if let Some(thumbnail) = found_thumbnail {

@ -3,6 +3,7 @@ use mediarepo_core::bromine::{namespace, namespace::Namespace, IPCBuilder};
pub mod files;
pub mod jobs;
pub mod presets;
pub mod repo;
pub mod tags;
@ -12,4 +13,5 @@ pub fn build_namespaces<L: AsyncStreamProtocolListener>(builder: IPCBuilder<L>)
.add_namespace(namespace!(tags::TagsNamespace))
.add_namespace(namespace!(repo::RepoNamespace))
.add_namespace(namespace!(jobs::JobsNamespace))
.add_namespace(namespace!(presets::PresetsNamespace))
}

@ -0,0 +1,118 @@
use crate::from_model::FromModel;
use crate::utils::get_repo_from_context;
use mediarepo_core::bromine::prelude::*;
use mediarepo_core::mediarepo_api::types::filtering::{SortDirection, SortKey, SortingPreset};
use mediarepo_logic::dao::DaoProvider;
use mediarepo_logic::dto::{AddSortKeyDto, AddSortingPresetDto, KeyType};
pub struct PresetsNamespace;
impl NamespaceProvider for PresetsNamespace {
fn name() -> &'static str {
"presets"
}
fn register(handler: &mut EventHandler) {
events!(handler,
"all_sorting_presets" => Self::all_sorting_presets,
"add_sorting_preset" => Self::add_sorting_preset,
"delete_sorting_preset" => Self::delete_sorting_preset
);
}
}
impl PresetsNamespace {
#[tracing::instrument(skip_all)]
pub async fn all_sorting_presets(ctx: &Context, _: Event) -> IPCResult<()> {
let repo = get_repo_from_context(ctx).await;
let sorting_presets: Vec<SortingPreset> = repo
.sorting_preset()
.all()
.await?
.into_iter()
.map(SortingPreset::from_model)
.collect();
ctx.emit_to(Self::name(), "all_sorting_presets", sorting_presets)
.await?;
Ok(())
}
#[tracing::instrument(skip_all)]
pub async fn add_sorting_preset(ctx: &Context, event: Event) -> IPCResult<()> {
let keys = event
.payload::<Vec<SortKey>>()?
.into_iter()
.map(sort_key_to_add_dto)
.collect();
let repo = get_repo_from_context(ctx).await;
let preset = repo
.sorting_preset()
.add(AddSortingPresetDto { keys })
.await?;
ctx.emit_to(
Self::name(),
"add_sorting_preset",
SortingPreset::from_model(preset),
)
.await?;
Ok(())
}
#[tracing::instrument(skip_all)]
pub async fn delete_sorting_preset(ctx: &Context, event: Event) -> IPCResult<()> {
let id = event.payload::<i32>()?;
let repo = get_repo_from_context(ctx).await;
repo.sorting_preset().delete(id).await?;
ctx.emit_to(Self::name(), "delete_sorting_preset", ())
.await?;
Ok(())
}
}
fn sort_key_to_add_dto(key: SortKey) -> AddSortKeyDto {
match key {
SortKey::Namespace(namespace) => AddSortKeyDto {
ascending: namespace.direction == SortDirection::Ascending,
key_type: KeyType::Namespace,
value: Some(namespace.name),
},
SortKey::FileName(dir) => AddSortKeyDto {
ascending: dir == SortDirection::Ascending,
key_type: KeyType::FileName,
value: None,
},
SortKey::FileSize(dir) => AddSortKeyDto {
ascending: dir == SortDirection::Ascending,
key_type: KeyType::FileSize,
value: None,
},
SortKey::FileImportedTime(dir) => AddSortKeyDto {
ascending: dir == SortDirection::Ascending,
key_type: KeyType::FileImportedTime,
value: None,
},
SortKey::FileCreatedTime(dir) => AddSortKeyDto {
ascending: dir == SortDirection::Ascending,
key_type: KeyType::FileCreatedTime,
value: None,
},
SortKey::FileChangeTime(dir) => AddSortKeyDto {
ascending: dir == SortDirection::Ascending,
key_type: KeyType::FileChangeTime,
value: None,
},
SortKey::FileType(dir) => AddSortKeyDto {
ascending: dir == SortDirection::Ascending,
key_type: KeyType::FileType,
value: None,
},
SortKey::NumTags(dir) => AddSortKeyDto {
ascending: dir == SortDirection::Ascending,
key_type: KeyType::NumTags,
value: None,
},
}
}

@ -161,9 +161,9 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "block-buffer"
version = "0.10.0"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1d36a02058e76b040de25a4464ba1c80935655595b661505c8b39b664828b95"
checksum = "03588e54c62ae6d763e2a80090d50353b785795361b4ff5b3bf0a5097fc31c0b"
dependencies = [
"generic-array",
]
@ -506,9 +506,9 @@ dependencies = [
[[package]]
name = "crossbeam-epoch"
version = "0.9.6"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97242a70df9b89a65d0b6df3c4bf5b9ce03c5b7309019777fbde37e7537f8762"
checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9"
dependencies = [
"cfg-if 1.0.0",
"crossbeam-utils",
@ -519,9 +519,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.6"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcae03edb34f947e64acdb1c33ec169824e20657e9ecb61cef6c8c74dcb8120"
checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6"
dependencies = [
"cfg-if 1.0.0",
"lazy_static",
@ -799,9 +799,9 @@ dependencies = [
[[package]]
name = "futf"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b"
checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843"
dependencies = [
"mac",
"new_debug_unreachable",
@ -1416,9 +1416,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.116"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "565dbd88872dbe4cc8a46e527f26483c1d1f7afa6b884a3bd6cd893d4f98da74"
checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c"
[[package]]
name = "lock_api"
@ -1499,7 +1499,7 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]]
name = "mediarepo-api"
version = "0.28.0"
version = "0.28.1"
dependencies = [
"async-trait",
"bromine",
@ -1507,7 +1507,7 @@ dependencies = [
"directories",
"futures",
"mime_guess",
"parking_lot",
"parking_lot 0.12.0",
"pathsearch",
"serde",
"serde_json",
@ -1865,7 +1865,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
"parking_lot_core",
"parking_lot_core 0.8.5",
]
[[package]]
name = "parking_lot"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
dependencies = [
"lock_api",
"parking_lot_core 0.9.0",
]
[[package]]
@ -1882,6 +1892,19 @@ dependencies = [
"winapi",
]
[[package]]
name = "parking_lot_core"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2f4f894f3865f6c0e02810fc597300f34dc2510f66400da262d8ae10e75767d"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"smallvec",
"windows-sys",
]
[[package]]
name = "pathdiff"
version = "0.2.1"
@ -2377,7 +2400,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver 1.0.4",
"semver 1.0.5",
]
[[package]]
@ -2425,9 +2448,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "security-framework"
version = "2.6.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fed7948b6c68acbb6e20c334f55ad635dc0f75506963de4464289fbd3b051ac"
checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc"
dependencies = [
"bitflags",
"core-foundation 0.9.2",
@ -2438,9 +2461,9 @@ dependencies = [
[[package]]
name = "security-framework-sys"
version = "2.6.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a57321bf8bc2362081b2599912d2961fe899c0efadf1b4b2f8d48b3e253bb96c"
checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556"
dependencies = [
"core-foundation-sys 0.8.3",
"libc",
@ -2477,9 +2500,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.4"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012"
checksum = "0486718e92ec9a68fbed73bb5ef687d71103b142595b406835649bebd33f72c7"
[[package]]
name = "semver-parser"
@ -2654,14 +2677,14 @@ dependencies = [
[[package]]
name = "string_cache"
version = "0.8.2"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "923f0f39b6267d37d23ce71ae7235602134b250ace715dd2c90421998ddac0c6"
checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26"
dependencies = [
"lazy_static",
"new_debug_unreachable",
"parking_lot",
"phf_shared 0.8.0",
"parking_lot 0.11.2",
"phf_shared 0.10.0",
"precomputed-hash",
"serde",
]
@ -2811,7 +2834,7 @@ dependencies = [
"ndk-glue",
"ndk-sys",
"objc",
"parking_lot",
"parking_lot 0.11.2",
"raw-window-handle 0.3.4",
"scopeguard",
"serde",
@ -2857,7 +2880,7 @@ dependencies = [
"rand 0.8.4",
"raw-window-handle 0.3.4",
"rfd",
"semver 1.0.4",
"semver 1.0.5",
"serde",
"serde_json",
"serde_repr",
@ -3088,9 +3111,9 @@ dependencies = [
[[package]]
name = "tracing"
version = "0.1.29"
version = "0.1.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105"
checksum = "2d8d93354fe2a8e50d5953f5ae2e47a3fc2ef03292e7ea46e3cc38f549525fb9"
dependencies = [
"cfg-if 1.0.0",
"pin-project-lite",
@ -3100,9 +3123,9 @@ dependencies = [
[[package]]
name = "tracing-attributes"
version = "0.1.18"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e"
checksum = "8276d9a4a3a558d7b7ad5303ad50b53d58264641b82914b7ada36bd762e7a716"
dependencies = [
"proc-macro2 1.0.36",
"quote 1.0.15",
@ -3111,11 +3134,12 @@ dependencies = [
[[package]]
name = "tracing-core"
version = "0.1.21"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4"
checksum = "03cfcb51380632a72d3111cb8d3447a8d908e577d31beeac006f836383d29a23"
dependencies = [
"lazy_static",
"valuable",
]
[[package]]
@ -3131,9 +3155,9 @@ dependencies = [
[[package]]
name = "tracing-subscriber"
version = "0.3.7"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5312f325fe3588e277415f5a6cca1f4ccad0f248c4cd5a4bd33032d7286abc22"
checksum = "74786ce43333fcf51efe947aed9718fbe46d5c7328ec3f1029e818083966d9aa"
dependencies = [
"ansi_term",
"lazy_static",
@ -3235,6 +3259,12 @@ dependencies = [
"getrandom 0.2.4",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
@ -3476,6 +3506,49 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ceb069ac8b2117d36924190469735767f0990833935ab430155e71a44bafe148"
dependencies = [
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_msvc"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d027175d00b01e0cbeb97d6ab6ebe03b12330a35786cbaca5252b1c4bf5d9b"
[[package]]
name = "windows_i686_gnu"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8793f59f7b8e8b01eda1a652b2697d87b93097198ae85f823b969ca5b89bba58"
[[package]]
name = "windows_i686_msvc"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8602f6c418b67024be2996c512f5f995de3ba417f4c75af68401ab8756796ae4"
[[package]]
name = "windows_x86_64_gnu"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3d615f419543e0bd7d2b3323af0d86ff19cbc4f816e6453f36a2c2ce889c354"
[[package]]
name = "windows_x86_64_msvc"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11d95421d9ed3672c280884da53201a5c46b7b2765ca6faf34b0d71cf34a3561"
[[package]]
name = "winres"
version = "0.1.12"

@ -10,20 +10,20 @@ edition = "2018"
build = "src/build.rs"
[build-dependencies]
tauri-build = { version = "^1.0.0-beta.4" }
tauri-build = "1.0.0-beta.4"
[dependencies]
serde_json = "^1.0"
serde = { version = "^1.0", features = ["derive"] }
thiserror = "^1.0.30"
typemap_rev = "^0.1.5"
serde_json = "1.0.78"
serde = { version = "1.0.136", features = ["derive"] }
thiserror = "1.0.30"
typemap_rev = "0.1.5"
[dependencies.tauri]
version = "^1.0.0-beta.8"
version = "1.0.0-beta.8"
features = ["dialog-all", "path-all", "shell-all"]
[dependencies.tracing-subscriber]
version = "^0.3.0"
version = "0.3.8"
features = ["env-filter"]
[dependencies.mediarepo-api]

@ -4,12 +4,14 @@ import {ApiFunction} from "./api-types/functions";
import {
AddLocalFileREquest,
AddRepositoryRequest,
AddSortingPresetRequest,
ChangeFileTagsRequest,
CheckDaemonRunningRequest,
CheckLocalRepositoryExistsRequest,
CreateTagsRequest,
DeleteFileRequest,
DeleteRepositoryRequest,
DeleteSortingPresetRequest,
DeleteThumbnailsRequest,
FindFilesRequest,
GetFileMetadataRequest,
@ -31,6 +33,7 @@ import {
import {RepositoryData, RepositoryMetadata, SizeMetadata} from "./api-types/repo";
import {CdTagMappings, NamespaceData, TagData} from "./api-types/tags";
import {ShortCache} from "./ShortCache";
import {SortingPresetData} from "./api-types/presets";
export class MediarepoApi {
@ -184,6 +187,18 @@ export class MediarepoApi {
return this.invokePlugin(ApiFunction.RunJob, request);
}
public static async getAllSortingPresets(): Promise<SortingPresetData[]> {
return ShortCache.cached("sorting-presets", () => this.invokePlugin(ApiFunction.GetAllSortingPresets), 1000);
}
public static async addSortingPreset(request: AddSortingPresetRequest): Promise<SortingPresetData> {
return this.invokePlugin(ApiFunction.AddSortingPreset, request);
}
public static async deleteSortingPreset(request: DeleteSortingPresetRequest): Promise<void> {
return this.invokePlugin(ApiFunction.DeleteSortingPreset, request);
}
private static async invokePlugin<T>(fn: ApiFunction, args?: any): Promise<T> {
return invoke<T>(`plugin:mediarepo|${fn}`, args);
}

@ -41,7 +41,7 @@ export type ValueComparator<T> =
| { Greater: T }
| { Between: T[] }
export type SortKey = { Namespace: SortNamespace }
export type SortKeyData = { Namespace: SortNamespace }
| { FileName: SortDirection }
| { FileSize: SortDirection }
| { FileImportedTime: SortDirection }

@ -40,4 +40,8 @@ export enum ApiFunction {
SetFrontendState = "set_frontend_state",
// jobs
RunJob = "run_job",
// presets
GetAllSortingPresets = "all_sorting_presets",
AddSortingPreset = "add_sorting_preset",
DeleteSortingPreset = "delete_sorting_preset",
}

@ -0,0 +1,6 @@
import {SortKeyData} from "./files";
export type SortingPresetData = {
id: number,
keys: SortKeyData[],
}

@ -1,4 +1,4 @@
import {FileOsMetadata, FileStatus, FilterExpression, SortKey} from "./files";
import {FileOsMetadata, FileStatus, FilterExpression, SortKeyData} from "./files";
import {RepositoryData, SizeType} from "./repo";
import {JobType} from "./job";
@ -40,7 +40,7 @@ export type GetSizeRequest = {
export type FindFilesRequest = {
filters: FilterExpression[],
sortBy: SortKey[]
sortBy: SortKeyData[]
};
export type UpdateFileNameRequest = {
@ -107,4 +107,12 @@ export type SetFrontendStateRequest = {
export type RunJobRequest = {
jobType: JobType,
}
};
export type AddSortingPresetRequest = {
sortKeys: SortKeyData[]
};
export type DeleteSortingPresetRequest = {
id: number
};

@ -22,11 +22,11 @@ export class File {
return this.basicData.status;
}
public get mimeType(): string {
return this.basicData.mime_type;
}
public set status(value: FileStatus) {
this.basicData.status = value;
}
public get mimeType(): string {
return this.basicData.mime_type;
}
}

@ -0,0 +1,108 @@
import {SortDirection, SortKeyData} from "../api-types/files";
export type SortType =
"Namespace"
| "FileName"
| "FileSize"
| "FileImportedTime"
| "FileCreatedTime"
| "FileChangeTime"
| "FileType"
| "NumTags";
export class SortKey {
constructor(private data: SortKeyData) {
this.data = data;
}
public get sortType(): SortType {
return Reflect.ownKeys(this.data)[0] as SortType;
}
public set sortType(value: SortType) {
if (value == "Namespace") {
this.data = {
Namespace: {
direction: this.sortDirection,
name: ""
}
};
} else {
this.data = {
[value]: this.sortDirection
} as SortKeyData;
}
}
public get sortDirection(): SortDirection {
if ("Namespace" in this.data) {
return this.data.Namespace.direction;
} else {
// @ts-ignore
return this.data[this.sortType];
}
}
public set sortDirection(value: SortDirection) {
const sortType = this.sortType;
if ("Namespace" in this.data) {
this.data.Namespace.direction = value;
} else {
// @ts-ignore
this.data[this.sortType] = value;
}
}
public get namespaceName(): string | undefined {
if ("Namespace" in this.data) {
return this.data.Namespace.name;
}
return undefined;
}
public set namespaceName(value: string | undefined) {
if (value && "Namespace" in this.data) {
this.data.Namespace.name = value;
}
}
public get rawData(): SortKeyData {
return this.data;
}
public static fromValues(
sortType: SortType,
sortDirection: SortDirection,
namespaceName: string | undefined
) {
let data;
if (sortType === "Namespace") {
data = {
Namespace: {
name: namespaceName!,
direction: sortDirection
}
};
} else {
data = {
[sortType]: sortDirection
} as SortKeyData;
}
return new SortKey(data);
}
public toString(): string {
if (this.sortType == "Namespace") {
return `${this.sortType} '${this.namespaceName}' ${this.getDirectionString()}`;
} else {
return `${this.sortType} ${this.getDirectionString()}`;
}
}
private getDirectionString(): string {
return this.sortDirection === "Ascending" ? "▲" : "▼";
}
}

@ -0,0 +1,48 @@
import {SortKey} from "./SortKey";
import {SortingPresetData} from "../api-types/presets";
import {mapNew} from "./adaptors";
export class SortingPreset {
private keys: SortKey[];
constructor(presetData: SortingPresetData) {
this._id = presetData.id;
this.keys = presetData.keys.map(mapNew(SortKey));
}
private _id: number;
public get id(): number {
return this._id;
}
public set id(value: number) {
this._id = value;
}
public get sortKeys(): SortKey[] {
return this.keys;
}
public get rawData(): SortingPresetData {
return {
id: this._id,
keys: this.keys.map(k => k.rawData),
};
}
public static fromValues(id: number, keys: SortKey[]) {
let preset = new SortingPreset({ id, keys: [] });
preset.keys = keys;
return preset;
}
public setData(data: SortingPresetData) {
this._id = data.id;
this.keys = data.keys.map(mapNew(SortKey));
}
public toString(): string {
return this.sortKeys.join(", ");
}
}

@ -36,6 +36,7 @@ mat-tab-group {
right: 0;
top: 0;
height: 100%;
ng-icon {
font-size: 1.5em;
margin-top: calc(-50%);

@ -1,17 +1,11 @@
import {NgModule} from "@angular/core";
import {CommonModule} from "@angular/common";
import {CoreComponent} from "./core.component";
import {
RepositoriesTabComponent
} from "./repositories-tab/repositories-tab.component";
import {RepositoriesTabComponent} from "./repositories-tab/repositories-tab.component";
import {FilesTabComponent} from "./files-tab/files-tab.component";
import {
FilesTabSidebarComponent
} from "./files-tab/files-tab-sidebar/files-tab-sidebar.component";
import {FilesTabSidebarComponent} from "./files-tab/files-tab-sidebar/files-tab-sidebar.component";
import {ImportTabComponent} from "./import-tab/import-tab.component";
import {
ImportTabSidebarComponent
} from "./import-tab/import-tab-sidebar/import-tab-sidebar.component";
import {ImportTabSidebarComponent} from "./import-tab/import-tab-sidebar/import-tab-sidebar.component";
import {MatButtonModule} from "@angular/material/button";
import {MatSidenavModule} from "@angular/material/sidenav";
import {MatProgressBarModule} from "@angular/material/progress-bar";
@ -30,9 +24,7 @@ import {SidebarModule} from "../shared/sidebar/sidebar.module";
import {FileModule} from "../shared/file/file.module";
import {AppCommonModule} from "../shared/app-common/app-common.module";
import {ReactiveFormsModule} from "@angular/forms";
import {
RepositoryCardComponent
} from "./repositories-tab/repository-card/repository-card.component";
import {RepositoryCardComponent} from "./repositories-tab/repository-card/repository-card.component";
import {MatCardModule} from "@angular/material/card";
import {MatListModule} from "@angular/material/list";
import {MatDialogModule} from "@angular/material/dialog";
@ -42,9 +34,7 @@ import {TagModule} from "../shared/tag/tag.module";
import {
DownloadDaemonDialogComponent
} from "./repositories-tab/download-daemon-dialog/download-daemon-dialog.component";
import {
RepositoryModule
} from "../shared/repository/repository/repository.module";
import {RepositoryModule} from "../shared/repository/repository/repository.module";
import {MatToolbarModule} from "@angular/material/toolbar";
import {
RepositoryDetailsViewComponent

@ -1,9 +1,12 @@
<div class="sidebar-inner">
<mat-tab-group headerPosition="below">
<mat-tab label="Search">
<app-file-search [state]="this.state" [availableTags]="this.allTags" [contextTags]="this.tags"
<app-file-search (searchEndEvent)="this.onDisplayedFilesChange(); this.searchEndEvent.emit($event);"
(searchStartEvent)="this.searchStartEvent.emit($event)"
(searchEndEvent)="this.onDisplayedFilesChange(); this.searchEndEvent.emit($event);" [tagsLoading]="this.tagsLoading"></app-file-search>
[availableTags]="this.allTags"
[contextTags]="this.tags"
[state]="this.state"
[tagsLoading]="this.tagsLoading"></app-file-search>
</mat-tab>
<mat-tab *ngIf="this.selectedFiles.length > 0" label="Edit Tags">
<app-tag-edit #fileedit [files]="this.selectedFiles"></app-tag-edit>

@ -1,25 +1,10 @@
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild
} from "@angular/core";
import {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";
import {
FileSearchComponent
} from "../../../shared/sidebar/file-search/file-search.component";
import {
RepositoryService
} from "../../../../services/repository/repository.service";
import {
TagEditComponent
} from "../../../shared/sidebar/tag-edit/tag-edit.component";
import {FileSearchComponent} from "../../../shared/sidebar/file-search/file-search.component";
import {RepositoryService} from "../../../../services/repository/repository.service";
import {TagEditComponent} from "../../../shared/sidebar/tag-edit/tag-edit.component";
import {TabState} from "../../../../models/TabState";
@Component({

@ -1,6 +1,7 @@
<mat-drawer-container (keyDownEvent)="this.onKeydown($event)" appInputReceiver autosize>
<mat-drawer disableClose mode="side" opened>
<app-files-tab-sidebar (searchEndEvent)="this.contentLoading = false;" (searchStartEvent)="this.contentLoading = true;"
<app-files-tab-sidebar (searchEndEvent)="this.contentLoading = false;"
(searchStartEvent)="this.contentLoading = true;"
[selectedFiles]="this.selectedFiles"
[state]="this.state"></app-files-tab-sidebar>
</mat-drawer>

@ -1,12 +1,13 @@
<div class="import-tab-inner">
<mat-tab-group headerPosition="below">
<mat-tab label="Import">
<app-file-import (importFinished)="importFinished.emit($event)" (fileImported)="fileImported.emit($event)"></app-file-import>
<app-file-import (fileImported)="fileImported.emit($event)"
(importFinished)="importFinished.emit($event)"></app-file-import>
</mat-tab>
<mat-tab label="Edit Tags" *ngIf="selectedFiles.length > 0">
<mat-tab *ngIf="selectedFiles.length > 0" label="Edit Tags">
<app-tag-edit [files]="selectedFiles"></app-tag-edit>
</mat-tab>
<mat-tab label="File Metadata" *ngIf="selectedFiles.length === 1">
<mat-tab *ngIf="selectedFiles.length === 1" label="File Metadata">
<app-file-metadata [file]="selectedFiles[0]"></app-file-metadata>
</mat-tab>
</mat-tab-group>

@ -1,11 +1,14 @@
<mat-drawer-container autosize>
<mat-drawer disableClose="true" mode="side" opened>
<app-import-tab-sidebar [selectedFiles]="selectedFiles" (fileImported)="this.addFileFromImport($event)"
(importFinished)="this.refreshFileView()"></app-import-tab-sidebar>
<app-import-tab-sidebar (fileImported)="this.addFileFromImport($event)"
(importFinished)="this.refreshFileView()"
[selectedFiles]="selectedFiles"></app-import-tab-sidebar>
</mat-drawer>
<mat-drawer-content>
<app-file-multiview [mode]="this.state.mode.value" (modeChangeEvent)="this.state.mode.next($event)"
[preselectedFile]="this.getSelectedFileFromState()"
(fileSelectEvent)="this.onFileSelect($event)" [files]="this.files"></app-file-multiview>
<app-file-multiview (fileSelectEvent)="this.onFileSelect($event)"
(modeChangeEvent)="this.state.mode.next($event)"
[files]="this.files"
[mode]="this.state.mode.value"
[preselectedFile]="this.getSelectedFileFromState()"></app-file-multiview>
</mat-drawer-content>
</mat-drawer-container>

@ -4,7 +4,8 @@
<div mat-dialog-content>
No daemon could be found on the system. Please download and install a daemon executable.
<br>
<button class="download-button" mat-flat-button color="primary" (click)="this.onClickDownloadDaemon()">Download</button>
<button (click)="this.onClickDownloadDaemon()" class="download-button" color="primary" mat-flat-button>Download
</button>
</div>
<div class="download-dialog-actions" mat-dialog-actions>
<button (click)="closeDialog(false)" color="accent" mat-stroked-button>Cancel</button>

@ -1,8 +1,6 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {
DownloadDaemonDialogComponent
} from "./download-daemon-dialog.component";
import {DownloadDaemonDialogComponent} from "./download-daemon-dialog.component";
describe("DownloadDaemonDialogComponent", () => {
let component: DownloadDaemonDialogComponent;

@ -9,9 +9,11 @@ import {shell} from "@tauri-apps/api";
})
export class DownloadDaemonDialogComponent {
constructor(public dialogRef: MatDialogRef<DownloadDaemonDialogComponent>,
constructor(
public dialogRef: MatDialogRef<DownloadDaemonDialogComponent>,
@Inject(
MAT_DIALOG_DATA) data: any) {
MAT_DIALOG_DATA) data: any
) {
}

@ -1,13 +1,14 @@
<div class="repo-page-content" *ngIf="!selectedRepository">
<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 [repository]="repository" (openEvent)="this.onOpenRepository($event)"></app-repository-card>
<app-repository-card (openEvent)="this.onOpenRepository($event)"
[repository]="repository"></app-repository-card>
</div>
</div>
</div>
<div class="repo-details" *ngIf="selectedRepository">
<div *ngIf="selectedRepository" class="repo-details">
<app-repository-details-view [repository]="selectedRepository"></app-repository-details-view>
</div>

@ -1,22 +1,9 @@
import {
Component, EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
ViewChild
} from "@angular/core";
import {Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild} from "@angular/core";
import {Repository} from "../../../../../api/models/Repository";
import {
RepositoryService
} from "../../../../services/repository/repository.service";
import {RepositoryService} from "../../../../services/repository/repository.service";
import {MatDialog} from "@angular/material/dialog";
import {
ConfirmDialogComponent
} from "../../../shared/app-common/confirm-dialog/confirm-dialog.component";
import {
BusyIndicatorComponent
} from "../../../shared/app-common/busy-indicator/busy-indicator.component";
import {ConfirmDialogComponent} from "../../../shared/app-common/confirm-dialog/confirm-dialog.component";
import {BusyIndicatorComponent} from "../../../shared/app-common/busy-indicator/busy-indicator.component";
import {
EditRepositoryDialogComponent
} from "../../../shared/repository/repository/edit-repository-dialog/edit-repository-dialog.component";
@ -39,7 +26,8 @@ export class RepositoryCardComponent implements OnInit, OnDestroy {
constructor(
public repoService: RepositoryService,
public dialog: MatDialog) {
public dialog: MatDialog
) {
}
public async ngOnInit() {
@ -81,28 +69,6 @@ export class RepositoryCardComponent implements OnInit, OnDestroy {
}
}
private async promtDeleteRepository() {
if (this.repository.local) {
const deleteContents = await this.dialog.open(
ConfirmDialogComponent, {
data: {
title: "Delete repository content",
message: "Do you want to remove the contents of the repository as well?",
confirmAction: "Delete",
confirmColor: "warn",
denyAction: "No",
}
}).afterClosed().toPromise();
if (deleteContents) {
await this.repoService.deleteRepository(this.repository.name);
} else {
await this.repoService.removeRepository(this.repository.name);
}
} else {
await this.repoService.removeRepository(this.repository.name);
}
}
public getDaemonStatusText(): string {
if (this.repository.local) {
return "Local";
@ -138,4 +104,26 @@ export class RepositoryCardComponent implements OnInit, OnDestroy {
}
});
}
private async promtDeleteRepository() {
if (this.repository.local) {
const deleteContents = await this.dialog.open(
ConfirmDialogComponent, {
data: {
title: "Delete repository content",
message: "Do you want to remove the contents of the repository as well?",
confirmAction: "Delete",
confirmColor: "warn",
denyAction: "No",
}
}).afterClosed().toPromise();
if (deleteContents) {
await this.repoService.deleteRepository(this.repository.name);
} else {
await this.repoService.removeRepository(this.repository.name);
}
} else {
await this.repoService.removeRepository(this.repository.name);
}
}
}

@ -1,13 +1,14 @@
<mat-toolbar>
<span class="repository-name">{{repository.name}}</span>
<button class="button-close-repository" mat-flat-button color="primary" (click)="this.closeRepository()">Close
<button (click)="this.closeRepository()" class="button-close-repository" color="primary" mat-flat-button>Close
</button>
</mat-toolbar>
<div class="details-content" fxLayout="row">
<div class="repository-metadata" fxFlex="100%">
<h1>Stats</h1>
<app-metadata-entry *ngIf="repository.path" attributeName="Path">{{repository.path}}</app-metadata-entry>
<app-metadata-entry *ngIf="repository.address" attributeName="Address">{{repository.address}}</app-metadata-entry>
<app-metadata-entry *ngIf="repository.address"
attributeName="Address">{{repository.address}}</app-metadata-entry>
<app-metadata-entry attributeName="File Count">
<mat-progress-bar *ngIf="!metadata"></mat-progress-bar>
{{metadata ? metadata!.file_count.toString() : ''}}
@ -29,15 +30,18 @@
{{this.totalSize | async}}
</app-metadata-entry>
<app-metadata-entry attributeName="File Folder Size">
<mat-progress-bar *ngIf="(this.fileFolderSize | async) === undefined" mode="indeterminate"></mat-progress-bar>
<mat-progress-bar *ngIf="(this.fileFolderSize | async) === undefined"
mode="indeterminate"></mat-progress-bar>
{{this.fileFolderSize | async}}
</app-metadata-entry>
<app-metadata-entry attributeName="Thumbnail Folder Size">
<mat-progress-bar *ngIf="(this.thumbFolderSize | async) === undefined" mode="indeterminate"></mat-progress-bar>
<mat-progress-bar *ngIf="(this.thumbFolderSize | async) === undefined"
mode="indeterminate"></mat-progress-bar>
{{this.thumbFolderSize | async}}
</app-metadata-entry>
<app-metadata-entry attributeName="Database File Size">
<mat-progress-bar *ngIf="(this.databaseFileSize | async) === undefined" mode="indeterminate"></mat-progress-bar>
<mat-progress-bar *ngIf="(this.databaseFileSize | async) === undefined"
mode="indeterminate"></mat-progress-bar>
{{this.databaseFileSize | async}}
</app-metadata-entry>
</div>

@ -1,8 +1,6 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {
RepositoryDetailsViewComponent
} from "./repository-details-view.component";
import {RepositoryDetailsViewComponent} from "./repository-details-view.component";
describe("RepositoryDetailsViewComponent", () => {
let component: RepositoryDetailsViewComponent;

@ -1,21 +1,10 @@
import {
Component,
Input,
OnChanges,
OnDestroy,
OnInit,
SimpleChanges
} from "@angular/core";
import {Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges} from "@angular/core";
import {Repository} from "../../../../../api/models/Repository";
import {
RepositoryService
} from "../../../../services/repository/repository.service";
import {RepositoryService} from "../../../../services/repository/repository.service";
import {RepositoryMetadata} from "../../../../models/RepositoryMetadata";
import {BehaviorSubject} from "rxjs";
import {MatDialog} from "@angular/material/dialog";
import {
BusyDialogComponent
} from "../../../shared/app-common/busy-dialog/busy-dialog.component";
import {BusyDialogComponent} from "../../../shared/app-common/busy-dialog/busy-dialog.component";
@Component({
selector: "app-repository-details-view",
@ -26,12 +15,11 @@ export class RepositoryDetailsViewComponent implements OnInit, OnChanges, OnDest
@Input() repository!: Repository;
public metadata?: RepositoryMetadata;
private refreshMetadataInterval?: number;
public totalSize = new BehaviorSubject<string | undefined>(undefined);
public fileFolderSize = new BehaviorSubject<string | undefined>(undefined);
public thumbFolderSize = new BehaviorSubject<string | undefined>(undefined);
public databaseFileSize = new BehaviorSubject<string | undefined>(undefined);
private refreshMetadataInterval?: number;
constructor(private repoService: RepositoryService, public dialog: MatDialog) {
}

@ -1,24 +1,17 @@
import {
Directive,
EventEmitter,
HostBinding,
HostListener,
Output
} from "@angular/core";
import {Directive, EventEmitter, HostBinding, HostListener, Output} from "@angular/core";
@Directive({
selector: "[appInputReceiver]"
})
export class InputReceiverDirective {
constructor() {
}
@Output() keyDownEvent = new EventEmitter<KeyboardEvent>();
@Output() keyUpEvent = new EventEmitter<KeyboardEvent>();
@HostBinding("tabindex") tabIndex = 1;
constructor() {
}
@HostListener("keydown", ["$event"])
onKeyDown(event: KeyboardEvent) {
this.keyDownEvent.emit(event);

@ -1,10 +1,4 @@
import {
Component,
HostListener,
Input,
OnChanges,
SimpleChanges
} from "@angular/core";
import {Component, HostListener, Input, OnChanges, SimpleChanges} from "@angular/core";
import {CdkDragMove} from "@angular/cdk/drag-drop";
import {SafeResourceUrl} from "@angular/platform-browser";

@ -1,12 +1,4 @@
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges
} from "@angular/core";
import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from "@angular/core";
import {FormControl} from "@angular/forms";
import {dialog} from "@tauri-apps/api";
import {DialogFilter} from "@tauri-apps/api/dialog";

@ -4,16 +4,24 @@
</div>
<div class="dialog-buttons" mat-dialog-actions>
<button (click)="closeDialog()" color="accent" mat-stroked-button>Cancel</button>
<button (click)="addRepository()" *ngIf="repoForm.formGroup.get('repositoryType')?.value === 'remote' || repoForm.localRepoExists"
[disabled]="!repoForm.formGroup.valid" color="primary" mat-flat-button
<button (click)="addRepository()"
*ngIf="repoForm.formGroup.get('repositoryType')?.value === 'remote' || repoForm.localRepoExists"
[disabled]="!repoForm.formGroup.valid"
color="primary"
mat-flat-button
matTooltip="Add the existing repository">Add
</button>
<button (click)="this.initLocalRepository()" *ngIf="repoForm.formGroup.get('repositoryType')?.value === 'local' && !repoForm.localRepoExists"
<button (click)="this.initLocalRepository()"
*ngIf="repoForm.formGroup.get('repositoryType')?.value === 'local' && !repoForm.localRepoExists"
[disabled]="!repoForm.formGroup.valid"
color="accent" mat-flat-button
color="accent"
mat-flat-button
matTooltip="Initialize the repository in the specified path">Init
</button>
<button (click)="repoForm.checkRepositoryStatus()" *ngIf="repoForm.formGroup.get('repositoryType')?.value === 'remote'" [disabled]="!repoForm.formGroup.valid"
class="check-connection-button" mat-stroked-button>Check Connection
<button (click)="repoForm.checkRepositoryStatus()"
*ngIf="repoForm.formGroup.get('repositoryType')?.value === 'remote'"
[disabled]="!repoForm.formGroup.valid"
class="check-connection-button"
mat-stroked-button>Check Connection
</button>
</div>

@ -1,8 +1,11 @@
<h1 mat-dialog-title>Edit {{this.selectedRepository.name}}</h1>
<div mat-dialog-content>
<app-repository-form #repoForm [name]="selectedRepository.name" [address]="selectedRepository.address ?? ''"
<app-repository-form #repoForm
[address]="selectedRepository.address ?? ''"
[name]="selectedRepository.name"
[path]="selectedRepository.path ?? ''"
[repositoryType]="selectedRepository.local? 'local' : 'remote'"
[path]="selectedRepository.path ?? ''" [validateNameDuplicate]="false"></app-repository-form>
[validateNameDuplicate]="false"></app-repository-form>
</div>
<div class="dialog-buttons" mat-dialog-actions>
<button (click)="closeDialog()" color="accent" mat-stroked-button>Cancel</button>

@ -1,8 +1,6 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {
EditRepositoryDialogComponent
} from "./edit-repository-dialog.component";
import {EditRepositoryDialogComponent} from "./edit-repository-dialog.component";
describe("EditRepositoryDialogComponent", () => {
let component: EditRepositoryDialogComponent;

@ -1,15 +1,7 @@
import {Component, Input, OnInit, Output} from "@angular/core";
import {
AbstractControl,
FormControl,
FormGroup,
ValidationErrors,
Validators
} from "@angular/forms";
import {AbstractControl, FormControl, FormGroup, ValidationErrors, Validators} from "@angular/forms";
import {Repository} from "../../../../../../api/models/Repository";
import {
RepositoryService
} from "../../../../../services/repository/repository.service";
import {RepositoryService} from "../../../../../services/repository/repository.service";
import {dialog} from "@tauri-apps/api";
import {MatDialog} from "@angular/material/dialog";

@ -1,14 +1,8 @@
import {NgModule} from "@angular/core";
import {CommonModule} from "@angular/common";
import {
AddRepositoryDialogComponent
} from "./add-repository-dialog/add-repository-dialog.component";
import {
RepositoryFormComponent
} from "./repository-form/repository-form.component";
import {
EditRepositoryDialogComponent
} from "./edit-repository-dialog/edit-repository-dialog.component";
import {AddRepositoryDialogComponent} from "./add-repository-dialog/add-repository-dialog.component";
import {RepositoryFormComponent} from "./repository-form/repository-form.component";
import {EditRepositoryDialogComponent} from "./edit-repository-dialog/edit-repository-dialog.component";
import {MatDialogModule} from "@angular/material/dialog";
import {MatButtonModule} from "@angular/material/button";
import {MatTooltipModule} from "@angular/material/tooltip";

@ -2,12 +2,12 @@
<app-metadata-entry *ngIf="mode === 'read'" [attributeName]="attributeName">{{value}}</app-metadata-entry>
<mat-form-field *ngIf="mode === 'write'">
<mat-label>{{attributeName}}</mat-label>
<input [formControl]="formControl" type="text" matInput [value]="value.toString()">
<input [formControl]="formControl" [value]="value.toString()" matInput type="text">
</mat-form-field>
<button *ngIf="mode === 'write'" mat-button (click)="this.onSave()">
<button (click)="this.onSave()" *ngIf="mode === 'write'" mat-button>
<ng-icon name="mat-save"></ng-icon>
</button>
<button *ngIf="mode === 'read'" mat-button (click)="mode = 'write'">
<button (click)="mode = 'write'" *ngIf="mode === 'read'" mat-button>
<ng-icon name="mat-edit"></ng-icon>
</button>
</div>

@ -1,8 +1,6 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {
EditableMetadataEntryComponent
} from "./editable-metadata-entry.component";
import {EditableMetadataEntryComponent} from "./editable-metadata-entry.component";
describe("EditableMetadataEntryComponent", () => {
let component: EditableMetadataEntryComponent;

@ -1,12 +1,4 @@
import {
Component,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges
} from "@angular/core";
import {Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges} from "@angular/core";
import {FormControl} from "@angular/forms";
@Component({

@ -20,9 +20,11 @@
width: 100%;
display: block;
}
:first-child {
margin-top: 0;
}
:last-child {
margin-bottom: 0;
}
@ -32,6 +34,7 @@
text-align: center;
height: 100px;
width: 100%;
h1 {
margin-top: 20px;
}

@ -35,7 +35,9 @@
</button>
</app-filter-input>
<button (click)="openSortDialog()" id="sort-button" mat-flat-button>Sort: {{sortExpression.join(", ")}}</button>
<app-sort-button (click)="openSortDialog()"
[selectedPreset]="this.sortingPreset"
id="sort-button"></app-sort-button>
</div>
<mat-divider fxFlex="1em"></mat-divider>

@ -9,7 +9,6 @@ import {
Output,
ViewChild
} from "@angular/core";
import {SortKey} from "../../../../models/SortKey";
import {MatDialog} from "@angular/material/dialog";
import {SortDialogComponent} from "./sort-dialog/sort-dialog.component";
import {LoggingService} from "../../../../services/logging/logging.service";
@ -23,6 +22,7 @@ import {FileStatus, FilterExpression,} from "../../../../../api/api-types/files"
import {filterExpressionToString} from "../../../../utils/filter-utils";
import {MatCheckboxChange} from "@angular/material/checkbox";
import * as deepEqual from "fast-deep-equal";
import {SortingPreset} from "../../../../../api/models/SortingPreset";
@Component({
@ -32,7 +32,7 @@ import * as deepEqual from "fast-deep-equal";
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FileSearchComponent implements AfterViewChecked, OnInit {
public sortExpression: SortKey[] = [];
public sortingPreset: SortingPreset = new SortingPreset({ id: -1, keys: [] });
public filters: SearchFilters = new SearchFilters([]);
@Input() availableTags: Tag[] = [];
@ -68,7 +68,7 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
this.filters = f;
this.assignDisplayedFilters();
});
this.state.sortKeys.subscribe(s => this.sortExpression = s);
this.state.sortingPreset.subscribe(s => this.sortingPreset = s);
this.applyStatusFromFilters();
this.needsScroll = true;
this.assignDisplayedFilters();
@ -127,22 +127,18 @@ export class FileSearchComponent implements AfterViewChecked, OnInit {
}
public openSortDialog() {
const sortEntries = this.sortExpression.map(
key => JSON.parse(JSON.stringify(key))).map(
key => new SortKey(key.sortType, key.sortDirection,
key.namespaceName
));
const sortingPreset = new SortingPreset(JSON.parse(JSON.stringify(this.sortingPreset.rawData)));
const openedDialog = this.dialog.open(SortDialogComponent, {
minWidth: "40vw",
data: {
sortEntries,
sortingPreset,
},
disableClose: true,
});
openedDialog.afterClosed().subscribe(async (sortExpression) => {
if (sortExpression) {
this.sortExpression = sortExpression;
this.state.setSortKeys(this.sortExpression);
openedDialog.afterClosed().subscribe(async (sortingPreset) => {
if (sortingPreset) {
this.sortingPreset = sortingPreset;
this.state.setSortingPreset(this.sortingPreset);
}
});
}

@ -0,0 +1,8 @@
<button (click)="this.appClick.emit()"
[matTooltipShowDelay]="1000"
[matTooltip]="selectedPreset.toString()"
class="sort-button"
mat-flat-button>
<i>Sort: </i>
<app-sort-preset-item [preset]="this.selectedPreset"></app-sort-preset-item>
</button>

@ -0,0 +1,9 @@
@import "src/colors";
.sort-button {
width: 100%;
align-items: center;
background-color: $background-lighter-10;
text-overflow: ellipsis;
overflow: hidden;
}

@ -0,0 +1,25 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {SortButtonComponent} from "./sort-button.component";
describe("SortButtonComponent", () => {
let component: SortButtonComponent;
let fixture: ComponentFixture<SortButtonComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SortButtonComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SortButtonComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,17 @@
import {ChangeDetectionStrategy, Component, EventEmitter, Input, Output} from "@angular/core";
import {SortingPreset} from "../../../../../../api/models/SortingPreset";
@Component({
selector: "app-sort-button",
templateUrl: "./sort-button.component.html",
styleUrls: ["./sort-button.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SortButtonComponent {
@Input() selectedPreset!: SortingPreset;
@Output() appClick = new EventEmitter<void>();
constructor() {
}
}

@ -1,7 +1,21 @@
<h1 mat-dialog-title>Sort Entries</h1>
<h1 mat-dialog-title>
Sort Entries
</h1>
<div mat-dialog-content>
<mat-form-field *ngIf="this.availablePresets.length > 0" class="preset-selection">
<mat-label>Preset</mat-label>
<mat-select (selectionChange)="this.selectPreset($event.value)" [value]="this.previousId">
<mat-option [value]="-1"></mat-option>
<mat-option *ngFor="let preset of this.availablePresets"
[matTooltipShowDelay]="1000"
[matTooltip]="preset.toString()"
[value]="preset.id">
{{preset.toString()}}
</mat-option>
</mat-select>
</mat-form-field>
<div (cdkDropListDropped)="this.onSortEntryDrop($event)" cdkDropList class="sort-input-list">
<div *ngFor="let sortKey of sortEntries" cdkDrag class="sort-input-row">
<div *ngFor="let sortKey of sortingPreset.sortKeys" cdkDrag class="sort-input-row">
<div *cdkDragPlaceholder class="drag-placeholder"></div>
<div cdkDragHandle class="drag-handle">
<ng-icon name="mat-drag-handle"></ng-icon>
@ -21,14 +35,16 @@
</mat-form-field>
<mat-form-field *ngIf="sortKey.sortType === 'Namespace'">
<mat-label>Namespace Name</mat-label>
<input #namespaceInput (change)="sortKey.namespaceName = namespaceInput.value"
[matAutocomplete]="namespaceAutocomplete"
(keyup)="this.updateAutocompleteSuggestions(namespaceInput.value)"
<input #namespaceInput
(change)="this.handlePresetChange(); sortKey.namespaceName = namespaceInput.value"
(focus)="this.updateAutocompleteSuggestions(namespaceInput.value)"
(keyup)="this.updateAutocompleteSuggestions(namespaceInput.value)"
[matAutocomplete]="namespaceAutocomplete"
[value]="sortKey.namespaceName ?? ''"
matInput
required>
<mat-autocomplete #namespaceAutocomplete (optionSelected)="sortKey.namespaceName = $event.option.value">
<mat-autocomplete #namespaceAutocomplete
(optionSelected)="this.handlePresetChange(); sortKey.namespaceName = $event.option.value">
<mat-option *ngFor="let namespace of suggestedNamespaces" [value]="namespace.name">
{{namespace.name}}
</mat-option>
@ -37,16 +53,18 @@
<div *ngIf="sortKey.sortType !== 'Namespace'" class="filler"></div>
<mat-form-field>
<mat-label>Direction</mat-label>
<mat-select [(value)]="sortKey.sortDirection" required>
<mat-select (selectionChange)="this.handlePresetChange()" [(value)]="sortKey.sortDirection" required>
<mat-option value="Ascending">Ascending</mat-option>
<mat-option value="Descending">Descending</mat-option>
</mat-select>
</mat-form-field>
<button (click)="addNewSortKey()" *ngIf="sortEntries.indexOf(sortKey) === sortEntries.length -1"
<button (click)="addNewSortKey()"
*ngIf="sortingPreset.sortKeys.indexOf(sortKey) === sortingPreset.sortKeys.length - 1"
mat-flat-button>
<ng-icon name="mat-add"></ng-icon>
</button>
<button (click)="removeSortKey(sortKey)" *ngIf="sortEntries.indexOf(sortKey) !== sortEntries.length -1"
<button (click)="removeSortKey(sortKey)"
*ngIf="sortingPreset.sortKeys.indexOf(sortKey) !== sortingPreset.sortKeys.length -1"
mat-flat-button>
<ng-icon name="mat-remove"></ng-icon>
</button>
@ -54,6 +72,22 @@
</div>
</div>
<div class="dialog-actions" mat-dialog-actions>
<button (click)="confirmSort()" color="primary" mat-flat-button>Sort</button>
<button (click)="deletePreset()" *ngIf="this.previousId >= 0" class="button-left" color="warn" mat-stroked-button>
Delete
</button>
<button (click)="saveNewPreset()"
*ngIf="this.sortingPreset.sortKeys.length > 0"
class="button-left"
color="accent"
mat-stroked-button>Save new
</button>
<button (click)="savePreset()"
*ngIf="this.sortingPreset.sortKeys.length > 0 && this.previousId >= 0"
class="button-left"
color="accent"
mat-flat-button>Save
</button>
<button (click)="cancelSort()" color="accent" mat-stroked-button>Cancel</button>
<button (click)="confirmSort()" color="primary" mat-flat-button>Sort</button>
</div>

@ -18,12 +18,17 @@ mat-form-field, .filler, .drag-handle {
}
.dialog-actions {
display: flex;
flex-direction: row-reverse;
width: 100%;
display: block;
button {
margin-left: 1em;
float: right;
}
button.button-left {
float: left;
margin-right: 1em;
}
}
@ -57,3 +62,9 @@ mat-form-field, .filler, .drag-handle {
background-color: darken(dimgrey, 20);
border-radius: 1em;
}
.preset-selection {
width: calc(100% - 2em);
display: block;
font-size: 1em;
}

@ -1,10 +1,13 @@
import {ChangeDetectionStrategy, Component, Inject} from "@angular/core";
import {ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit} from "@angular/core";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {SortKey} from "../../../../../models/SortKey";
import {SortKey} from "../../../../../../api/models/SortKey";
import {CdkDragDrop, moveItemInArray} from "@angular/cdk/drag-drop";
import {Namespace} from "../../../../../../api/models/Namespace";
import {TagService} from "../../../../../services/tag/tag.service";
import {compareSearchResults} from "../../../../../utils/compare-utils";
import {SortingPreset} from "../../../../../../api/models/SortingPreset";
import {PresetService} from "../../../../../services/preset/preset.service";
import {LoggingService} from "../../../../../services/logging/logging.service";
@Component({
selector: "app-sort-dialog",
@ -12,32 +15,52 @@ import {compareSearchResults} from "../../../../../utils/compare-utils";
styleUrls: ["./sort-dialog.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class SortDialogComponent {
export class SortDialogComponent implements OnInit {
public sortEntries: SortKey[] = [];
public sortingPreset: SortingPreset = SortingPreset.fromValues(-1, []);
public availablePresets: SortingPreset[] = [];
public suggestedNamespaces: Namespace[] = [];
public emptyPreset = SortingPreset.fromValues(-1, [
SortKey.fromValues("FileImportedTime", "Ascending", undefined)
]);
public previousId: number = -1;
private namespaces: Namespace[] = [];
constructor(public tagService: TagService, public dialogRef: MatDialogRef<SortDialogComponent>, @Inject(
MAT_DIALOG_DATA) data: any) {
this.sortEntries = data.sortEntries;
constructor(
public logger: LoggingService,
public tagService: TagService,
public presetService: PresetService,
public changeDetector: ChangeDetectorRef,
public dialogRef: MatDialogRef<SortDialogComponent>,
@Inject(
MAT_DIALOG_DATA) data: any
) {
this.sortingPreset = data.sortingPreset;
this.previousId = this.sortingPreset.id;
console.debug(this.sortingPreset);
tagService.namespaces.subscribe(
namespaces => this.namespaces = namespaces);
}
public async ngOnInit() {
this.availablePresets = await this.presetService.getAllSortingPresets();
}
addNewSortKey() {
const sortKey = new SortKey("FileName", "Ascending", undefined);
this.sortEntries.push(sortKey);
const sortKey = SortKey.fromValues("FileName", "Ascending", undefined);
this.handlePresetChange();
this.sortingPreset.sortKeys.push(sortKey);
}
public removeSortKey(sortKey: SortKey): void {
const index = this.sortEntries.indexOf(sortKey);
this.sortEntries.splice(index, 1);
const index = this.sortingPreset.sortKeys.indexOf(sortKey);
this.handlePresetChange();
this.sortingPreset.sortKeys.splice(index, 1);
}
public confirmSort(): void {
this.dialogRef.close(this.sortEntries);
this.dialogRef.close(this.sortingPreset);
}
public cancelSort(): void {
@ -45,7 +68,8 @@ export class SortDialogComponent {
}
public onSortEntryDrop(event: CdkDragDrop<SortKey[]>): void {
moveItemInArray(this.sortEntries, event.previousIndex,
this.handlePresetChange();
moveItemInArray(this.sortingPreset.sortKeys, event.previousIndex,
event.currentIndex
);
}
@ -55,4 +79,50 @@ export class SortDialogComponent {
(a, b) => compareSearchResults(value, a.name, b.name))
.slice(0, 50);
}
public handlePresetChange() {
if (this.sortingPreset.id >= 0) {
this.previousId = this.sortingPreset.id;
this.sortingPreset.id = -1;
}
}
public async savePreset() {
await this.deletePreset();
await this.saveNewPreset();
}
public async saveNewPreset() {
let newPreset = await this.logger.try(() => this.presetService.addSortingPreset(this.sortingPreset.sortKeys));
if (newPreset) {
this.sortingPreset.setData(newPreset.rawData);
this.previousId = this.sortingPreset.id;
this.availablePresets.push(new SortingPreset(JSON.parse(JSON.stringify(newPreset.rawData))));
this.changeDetector.detectChanges();
}
}
public async deletePreset() {
if (this.previousId >= 0) {
const index = this.availablePresets.findIndex(p => p.id == this.previousId);
if (index >= 0) {
this.availablePresets.splice(index, 1);
this.changeDetector.detectChanges();
}
try {
await this.presetService.deleteSortingPreset(this.previousId);
} catch (err: any) {
this.logger.warn(`Could not delete previous preset: ${err.message}`);
}
}
}
public selectPreset(presetId: number): void {
const preset = this.availablePresets.find(p => p.id == presetId) ?? this.emptyPreset;
if (preset) {
this.sortingPreset.setData(JSON.parse(JSON.stringify(preset.rawData)));
this.previousId = preset.id;
}
}
}

@ -0,0 +1,7 @@
<span *ngFor="let key of this.preset.sortKeys" class="sort-key">
<span class="sort-key-type">{{key.sortType}}</span>
<span *ngIf="key.sortType === 'Namespace'" class="sort-key-namespace"> {{key.namespaceName}}</span>
<ng-icon *ngIf="key.sortDirection === 'Ascending'" class="sort-key-direction" name="matExpandLess"></ng-icon>
<ng-icon *ngIf="key.sortDirection === 'Descending'" class="sort-key-direction" name="matExpandMore"></ng-icon>
<span class="key-divider">| </span>
</span>

@ -0,0 +1,20 @@
@import "src/colors";
.sort-key-direction {
font-size: 1.5em;
vertical-align: bottom;
margin-bottom: 0.6em;
}
.sort-key-type {
color: $primary-lighter-50
}
.sort-key-namespace {
color: $accent-lighter-10;
}
.sort-key:last-child .key-divider {
display: none;
}

@ -0,0 +1,25 @@
import {ComponentFixture, TestBed} from "@angular/core/testing";
import {SortPresetItemComponent} from "./sort-preset-item.component";
describe("SortPresetItemComponent", () => {
let component: SortPresetItemComponent;
let fixture: ComponentFixture<SortPresetItemComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [SortPresetItemComponent]
})
.compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(SortPresetItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it("should create", () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,16 @@
import {ChangeDetectionStrategy, Component, Input} from "@angular/core";
import {SortingPreset} from "../../../../../../api/models/SortingPreset";
@Component({
selector: "app-sort-preset-item",
templateUrl: "./sort-preset-item.component.html",
styleUrls: ["./sort-preset-item.component.scss"],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SortPresetItemComponent {
@Input() preset!: SortingPreset;
constructor() {
}
}

@ -7,9 +7,11 @@ import {
MatAddCircle,
MatChangeCircle,
MatDeleteSweep,
MatExpandLess,
MatExpandMore,
MatFilterAlt,
MatRemove,
MatRemoveCircle
MatRemoveCircle,
} from "@ng-icons/material-icons";
import {MatRippleModule} from "@angular/material/core";
import {MatButtonModule} from "@angular/material/button";
@ -48,6 +50,9 @@ import {
} from "./file-search/filter-dialog/filter-expression-list-item/filter-expression-list-item.component";
import {GetTagQueryPipe} from "./file-search/filter-pipes/get-tag-query.pipe";
import {GetPropertyQueryPipe} from "./file-search/filter-pipes/get-property-query.pipe";
import {SortButtonComponent} from "./file-search/sort-button/sort-button.component";
import {MatTooltipModule} from "@angular/material/tooltip";
import {SortPresetItemComponent} from "./file-search/sort-preset-item/sort-preset-item.component";
@NgModule({
@ -66,6 +71,8 @@ import {GetPropertyQueryPipe} from "./file-search/filter-pipes/get-property-quer
FilterExpressionListItemComponent,
GetTagQueryPipe,
GetPropertyQueryPipe,
SortButtonComponent,
SortPresetItemComponent,
],
exports: [
TagEditComponent,
@ -81,7 +88,9 @@ import {GetPropertyQueryPipe} from "./file-search/filter-pipes/get-property-quer
MatAddCircle,
MatRemoveCircle,
MatDeleteSweep,
MatFilterAlt
MatFilterAlt,
MatExpandMore,
MatExpandLess,
}),
MatRippleModule,
MatButtonModule,
@ -102,6 +111,7 @@ import {GetPropertyQueryPipe} from "./file-search/filter-pipes/get-property-quer
MatMenuModule,
ReactiveFormsModule,
MatAutocompleteModule,
MatTooltipModule,
]
})
export class SidebarModule {

@ -1,35 +0,0 @@
export class SortKey {
constructor(
public sortType: "Namespace" | "FileName" | "FileSize" | "FileImportedTime" | "FileCreatedTime" | "FileChangeTime" | "FileType" | "NumTags",
public sortDirection: "Ascending" | "Descending",
public namespaceName: string | undefined
) {
}
public toString(): string {
if (this.sortType == "Namespace") {
return `${this.sortType} '${this.namespaceName}' ${this.sortDirection}`;
} else {
return `${this.sortType} ${this.sortDirection}`;
}
}
public toBackendType(): any {
if (this.sortType == "Namespace") {
return {
"Namespace": {
direction: this.sortDirection,
name: this.namespaceName
}
};
} else {
let returnObj: any = {};
returnObj[this.sortType] = this.sortDirection;
return returnObj;
}
}
}

@ -2,10 +2,11 @@ import {BehaviorSubject} from "rxjs";
import {TabCategory} from "./TabCategory";
import {FileService} from "../services/file/file.service";
import {File} from "../../api/models/File";
import {SortKey} from "./SortKey";
import {SortKey} from "../../api/models/SortKey";
import {debounceTime} from "rxjs/operators";
import {mapNew} from "../../api/models/adaptors";
import {SearchFilters} from "../../api/models/SearchFilters";
import {SortingPreset} from "../../api/models/SortingPreset";
export class TabState {
public uuid: number;
@ -16,12 +17,14 @@ export class TabState {
public files = new BehaviorSubject<File[]>([]);
public filters = new BehaviorSubject<SearchFilters>(new SearchFilters([]));
public sortKeys = new BehaviorSubject<SortKey[]>(
[new SortKey(
public sortingPreset = new BehaviorSubject<SortingPreset>(SortingPreset.fromValues(
-1,
[SortKey.fromValues(
"FileImportedTime",
"Ascending",
undefined
)]);
)]
));
private fileService: FileService;
@ -36,7 +39,7 @@ export class TabState {
if (this.category === TabCategory.Files) {
this.filters.pipe(debounceTime(500))
.subscribe(async () => await this.findFiles());
this.sortKeys.pipe(debounceTime(100))
this.sortingPreset.pipe(debounceTime(100))
.subscribe(async () => await this.findFiles());
}
}
@ -50,16 +53,9 @@ export class TabState {
dto.category,
fileService
);
const sortKeys = dto.sortKeys.map(
(s: { sortType: any, sortDirection: any, namespaceName: any }) =>
new SortKey(
s.sortType,
s.sortDirection,
s.namespaceName
)
);
state.filters.next(new SearchFilters(dto.filters ?? []));
state.sortKeys.next(sortKeys);
state.sortingPreset.next(new SortingPreset(dto.sortingPreset));
state.mode.next(dto.mode ?? "grid");
state.selectedCD.next(dto.selectedFileHash);
state.files.next((dto.files ?? []).map(mapNew(File)));
@ -71,7 +67,7 @@ export class TabState {
this.loading.next(true);
const files = await this.fileService.findFiles(
this.filters.value,
this.sortKeys.value
this.sortingPreset.value.sortKeys
);
this.files.next(files);
this.loading.next(false);
@ -81,8 +77,8 @@ export class TabState {
this.filters.next(filters);
}
public setSortKeys(keys: SortKey[]) {
this.sortKeys.next(keys);
public setSortingPreset(preset: SortingPreset) {
this.sortingPreset.next(preset);
}
public getDTO(): any {
@ -90,7 +86,7 @@ export class TabState {
uuid: this.uuid,
category: this.category,
filters: this.filters.value.getFilters(),
sortKeys: this.sortKeys.value,
sortingPreset: this.sortingPreset.value.rawData,
mode: this.mode.value,
selectedFileHash: this.selectedCD.value,
files: this.category === TabCategory.Import ? this.files.value.map(

@ -1,7 +1,7 @@
import {Inject, Injectable} from "@angular/core";
import {File} from "../../../api/models/File";
import {DomSanitizer, SafeResourceUrl} from "@angular/platform-browser";
import {SortKey} from "../../models/SortKey";
import {SortKey} from "../../../api/models/SortKey";
import {MediarepoApi} from "../../../api/Api";
import {mapMany, mapNew} from "../../../api/models/adaptors";
import {FileMetadata, FileStatus} from "../../../api/api-types/files";
@ -26,7 +26,7 @@ export class FileService {
return MediarepoApi.findFiles(
{
filters: filters.getFilters(),
sortBy: sortBy.map(k => k.toBackendType())
sortBy: sortBy.map(k => k.rawData)
})
.then(mapMany(mapNew(File)));
}

@ -0,0 +1,16 @@
import {TestBed} from "@angular/core/testing";
import {PresetService} from "./preset.service";
describe("PresetService", () => {
let service: PresetService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(PresetService);
});
it("should be created", () => {
expect(service).toBeTruthy();
});
});

@ -0,0 +1,26 @@
import {Injectable} from "@angular/core";
import {SortingPreset} from "../../../api/models/SortingPreset";
import {MediarepoApi} from "../../../api/Api";
import {mapMany, mapNew} from "../../../api/models/adaptors";
import {SortKey} from "../../../api/models/SortKey";
@Injectable({
providedIn: "root"
})
export class PresetService {
constructor() {
}
public async getAllSortingPresets(): Promise<SortingPreset[]> {
return MediarepoApi.getAllSortingPresets().then(mapMany(mapNew(SortingPreset)));
}
public async addSortingPreset(keys: SortKey[]): Promise<SortingPreset> {
return MediarepoApi.addSortingPreset({ sortKeys: keys.map(k => k.rawData) }).then(mapNew(SortingPreset));
}
public async deleteSortingPreset(id: number): Promise<void> {
return MediarepoApi.deleteSortingPreset({ id });
}
}

@ -59,6 +59,16 @@ export class StateService {
this.state.next(state);
}
/**
* Sets the state of the frontend
* @returns {Promise<void>}
*/
public async saveState(): Promise<void> {
if (this.repoService.selectedRepository.value) {
await MediarepoApi.setFrontendState({ state: this.state.value.serializeJson() });
}
}
private subscribeToState(state: AppState) {
state.tabs.subscribe(async tabs => {
this.tabSubscriptions.forEach(s => s.unsubscribe());
@ -70,7 +80,7 @@ export class StateService {
private subscribeToTab(tab: TabState) {
this.tabSubscriptions.push(tab.filters
.subscribe(() => this.stateChange.next()));
this.tabSubscriptions.push(tab.sortKeys
this.tabSubscriptions.push(tab.sortingPreset
.subscribe(() => this.stateChange.next()));
this.tabSubscriptions.push(
tab.selectedCD.subscribe(() => this.stateChange.next()));
@ -78,14 +88,4 @@ export class StateService {
tab.mode.subscribe(() => this.stateChange.next()));
this.tabSubscriptions.push(tab.files.subscribe(() => this.stateChange.next()));
}
/**
* Sets the state of the frontend
* @returns {Promise<void>}
*/
public async saveState(): Promise<void> {
if (this.repoService.selectedRepository.value) {
await MediarepoApi.setFrontendState({state: this.state.value.serializeJson()});
}
}
}

@ -2,81 +2,81 @@
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="120"
height="120"
viewBox="0 0 120 120"
version="1.1"
id="svg5"
sodipodi:docname="mediarepo-icon.svg"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
inkscape:export-filename="/home/trivernis/Pictures/mediarepo-icon_32x32.png"
inkscape:export-xdpi="25.6"
inkscape:export-ydpi="25.6"
inkscape:version="1.1.1 (3bf5ae0d25, 2021-09-20, custom)"
sodipodi:docname="mediarepo-icon.svg"
version="1.1"
viewBox="0 0 120 120"
width="120"
xmlns="http://www.w3.org/2000/svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
>
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:document-units="px"
showgrid="true"
inkscape:lockguides="true"
objecttolerance="20"
guidetolerance="10000"
inkscape:zoom="5.6833333"
id="namedview7"
inkscape:current-layer="layer1"
inkscape:cx="32.199413"
inkscape:cy="77.859238"
inkscape:window-width="2560"
inkscape:document-units="px"
inkscape:lockguides="true"
inkscape:pagecheckerboard="0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:window-height="1359"
inkscape:window-maximized="1"
inkscape:window-width="2560"
inkscape:window-x="1200"
inkscape:window-y="240"
inkscape:window-maximized="1"
inkscape:current-layer="layer1">
inkscape:zoom="5.6833333"
objecttolerance="20"
pagecolor="#ffffff"
showgrid="true">
<inkscape:grid
type="xygrid"
id="grid13779"
dotted="false"
id="grid13779"
spacingx="0.5"
spacingy="0.5" />
spacingy="0.5"
type="xygrid"/>
</sodipodi:namedview>
<defs
id="defs2"/>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:groupmode="layer"
inkscape:label="Background"
style="display:none">
<rect
style="fill:#010001;fill-opacity:1;stroke:none;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
height="142.5"
id="rect40027"
style="fill:#010001;fill-opacity:1;stroke:none;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
width="170"
height="142.5"
x="-20"
y="-10"/>
</g>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
inkscape:groupmode="layer"
inkscape:label="Layer 1"
style="display:inline">
<g
aria-label="M"
id="text13769"
style="font-size:120px;font-family:'DejaVu Sans', 'Bitstream Vera Sans';fill:#b300df;fill-opacity:1;stroke:none;stroke-opacity:1"
inkscape:export-filename="/home/trivernis/Pictures/mediarepo-icon_128x128.png"
inkscape:export-xdpi="102.4"
inkscape:export-ydpi="102.4">
inkscape:export-ydpi="102.4"
style="font-size:120px;font-family:'DejaVu Sans', 'Bitstream Vera Sans';fill:#b300df;fill-opacity:1;stroke:none;stroke-opacity:1">
<path
id="path14062"
style="fill:#b300df;fill-opacity:1;stroke:none;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 91.853516,14.957031 C 87.625624,14.940758 82.986173,16.079346 80,17.5 69.344922,22.56909 60,45 60,45 60,45 50.680027,22.566415 40,17.5 34.688445,14.980293 24.157014,13.342986 20,17.5 -0.61934311,38.119343 15,80 5,105 3.5782709,108.55432 17.01074,102.39141 20,100 32.5,90 33.253324,58.268354 32.5,47.5 33.314651,57.148409 35.039944,78.87714 50,90 c 5.349969,3.977723 14.654765,3.984082 20,0 10.464133,-7.799463 15.084189,-27.639159 17.5,-42.5 -1.296836,17.464817 0,42.5 12.5,52.5 3.00451,2.40361 16.42898,8.57246 15,5 -10,-25 5.61934,-66.880657 -15,-87.5 -1.818694,-1.818694 -4.858124,-2.530312 -8.146484,-2.542969 z M 60,50 c 8.28427,0 15,6.715729 15,15 0,8.28427 -6.71573,15 -15,15 -8.284271,0 -15,-6.71573 -15,-15 0,-8.284271 6.715729,-15 15,-15 z m 0,5.615234 c -5.182976,0 -9.384766,4.201791 -9.384766,9.384766 0,5.182975 4.20179,9.384766 9.384766,9.384766 5.182975,0 9.384766,-4.201791 9.384766,-9.384766 0,-5.182975 -4.201791,-9.384766 -9.384766,-9.384766 z"
sodipodi:nodetypes="sscsssscsscssssssssssssss" />
id="path14062"
sodipodi:nodetypes="sscsssscsscssssssssssssss"
style="fill:#b300df;fill-opacity:1;stroke:none;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

@ -6,6 +6,7 @@ $primary-lighter-10: lighten($primary, 10%);
$primary-lighter-20: lighten($primary, 20%);
$primary-lighter-30: lighten($primary, 30%);
$primary-lighter-40: lighten($primary, 40%);
$primary-lighter-50: lighten($primary, 50%);
$primary-darker-10: darken($primary, 10%);
$primary-darker-20: darken($primary, 20%);
$primary-darker-30: darken($primary, 30%);
@ -21,6 +22,8 @@ $text: #FFF;
$text-darker-10: darken($text, 10%);
$background: #424242;
$background-lighter-05: lighten($background, 4%);
$background-lighter-10: lighten($background, 10%);
// specifics
$tag-namespace: $accent-lighter-10;

@ -2,10 +2,7 @@
import "zone.js/testing";
import {getTestBed} from "@angular/core/testing";
import {
BrowserDynamicTestingModule,
platformBrowserDynamicTesting
} from "@angular/platform-browser-dynamic/testing";
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from "@angular/platform-browser-dynamic/testing";
declare const require: {
context(path: string, deep?: boolean, filter?: RegExp): {

Loading…
Cancel
Save