1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2025-12-23 23:02:58 +00:00

Merge pull request #60 from koloml/release/0.3.3

Release: 0.3.3
This commit is contained in:
2024-11-30 22:10:25 +04:00
committed by GitHub
38 changed files with 315 additions and 287 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "Furbooru Tagging Assistant",
"description": "Experimental extension with a set of tools to make the tagging faster and easier. Made specifically for Furbooru.",
"version": "0.3.2",
"version": "0.3.3",
"browser_specific_settings": {
"gecko": {
"id": "furbooru-tagging-assistant@thecore.city"

18
package-lock.json generated
View File

@@ -1,13 +1,14 @@
{
"name": "furbooru-tagging-assistant",
"version": "0.3.2",
"version": "0.3.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "furbooru-tagging-assistant",
"version": "0.3.2",
"version": "0.3.3",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.1",
"lz-string": "^1.5.0"
},
"devDependencies": {
@@ -405,6 +406,14 @@
"node": ">=12"
}
},
"node_modules/@fortawesome/fontawesome-free": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.1.tgz",
"integrity": "sha512-ALIk/MOh5gYe1TG/ieS5mVUsk7VUIJTJKPMK9rFFqOgfp0Q3d5QiBXbcOMwUvs37fyZVCz46YjOE6IFeOAXCHA==",
"engines": {
"node": ">=6"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",
@@ -2395,6 +2404,11 @@
"dev": true,
"optional": true
},
"@fortawesome/fontawesome-free": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.1.tgz",
"integrity": "sha512-ALIk/MOh5gYe1TG/ieS5mVUsk7VUIJTJKPMK9rFFqOgfp0Q3d5QiBXbcOMwUvs37fyZVCz46YjOE6IFeOAXCHA=="
},
"@jridgewell/gen-mapping": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz",

View File

@@ -1,13 +1,13 @@
{
"name": "furbooru-tagging-assistant",
"version": "0.3.2",
"version": "0.3.3",
"private": true,
"scripts": {
"build": "npm run build:popup && npm run build:extension",
"build:popup": "vite build",
"build:extension": "node build-extension.js",
"check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch"
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0",
@@ -24,6 +24,7 @@
},
"type": "module",
"dependencies": {
"@fortawesome/fontawesome-free": "^6.7.1",
"lz-string": "^1.5.0"
}
}

41
src/app.d.ts vendored
View File

@@ -1,24 +1,31 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
type LinkTarget = "_blank" | "_self" | "_parent" | "_top";
type IconName = (
"tag"
| "paint-brush"
| "arrow-left"
| "info-circle"
| "wrench"
| "globe"
| "plus"
| "file-export"
);
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface PageState {}
// interface Platform {}
type LinkTarget = "_blank" | "_self" | "_parent" | "_top";
type IconName = (
"tag"
| "paint-brush"
| "arrow-left"
| "info-circle"
| "wrench"
| "globe"
| "plus"
| "file-export"
| "trash"
);
interface EntityNamesMap {
profiles: MaintenanceProfile;
}
}
}
export {};

View File

@@ -1,5 +1,5 @@
<script>
/** @type {import('$lib/extension/entities/MaintenanceProfile.js').default} */
/** @type {import('$entities/MaintenanceProfile.ts').default} */
export let profile;
const sortedTagsList = profile.settings.tags.sort((a, b) => a.localeCompare(b));

View File

@@ -2,9 +2,11 @@ import {BaseComponent} from "$lib/components/base/BaseComponent.js";
export class FullscreenViewer extends BaseComponent {
/** @type {HTMLVideoElement} */
#videoElement;
#videoElement = document.createElement('video');
/** @type {HTMLImageElement} */
#imageElement;
#imageElement = document.createElement('img');
#spinnerElement = document.createElement('i');
/** @type {number|null} */
#touchId = null;
@@ -20,9 +22,9 @@ export class FullscreenViewer extends BaseComponent {
*/
build() {
this.container.classList.add('fullscreen-viewer');
this.container.append(this.#spinnerElement);
this.#videoElement = document.createElement('video');
this.#imageElement = document.createElement('img');
this.#spinnerElement.classList.add('spinner', 'fa', 'fa-circle-notch', 'fa-spin');
}
/**
@@ -35,6 +37,13 @@ export class FullscreenViewer extends BaseComponent {
this.on('touchstart', this.#onTouchStart.bind(this));
this.on('touchmove', this.#onTouchMove.bind(this));
this.on('touchend', this.#onTouchEnd.bind(this));
this.#videoElement.addEventListener('loadeddata', this.#onLoaded.bind(this));
this.#imageElement.addEventListener('load', this.#onLoaded.bind(this));
}
#onLoaded() {
this.container.classList.remove('loading');
}
/**
@@ -169,6 +178,8 @@ export class FullscreenViewer extends BaseComponent {
* @param {string} url
*/
show(url) {
this.container.classList.add('loading');
requestAnimationFrame(() => {
this.container.classList.add(FullscreenViewer.#shownState);
document.body.style.overflow = 'hidden';

View File

@@ -1,5 +1,5 @@
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import {getComponent} from "$lib/components/base/ComponentUtils.js";
import ScrapedAPI from "$lib/booru/scraped/ScrapedAPI.js";

View File

@@ -1,5 +1,5 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.js";
import {getComponent} from "$lib/components/base/ComponentUtils.js";

View File

@@ -1,4 +1,5 @@
import StorageHelper from "$lib/browser/StorageHelper.js";
import type StorageEntity from "$lib/extension/base/StorageEntity.ts";
export default class EntitiesController {
static #storageHelper = new StorageHelper(chrome.storage.local);
@@ -6,15 +7,13 @@ export default class EntitiesController {
/**
* Read all entities of the given type from the storage. Build the entities from the raw data and return them.
*
* @template EntityClass
* @param entityName Name of the entity to read.
* @param entityClass Class of the entity to read. Must have a constructor that accepts the ID and the settings
* object.
*
* @param {string} entityName Name of the entity to read.
* @param {EntityClass} entityClass Class of the entity to read. Must have a constructor that accepts the ID and the
* settings object.
*
* @return {Promise<InstanceType<EntityClass>[]>} List of entities of the given type.
* @return List of entities of the given type.
*/
static async readAllEntities(entityName, entityClass) {
static async readAllEntities<Type extends StorageEntity<any>>(entityName: string, entityClass: new (...any: any[]) => Type): Promise<Type[]> {
const rawEntities = await this.#storageHelper.read(entityName, {});
if (!rawEntities || Object.keys(rawEntities).length === 0) {
@@ -29,13 +28,11 @@ export default class EntitiesController {
/**
* Update the single entity in the storage. If the entity with the given ID already exists, it will be overwritten.
*
* @param {string} entityName Name of the entity to update.
* @param {StorageEntity} entity Entity to update.
*
* @return {Promise<void>}
* @param entityName Name of the entity to update.
* @param entity Entity to update.
*/
static async updateEntity(entityName, entity) {
await this.#storageHelper.write(
static async updateEntity(entityName: string, entity: StorageEntity<Object>): Promise<void> {
this.#storageHelper.write(
entityName,
Object.assign(
await this.#storageHelper.read(
@@ -51,15 +48,13 @@ export default class EntitiesController {
/**
* Delete the entity with the given ID.
*
* @param {string} entityName Name of the entity to delete.
* @param {string} entityId ID of the entity to delete.
*
* @return {Promise<void>}
* @param entityName Name of the entity to delete.
* @param entityId ID of the entity to delete.
*/
static async deleteEntity(entityName, entityId) {
static async deleteEntity(entityName: string, entityId: string): Promise<void> {
const entities = await this.#storageHelper.read(entityName, {});
delete entities[entityId];
await this.#storageHelper.write(entityName, entities);
this.#storageHelper.write(entityName, entities);
}
/**
@@ -67,17 +62,16 @@ export default class EntitiesController {
*
* @template EntityClass
*
* @param {string} entityName Name of the entity to subscribe to.
* @param {EntityClass} entityClass Class of the entity to subscribe to.
* @param {function(InstanceType<EntityClass>[]): any} callback Callback to call when the storage changes.
* @return {function(): void} Unsubscribe function.
* @param entityName Name of the entity to subscribe to.
* @param entityClass Class of the entity to subscribe to.
* @param callback Callback to call when the storage changes.
* @return Unsubscribe function.
*/
static subscribeToEntity(entityName, entityClass, callback) {
static subscribeToEntity<Type extends StorageEntity<any>>(entityName: string, entityClass: new (...any: any[]) => Type, callback: (entities: Type[]) => void): () => void {
/**
* Watch the changes made to the storage and call the callback when the entity changes.
* @param {Object<string, StorageChange>} changes Changes made to the storage.
*/
const storageChangesSubscriber = changes => {
const storageChangesSubscriber = (changes: Record<string, chrome.storage.StorageChange>) => {
if (!changes[entityName]) {
return;
}

View File

@@ -1,16 +1,28 @@
import {validateImportedEntity} from "$lib/extension/transporting/validators.js";
import {exportEntityToObject} from "$lib/extension/transporting/exporters.js";
import StorageEntity from "./base/StorageEntity.js";
import {exportEntityToObject} from "$lib/extension/transporting/exporters.ts";
import StorageEntity from "$lib/extension/base/StorageEntity.ts";
import {compressToEncodedURIComponent, decompressFromEncodedURIComponent} from "lz-string";
type EntityConstructor<T extends StorageEntity> =
(new (id: string, settings: Record<string, any>) => T)
& typeof StorageEntity;
export default class EntitiesTransporter<EntityType> {
readonly #targetEntityConstructor: new (...any: any[]) => EntityType;
export default class EntitiesTransporter<EntityType extends StorageEntity> {
readonly #targetEntityConstructor: EntityConstructor<EntityType>;
/**
* Name of the entity, exported directly from the constructor.
* @private
*/
get #entityName() {
// How the hell should I even do this?
return ((this.#targetEntityConstructor as any) as typeof StorageEntity)._entityName;
}
/**
* @param entityConstructor Class which should be used for import or export.
*/
constructor(entityConstructor: new (...any: any[]) => EntityType) {
if (!(entityConstructor.prototype instanceof StorageEntity)) {
throw new TypeError('Invalid class provided as the target for importing!');
}
constructor(entityConstructor: EntityConstructor<EntityType>) {
this.#targetEntityConstructor = entityConstructor;
}
@@ -23,7 +35,7 @@ export default class EntitiesTransporter<EntityType extends StorageEntity> {
validateImportedEntity(
importedObject,
this.#targetEntityConstructor._entityName
this.#entityName
);
return new this.#targetEntityConstructor(
@@ -43,9 +55,13 @@ export default class EntitiesTransporter<EntityType extends StorageEntity> {
throw new TypeError('Transporter should be connected to the same entity to export!');
}
if (!(entityObject instanceof StorageEntity)) {
throw new TypeError('Only storage entities could be exported!');
}
const exportableObject = exportEntityToObject(
entityObject,
this.#targetEntityConstructor._entityName
this.#entityName
);
return JSON.stringify(exportableObject, null, 2);

View File

@@ -1,56 +0,0 @@
import EntitiesController from "$lib/extension/EntitiesController.js";
class StorageEntity {
/**
* @type {string}
*/
#id;
/**
* @type {Object}
*/
#settings;
/**
* @param {string} id
* @param {Object} settings
*/
constructor(id, settings = {}) {
this.#id = id;
this.#settings = settings;
}
/**
* @return {string}
*/
get id() {
return this.#id;
}
/**
* @return {Object}
*/
get settings() {
return this.#settings;
}
static _entityName = "entity";
async save() {
await EntitiesController.updateEntity(this.constructor._entityName, this);
}
async delete() {
await EntitiesController.deleteEntity(this.constructor._entityName, this.id);
}
/**
* Static function to read all entities of this type from the storage. Must be implemented in the child class.
* @return {Promise<array>}
*/
static async readAll() {
throw new Error("Not implemented");
}
}
export default StorageEntity;

View File

@@ -0,0 +1,59 @@
import EntitiesController from "$lib/extension/EntitiesController.js";
export default abstract class StorageEntity<SettingsType extends Object = {}> {
/**
* @type {string}
*/
readonly #id: string;
/**
* @type {Object}
*/
readonly #settings: SettingsType;
protected constructor(id: string, settings: SettingsType) {
this.#id = id;
this.#settings = settings;
}
get id(): string {
return this.#id;
}
get settings(): SettingsType {
return this.#settings;
}
public static readonly _entityName: string = "entity";
async save() {
await EntitiesController.updateEntity(
(this.constructor as typeof StorageEntity)._entityName,
this
);
}
async delete() {
await EntitiesController.deleteEntity(
(this.constructor as typeof StorageEntity)._entityName,
this.id
);
}
public static async readAll<Type extends StorageEntity<any>>(this: new (...args: any[]) => Type): Promise<Type[]> {
return await EntitiesController.readAllEntities(
// Voodoo magic, once again.
((this as any) as typeof StorageEntity)._entityName,
this
)
}
public static subscribe<Type extends StorageEntity<any>>(this: new (...args: any[]) => Type, callback: (entities: Type[]) => void): () => void {
return EntitiesController.subscribeToEntity(
// And once more.
((this as any) as typeof StorageEntity)._entityName,
this,
callback
);
}
}

View File

@@ -1,63 +0,0 @@
import StorageEntity from "$lib/extension/base/StorageEntity.js";
import EntitiesController from "$lib/extension/EntitiesController.js";
/**
* @typedef {Object} MaintenanceProfileSettings
* @property {string} name
* @property {string[]} tags
*/
/**
* Class representing the maintenance profile entity.
*/
class MaintenanceProfile extends StorageEntity {
/**
* @param {string} id ID of the entity.
* @param {Partial<MaintenanceProfileSettings>} settings Maintenance profile settings object.
*/
constructor(id, settings) {
super(id, {
name: settings.name || '',
tags: settings.tags || []
});
}
/**
* @return {MaintenanceProfileSettings}
*/
get settings() {
return super.settings;
}
static _entityName = "profiles";
/**
* Read all maintenance profiles from the storage.
*
* @return {Promise<InstanceType<MaintenanceProfile>[]>}
*/
static async readAll() {
return await EntitiesController.readAllEntities(
this._entityName,
MaintenanceProfile
);
}
/**
* Subscribe to the changes and receive the new list of profiles when they change.
*
* @param {function(MaintenanceProfile[]): void} callback Callback to call when the profiles change. The new list of
* profiles is passed as an argument.
*
* @return {function(): void} Unsubscribe function.
*/
static subscribe(callback) {
return EntitiesController.subscribeToEntity(
this._entityName,
MaintenanceProfile,
callback
);
}
}
export default MaintenanceProfile;

View File

@@ -0,0 +1,25 @@
import StorageEntity from "$lib/extension/base/StorageEntity.ts";
import EntitiesController from "$lib/extension/EntitiesController.ts";
export interface MaintenanceProfileSettings {
name: string;
tags: string[];
}
/**
* Class representing the maintenance profile entity.
*/
export default class MaintenanceProfile extends StorageEntity<MaintenanceProfileSettings> {
/**
* @param id ID of the entity.
* @param settings Maintenance profile settings object.
*/
constructor(id: string, settings: Partial<MaintenanceProfileSettings>) {
super(id, {
name: settings.name || '',
tags: settings.tags || []
});
}
public static readonly _entityName = "profiles";
}

View File

@@ -1,5 +1,5 @@
import ConfigurationController from "$lib/extension/ConfigurationController.js";
import MaintenanceProfile from "$lib/extension/entities/MaintenanceProfile.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
import CacheableSettings from "$lib/extension/base/CacheableSettings.js";
export default class MaintenanceSettings extends CacheableSettings {

View File

@@ -1,26 +0,0 @@
/**
* @type {Map<string, ((entity: import('../base/StorageEntity.js').default) => Record<string, any>)>}
*/
const entitiesExporters = new Map([
['profiles', /** @param {import('../entities/MaintenanceProfile.js').default} entity */entity => {
return {
v: 1,
id: entity.id,
name: entity.settings.name,
tags: entity.settings.tags,
}
}]
])
/**
* @param entityInstance
* @param {string} entityName
* @returns {Record<string, *>}
*/
export function exportEntityToObject(entityInstance, entityName) {
if (!entitiesExporters.has(entityName)) {
throw new Error(`Missing exporter for entity: ${entityName}`);
}
return entitiesExporters.get(entityName).call(null, entityInstance);
}

View File

@@ -0,0 +1,24 @@
import StorageEntity from "$lib/extension/base/StorageEntity.ts";
type ExportersMap = {
[EntityName in keyof App.EntityNamesMap]: (entity: App.EntityNamesMap[EntityName]) => Record<string, any>
};
const entitiesExporters: ExportersMap = {
profiles: entity => {
return {
v: 1,
id: entity.id,
name: entity.settings.name,
tags: entity.settings.tags,
}
},
};
export function exportEntityToObject(entityInstance: StorageEntity<any>, entityName: string): Record<string, any> {
if (!(entityName in entitiesExporters) || !entitiesExporters.hasOwnProperty(entityName)) {
throw new Error(`Missing exporter for entity: ${entityName}`);
}
return entitiesExporters[entityName as keyof App.EntityNamesMap].call(null, entityInstance);
}

View File

@@ -1,6 +1,6 @@
/**
* Map of validators for each entity. Function should throw the error if validation failed.
* @type {Map<string, ((importedObject: Object) => void)>}
* @type {Map<keyof App.EntityNamesMap|string, ((importedObject: Object) => void)>}
*/
const entitiesValidators = new Map([
['profiles', importedObject => {

View File

@@ -4,7 +4,7 @@
import { activeProfileStore, maintenanceProfilesStore } from "$stores/maintenance-profiles-store.js";
import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
/** @type {import('$lib/extension/entities/MaintenanceProfile.js').default|undefined} */
/** @type {import('$entities/MaintenanceProfile.ts').default|undefined} */
let activeProfile;
$: activeProfile = $maintenanceProfilesStore.find(profile => profile.id === $activeProfileStore);

View File

@@ -4,7 +4,7 @@
import MenuRadioItem from "$components/ui/menu/MenuRadioItem.svelte";
import {activeProfileStore, maintenanceProfilesStore} from "$stores/maintenance-profiles-store.js";
/** @type {import('$lib/extension/entities/MaintenanceProfile.js').default[]} */
/** @type {import('$entities/MaintenanceProfile.ts').default[]} */
let profiles = [];
$: profiles = $maintenanceProfilesStore.sort((a, b) => a.settings.name.localeCompare(b.settings.name));

View File

@@ -7,7 +7,7 @@
import ProfileView from "$components/maintenance/ProfileView.svelte";
const profileId = $page.params.id;
/** @type {import('$lib/extension/entities/MaintenanceProfile.js').default|null} */
/** @type {import('$entities/MaintenanceProfile.ts').default|null} */
let profile = null;
let isActiveProfile = false;
@@ -57,6 +57,9 @@
<MenuItem icon="file-export" href="/features/maintenance/{profileId}/export">
Export Profile
</MenuItem>
<MenuItem icon="trash" href="/features/maintenance/{profileId}/delete">
Delete Profile
</MenuItem>
</Menu>
<style lang="scss">

View File

@@ -0,0 +1,41 @@
<script>
import { goto } from "$app/navigation";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { page } from "$app/stores";
import { maintenanceProfilesStore } from "$stores/maintenance-profiles-store.js";
const profileId = $page.params.id;
const targetProfile = $maintenanceProfilesStore.find(profile => profile.id===profileId);
if (!targetProfile) {
void goto('/features/maintenance');
}
async function deleteProfile() {
if (!targetProfile) {
console.warn('Attempting to delete the profile, but the profile is not loaded yet.');
return;
}
await targetProfile.delete();
await goto('/features/maintenance');
}
</script>
<Menu>
<MenuItem icon="arrow-left" href="/features/maintenance/{profileId}">Back</MenuItem>
<hr>
</Menu>
{#if targetProfile}
<p>
Do you want to remove profile "{targetProfile.settings.name}"? This action is irreversible.
</p>
<Menu>
<hr>
<MenuItem on:click={deleteProfile}>Yes</MenuItem>
<MenuItem href="/features/maintenance/{profileId}">No</MenuItem>
</Menu>
{:else}
<p>Loading...</p>
{/if}

View File

@@ -8,7 +8,7 @@
import {page} from "$app/stores";
import {goto} from "$app/navigation";
import {maintenanceProfilesStore} from "$stores/maintenance-profiles-store.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
/** @type {string} */
let profileId = $page.params.id;
@@ -46,16 +46,6 @@
await targetProfile.save();
await goto('/features/maintenance/' + targetProfile.id);
}
async function deleteProfile() {
if (!targetProfile) {
console.warn('Attempting to delete the profile, but the profile is not loaded yet.');
return;
}
await targetProfile.delete();
await goto('/features/maintenance');
}
</script>
<Menu>
@@ -75,7 +65,4 @@
<Menu>
<hr>
<MenuItem href="#" on:click={saveProfile}>Save Profile</MenuItem>
{#if profileId !== 'new'}
<MenuItem href="#" on:click={deleteProfile}>Delete Profile</MenuItem>
{/if}
</Menu>

View File

@@ -7,13 +7,9 @@
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter.ts";
import MaintenanceProfile from "$entities/MaintenanceProfile.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
const profileId = $page.params.id;
/**
* @type {import('$lib/extension/entities/MaintenanceProfile.js').default|undefined}
*/
const profile = $maintenanceProfilesStore.find(profile => profile.id === profileId);
const profilesTransporter = new EntitiesTransporter(MaintenanceProfile);

View File

@@ -2,7 +2,7 @@
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import MaintenanceProfile from "$entities/MaintenanceProfile.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
import FormControl from "$components/ui/forms/FormControl.svelte";
import ProfileView from "$components/maintenance/ProfileView.svelte";
import {maintenanceProfilesStore} from "$stores/maintenance-profiles-store.js";

View File

@@ -1,5 +1,5 @@
import {writable} from "svelte/store";
import MaintenanceProfile from "$lib/extension/entities/MaintenanceProfile.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.js";
/**

View File

@@ -177,6 +177,21 @@
object-fit: contain;
width: 100%;
height: 100%;
opacity: 1;
}
.spinner {
position: fixed;
opacity: 0;
left: 50vw;
top: 50vh;
transform: translate(-50%, -50%);
font-size: 64px;
text-shadow: 0 0 15px black;
}
img, video, .spinner {
transition: opacity .25s ease;
}
&.shown {
@@ -188,4 +203,14 @@
opacity: var(--opacity, 1);
transition: none;
}
&.loading {
img, video {
opacity: 0.25;
}
.spinner {
opacity: 1;
}
}
}

View File

@@ -1,6 +1,6 @@
@mixin insert-icon($icon_src) {
mask-image: url($icon_src);
-webkit-mask-image: url($icon_src);
@mixin insert-icon($url) {
mask-image: $url;
-webkit-mask-image: $url;
}
.icon {
@@ -11,33 +11,37 @@
}
.icon.icon-tag {
@include insert-icon('/img/tag.svg');
@include insert-icon(url('@fortawesome/fontawesome-free/svgs/solid/tag.svg'));
}
.icon.icon-paint-brush {
@include insert-icon('/img/paint-brush.svg');
@include insert-icon(url('@fortawesome/fontawesome-free/svgs/solid/paintbrush.svg'));
}
.icon.icon-arrow-left {
@include insert-icon('/img/arrow-left.svg');
@include insert-icon(url('@fortawesome/fontawesome-free/svgs/solid/arrow-left.svg'));
}
.icon.icon-info-circle {
@include insert-icon('/img/info-circle.svg');
@include insert-icon(url('@fortawesome/fontawesome-free/svgs/solid/circle-info.svg'));
}
.icon.icon-wrench {
@include insert-icon('/img/wrench.svg');
@include insert-icon(url('@fortawesome/fontawesome-free/svgs/solid/wrench.svg'));
}
.icon.icon-globe {
@include insert-icon('/img/globe.svg');
@include insert-icon(url('@fortawesome/fontawesome-free/svgs/solid/globe.svg'));
}
.icon.icon-plus {
@include insert-icon('/img/plus.svg');
@include insert-icon(url('@fortawesome/fontawesome-free/svgs/solid/plus.svg'));
}
.icon.icon-file-export {
@include insert-icon('/img/file-export.svg');
@include insert-icon(url('@fortawesome/fontawesome-free/svgs/solid/file-export.svg'));
}
.icon.icon-trash {
@include insert-icon(url('@fortawesome/fontawesome-free/svgs/solid/trash.svg'));
}

View File

@@ -1,6 +0,0 @@
<svg width="32" height="32" version="1.1" viewBox="0 -256 1472 1558" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,-1,-64,1099)">
<path d="m1536 640v-128q0-53-32.5-90.5t-84.5-37.5h-704l293-294q38-36 38-90t-38-90l-75-76q-37-37-90-37-52 0-91 37l-651 652q-37 37-37 90 0 52 37 91l651 650q38 38 91 38 52 0 90-38l75-74q38-38 38-91t-38-91l-293-293h704q52 0 84.5-37.5t32.5-90.5z"
fill="currentColor"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 453 B

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512">
<path d="M384 121.9c0-6.3-2.5-12.4-7-16.9L279.1 7c-4.5-4.5-10.6-7-17-7H256v128h128v-6.1zM192 336v-32c0-8.84 7.16-16 16-16h176V160H248c-13.2 0-24-10.8-24-24V0H24C10.7 0 0 10.7 0 24v464c0 13.3 10.7 24 24 24h336c13.3 0 24-10.7 24-24V352H208c-8.84 0-16-7.16-16-16zm379.05-28.02l-95.7-96.43c-10.06-10.14-27.36-3.01-27.36 11.27V288H384v64h63.99v65.18c0 14.28 17.29 21.41 27.36 11.27l95.7-96.42c6.6-6.66 6.6-17.4 0-24.05z"/>
</svg>

Before

Width:  |  Height:  |  Size: 492 B

View File

@@ -1,6 +0,0 @@
<svg width="32" height="32" version="1.1" viewBox="0 -256 1536 1536" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,-1,0,1152)">
<path d="m1193 993q11 7 25 22v-1q0-2-9.5-10t-11.5-12q-1 1-4 1zm-6-1q-1 1-2.5 3t-1.5 3q3-2 10-5-6-4-6-1zm-459 183q-16 2-26 5 1 0 6.5-1t10.5-2 9-2zm45 37q7 4 13.5 2.5t7.5-7.5q-5 3-21 5zm-8-6-3 2q-2 3-5.5 5t-4.5 2q2-1 21-3-6-4-8-6zm-102 84v2q1-2 3-5.5t3-5.5zm-105-40q0-2-1-2l-1 2zm375-1044v-1zm-165 1202q209 0 385.5-103t279.5-279.5 103-385.5-103-385.5-279.5-279.5-385.5-103-385.5 103-279.5 279.5-103 385.5 103 385.5 279.5 279.5 385.5 103zm472-1246 5 5q-7 10-29 12 1 12-14 26.5t-27 15.5q0 4-10.5 11t-17.5 8q-9 2-27-9-7-3-4-5-3 3-12 11t-16 11q-2 1-7.5 1t-8.5 2q-1 1-6 4.5t-7 4.5-6.5 3-7.5 1.5-7.5-2.5-8.5-6-4.5-15.5-2.5-14.5q-8 6-0.5 20t1.5 20q-7 7-21 0.5t-21-15.5q-1-1-9.5-5.5t-11.5-7.5q-4-6-9-17.5t-6-13.5q0 2-2.5 6.5t-2.5 6.5q-12-2-16 3 5-16 8-17l-4 2q-1-6 3-15t4-11q1-5-1.5-13t-2.5-11q0-2 5-11 4-19-2-32 0-1-3.5-7t-6.5-11l-2-5-2 1q-1 1-2 0-1-6-9-13t-10-11q-15-23-9-38 3-8 10-10 3-1 3 2 1-9-11-27 1-1 4-3-17 0-10-14 202 36 352 181h-3zm-560 185q16 3 30.5-16t22.5-23q41-20 59-11 0-9 14-28 3-4 6.5-11.5t5.5-10.5q5-7 19-16t19-16q6 3 9 9 13-35 24-34 5 0 8 8 0-1-0.5-3t-1.5-3q7 15 5 26l6 4q5 4 5 5-6 6-9-3-30-14-48 22-2 3-4.5 8t-5 12-1.5 11.5 6 4.5q11 0 12.5 1.5t-2.5 6-4 7.5q-1 4-1.5 12.5t-1.5 12.5l-5 6q-5 6-11.5 13.5t-7.5 9.5q-4-10-16.5-8.5t-18.5 9.5q1-2-0.5-6.5t-1.5-6.5q-14 0-17 1 1 6 3 21t4 22q1 5 5.5 13.5t8 15.5 4.5 14-4.5 10.5-18.5 2.5q-20-1-29-22-1-3-3-11.5t-5-12.5-9-7q-8-3-27-2t-26 5q-14 8-24 30.5t-11 41.5q0 10 3 27.5t3 27-6 26.5q3 2 10 10.5t11 11.5q2 2 5 2h5t4 2 3 6q-1 1-4 3-3 3-4 3 4-3 19-1t19 2q0 1 22 0 17-13 24 2 0 1-2.5 10.5t-0.5 14.5q5-29 32-10 3-4 16.5-6t18.5-5q3-2 7-5.5t6-5 6-0.5 9 7q11-17 13-25 11-43 20-48 8-2 12.5-2t5 10.5 0 15.5-1.5 13l-2 37q-16 3-20 12.5t1.5 20 16.5 19.5q1 1 16.5 8t21.5 12q24 19 17 39 9-2 11 9l-5 3q-4 3-8 5.5t-5 1.5q11 7 2 18 5 3 8 11.5t9 11.5q9-14 22-3 8 9 2 18 5 8 22 11.5t20 9.5q5-1 7 0t2 4.5v7.5t1 8.5 3 7.5q4 6 16 10.5t14 5.5l19 12q4 4 0 4 18-2 32 11 13 12-5 23 2 7-4 10.5t-16 5.5q3 1 12 0.5t12 1.5q15 11-7 17-20 5-47-13-3-2-13-12t-17-11q15 18 5 22 8-1 22.5 9t15.5 11q4 2 10.5 2.5t8.5 1.5q71 25 92-1 8 11 11 15t9.5 9 15.5 8q21 7 23 9l1 23q-12-1-18 8t-7 22l-6-8q0 6-3.5 7.5t-7.5 0.5-9.5-2-7.5 0q-9 2-19.5 15.5t-14.5 16.5q9 0 9 5-2 5-10 8 1 6-2 8t-9 0q-2 12-1 13-6 1-11 11t-8 10q-2 0-4.5-2t-5-5.5l-5-7t-3.5-5.5l-2-2q-12 6-24-10-9 1-17-2 15 6 2 13-11 5-21 2 12 5 10 14t-12 16q1 0 4-1t4-1q-1 5-9.5 9.5t-19.5 9-14 6.5q-7 5-36 10.5t-36 1.5q-5-3-6-6t1.5-8.5 3.5-8.5q6-23 5-27-1-3-8.5-8t-5.5-12q1-4 11.5-10t12.5-12q5-13-4-25-4-5-15-11t-14-10q-5-5-3.5-11.5t0.5-9.5q1 1 1 2.5t1 2.5q0-13 11-22 8-6-16-18-20-11-20-4 1 8-7.5 16t-10.5 12-3.5 19-9.5 21q-6 4-19 4t-18-5q0 10-49 30-17 8-58 4 7 1 0 17-8 16-21 12-8 25-4 35 2 5 9 14t9 15q1 3 15.5 6t16.5 8q1 4-2.5 6.5t-9.5 4.5q53-6 63 18 5 9 3 14 0-1 2-1t2-1q12 3 7 17 19 8 26 8 5-1 11-6t10-5q17-3 21.5 10t-9.5 23q7-4 7 6-1 13-7 19-3 2-6.5 2.5t-6.5 0-7 0.5q-1 0-8 2-1-1-2-1h-8q-4-2-4-5v-1q-1-3 4-6l5-1 3-2q-1 0-2.5-2.5t-2.5-2.5q0-3 3-5-2-1-14-7.5t-17-10.5q-1-1-4-2.5t-4-2.5q-2-1-4 2t-4 9-4 11.5-4.5 10-5.5 4.5q-12 0-18-17 3 10-13 17.5t-25 7.5q20 15-9 30l-1 1q-30-4-45-7-2-6 3-12-1-7 6-9 0-1 0.5-1t0.5-1q0 1-0.5 1t-0.5 1q3-1 10.5-1.5t9.5-1.5q3-1 4.5-2l7.5-5t5.5-6-2.5-5q-2-1-9-4t-12.5-5.5-6.5-3.5q-3-5 0-16t-2-15q-5 5-10 18.5t-8 17.5q8-9-30-6l-8 1q-4 0-15-2t-16-1q-7 0-29 6 7 17 5 25 5 0 7 2l-6 3q-3-1-25-9 2-3 8-9.5t9-11.5q-22 6-27-2 0-1-9 0-25 1-24-7 1-4 9-12 0-9-1-9-27 22-30 23-172-83-276-248 1-2 2.5-11t3.5-8.5 11 4.5q9-9 3-21 2 2 36-21 56-40 22-53v5.5t1 6.5q-9-1-19 5-3-6 0.5-20t11.5-14q-8 0-10.5-17t-2.5-38.5-1-25.5l2-1q-3-13 6-37.5t24-20.5q-4-18 5-21-1-4 0-8t4.5-8.5 6-7l13.5-13.5q28-11 41-29 4-6 10.5-24.5t15.5-25.5q-2-6 10-21.5t11-25.5q-1 0-2.5-0.5t-2.5-0.5q3-8 16.5-16t16.5-14q2-3 2.5-10.5t3-12 8.5-2.5q3 24-26 68-16 27-18 31-3 5-5.5 16.5t-4.5 15.5q27-9 26-13-5-10 26-52 2-3 10-10t11-12q3-4 9.5-14.5t10.5-15.5q-1 0-3-2l-3-3q4-2 9-5t8-4.5 7.5-5 7.5-7.5q16-18 20-33 1-4 0.5-15.5t1.5-16.5q2-6 6-11t11.5-10 11.5-7 14.5-6.5 11.5-5.5q2-1 18-11t25-14q10-4 16.5-4.5t16 2.5 15.5 4z"
fill="currentColor"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -1,3 +0,0 @@
<svg width="32" height="32" version="1.1" viewBox="0 0 496 496" xmlns="http://www.w3.org/2000/svg">
<path d="m248 0c-136.96 0-248 111.08-248 248 0 137 111.04 248 248 248s248-111 248-248c0-136.92-111.04-248-248-248zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12z"/>
</svg>

Before

Width:  |  Height:  |  Size: 514 B

View File

@@ -1,3 +0,0 @@
<svg width="32" height="32" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path d="M167.02 309.34c-40.12 2.58-76.53 17.86-97.19 72.3-2.35 6.21-8 9.98-14.59 9.98-11.11 0-45.46-27.67-55.25-34.35C0 439.62 37.93 512 128 512c75.86 0 128-43.77 128-120.19 0-3.11-.65-6.08-.97-9.13l-88.01-73.34zM457.89 0c-15.16 0-29.37 6.71-40.21 16.45C213.27 199.05 192 203.34 192 257.09c0 13.7 3.25 26.76 8.73 38.7l63.82 53.18c7.21 1.8 14.64 3.03 22.39 3.03 62.11 0 98.11-45.47 211.16-256.46 7.38-14.35 13.9-29.85 13.9-45.99C512 20.64 486 0 457.89 0z"/>
</svg>

Before

Width:  |  Height:  |  Size: 554 B

View File

@@ -1,6 +0,0 @@
<svg width="1408" height="1408" version="1.1" viewBox="0 -256 1408 1408" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,-1,0,1152)">
<path d="m1408 800v-192q0-40-28-68t-68-28h-416v-416q0-40-28-68t-68-28h-192q-40 0-68 28t-28 68v416h-416q-40 0-68 28t-28 68v192q0 40 28 68t68 28h416v416q0 40 28 68t68 28h192q40 0 68-28t28-68v-416h416q40 0 68-28t28-68z"
fill="currentColor"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 430 B

View File

@@ -1,6 +0,0 @@
<svg width="32" height="32" version="1.1" viewBox="0 -256 1515 1515" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,-1,0,1152)">
<path d="m448 1088q0 53-37.5 90.5t-90.5 37.5-90.5-37.5-37.5-90.5 37.5-90.5 90.5-37.5 90.5 37.5 37.5 90.5zm1067-576q0-53-37-90l-491-492q-39-37-91-37-53 0-90 37l-715 716q-38 37-64.5 101t-26.5 117v416q0 52 38 90t90 38h416q53 0 117-26.5t102-64.5l715-714q37-39 37-91z"
fill="currentColor"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 473 B

View File

@@ -1,5 +0,0 @@
<svg width="32" height="32" version="1.1" viewBox="0 -256 1641 1643" xmlns="http://www.w3.org/2000/svg">
<g transform="matrix(1,0,0,-1,-21,1152)">
<path d="m384 64q0 26-19 45t-45 19-45-19-19-45 19-45 45-19 45 19 19 45zm644 420-682-682q-37-37-90-37-52 0-91 37l-106 108q-38 36-38 90 0 53 38 91l681 681q39-98 114.5-173.5t173.5-114.5zm634 435q0-39-23-106-47-134-164.5-217.5t-258.5-83.5q-185 0-316.5 131.5t-131.5 316.5 131.5 316.5 316.5 131.5q58 0 121.5-16.5t107.5-46.5q16-11 16-28t-16-28l-293-169v-224l193-107q5 3 79 48.5t135.5 81 70.5 35.5q15 0 23.5-10t8.5-25z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 589 B

View File

@@ -2,7 +2,11 @@ import {sveltekit} from '@sveltejs/kit/vite';
import {defineConfig} from 'vite';
export default defineConfig({
build: {
// SVGs imported from the FA6 don't need to be inlined!
assetsInlineLimit: 0
},
plugins: [
sveltekit(),
]
],
});