diff --git a/src/app.d.ts b/src/app.d.ts
index 3735081..6040841 100644
--- a/src/app.d.ts
+++ b/src/app.d.ts
@@ -1,6 +1,7 @@
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
+import type TagGroup from "$entities/TagGroup.ts";
declare global {
namespace App {
@@ -24,6 +25,14 @@ declare global {
interface EntityNamesMap {
profiles: MaintenanceProfile;
+ groups: TagGroup;
+ }
+
+ interface ImageURIs {
+ full: string;
+ large: string;
+ medium: string;
+ small: string;
}
}
}
diff --git a/src/components/features/GroupView.svelte b/src/components/features/GroupView.svelte
new file mode 100644
index 0000000..e984d2a
--- /dev/null
+++ b/src/components/features/GroupView.svelte
@@ -0,0 +1,59 @@
+
+
+
+
Group Name:
+
{group.settings.name}
+
+{#if sortedTagsList.length}
+
+
Tags:
+
+
+ {#each sortedTagsList as tagName}
+ {tagName}
+ {/each}
+
+
+
+{/if}
+{#if sortedPrefixes.length}
+
+
Prefixes:
+
+
+ {#each sortedPrefixes as prefixName}
+ {prefixName}*
+ {/each}
+
+
+
+{/if}
+
+
diff --git a/src/components/tags/TagsColorContainer.svelte b/src/components/tags/TagsColorContainer.svelte
new file mode 100644
index 0000000..ba02c41
--- /dev/null
+++ b/src/components/tags/TagsColorContainer.svelte
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
diff --git a/src/components/ui/forms/TagCategorySelectField.svelte b/src/components/ui/forms/TagCategorySelectField.svelte
new file mode 100644
index 0000000..61040ee
--- /dev/null
+++ b/src/components/ui/forms/TagCategorySelectField.svelte
@@ -0,0 +1,80 @@
+
+
+
+
+
diff --git a/src/components/ui/menu/MenuCheckboxItem.svelte b/src/components/ui/menu/MenuCheckboxItem.svelte
index 75eb520..651bba6 100644
--- a/src/components/ui/menu/MenuCheckboxItem.svelte
+++ b/src/components/ui/menu/MenuCheckboxItem.svelte
@@ -23,7 +23,7 @@
-
+
diff --git a/src/lib/booru/tag-categories.js b/src/lib/booru/tag-categories.js
new file mode 100644
index 0000000..cbd33da
--- /dev/null
+++ b/src/lib/booru/tag-categories.js
@@ -0,0 +1,12 @@
+export const categories = [
+ 'rating',
+ 'spoiler',
+ 'origin',
+ 'oc',
+ 'error',
+ 'character',
+ 'content-official',
+ 'content-fanmade',
+ 'species',
+ 'body-type',
+];
diff --git a/src/lib/components/FullscreenViewer.js b/src/lib/components/FullscreenViewer.js
index 204f287..ee8b939 100644
--- a/src/lib/components/FullscreenViewer.js
+++ b/src/lib/components/FullscreenViewer.js
@@ -1,13 +1,14 @@
import {BaseComponent} from "$lib/components/base/BaseComponent.js";
+import MiscSettings from "$lib/extension/settings/MiscSettings.ts";
export class FullscreenViewer extends BaseComponent {
/** @type {HTMLVideoElement} */
#videoElement = document.createElement('video');
/** @type {HTMLImageElement} */
#imageElement = document.createElement('img');
-
#spinnerElement = document.createElement('i');
-
+ #sizeSelectorElement = document.createElement('select');
+ #closeButtonElement = document.createElement('i');
/** @type {number|null} */
#touchId = null;
/** @type {number|null} */
@@ -16,15 +17,33 @@ export class FullscreenViewer extends BaseComponent {
#startY = null;
/** @type {boolean|null} */
#isClosingSwipeStarted = null;
+ #isSizeFetched = false;
+ /** @type {App.ImageURIs|null} */
+ #currentURIs = null;
/**
* @protected
*/
build() {
this.container.classList.add('fullscreen-viewer');
- this.container.append(this.#spinnerElement);
+
+ this.container.append(
+ this.#spinnerElement,
+ this.#sizeSelectorElement,
+ this.#closeButtonElement,
+ );
this.#spinnerElement.classList.add('spinner', 'fa', 'fa-circle-notch', 'fa-spin');
+ this.#closeButtonElement.classList.add('close', 'fa', 'fa-xmark');
+ this.#sizeSelectorElement.classList.add('size-selector', 'input');
+
+ for (const [sizeKey, sizeName] of Object.entries(FullscreenViewer.#previewSizes)) {
+ const sizeOptionElement = document.createElement('option');
+ sizeOptionElement.value = sizeKey;
+ sizeOptionElement.innerText = sizeName;
+
+ this.#sizeSelectorElement.append(sizeOptionElement);
+ }
}
/**
@@ -40,6 +59,12 @@ export class FullscreenViewer extends BaseComponent {
this.#videoElement.addEventListener('loadeddata', this.#onLoaded.bind(this));
this.#imageElement.addEventListener('load', this.#onLoaded.bind(this));
+ this.#sizeSelectorElement.addEventListener('click', event => event.stopPropagation());
+
+ FullscreenViewer.#miscSettings
+ .resolveFullscreenViewerPreviewSize()
+ .then(this.#onSizeResolved.bind(this))
+ .then(this.#watchForSizeSelectionChanges.bind(this));
}
#onLoaded() {
@@ -163,7 +188,49 @@ export class FullscreenViewer extends BaseComponent {
}
}
+ /**
+ * @param {import("$lib/extension/settings/MiscSettings.js").FullscreenViewerSize} size
+ */
+ #onSizeResolved(size) {
+ this.#sizeSelectorElement.value = size;
+ this.#isSizeFetched = true;
+
+ this.emit('size-loaded');
+ }
+
+ #watchForSizeSelectionChanges() {
+ let lastActiveSize = this.#sizeSelectorElement.value;
+
+ FullscreenViewer.#miscSettings.subscribe(settings => {
+ const targetSize = settings.fullscreenViewerSize;
+
+ if (!targetSize || lastActiveSize === targetSize) {
+ return;
+ }
+
+ lastActiveSize = targetSize;
+ this.#sizeSelectorElement.value = targetSize;
+ });
+
+ this.#sizeSelectorElement.addEventListener('input', () => {
+ const targetSize = this.#sizeSelectorElement.value;
+
+ if (this.#currentURIs) {
+ void this.show(this.#currentURIs);
+ }
+
+ if (!targetSize || targetSize === lastActiveSize || !(targetSize in FullscreenViewer.#previewSizes)) {
+ return;
+ }
+
+ lastActiveSize = targetSize;
+ void FullscreenViewer.#miscSettings.setFullscreenViewerPreviewSize(targetSize);
+ });
+ }
+
#close() {
+ this.#currentURIs = null;
+
this.container.classList.remove(FullscreenViewer.#shownState);
document.body.style.overflow = null;
@@ -175,9 +242,44 @@ export class FullscreenViewer extends BaseComponent {
}
/**
- * @param {string} url
+ * @param {App.ImageURIs} imageUris
+ * @return {Promise}
*/
- show(url) {
+ async #resolveCurrentSelectedSizeUrl(imageUris) {
+ if (!this.#isSizeFetched) {
+ await new Promise(resolve => this.on('size-loaded', resolve))
+ }
+
+ let targetSize = this.#sizeSelectorElement.value;
+
+ if (!imageUris.hasOwnProperty(targetSize)) {
+ targetSize = FullscreenViewer.#fallbackSize;
+ }
+
+ if (!imageUris.hasOwnProperty(targetSize)) {
+ targetSize = Object.keys(imageUris)[0];
+ }
+
+ if (!targetSize) {
+ return null;
+ }
+
+ return imageUris[targetSize];
+ }
+
+ /**
+ * @param {App.ImageURIs} imageUris
+ */
+ async show(imageUris) {
+ this.#currentURIs = imageUris;
+
+ const url = await this.#resolveCurrentSelectedSizeUrl(imageUris);
+
+ if (!url) {
+ console.warn('Failed to resolve media for the viewer!');
+ return;
+ }
+
this.container.classList.add('loading');
requestAnimationFrame(() => {
@@ -214,9 +316,23 @@ export class FullscreenViewer extends BaseComponent {
return url.endsWith('.mp4') || url.endsWith('.webm');
}
+ static #miscSettings = new MiscSettings();
+
static #offsetProperty = '--offset';
static #opacityProperty = '--opacity';
static #shownState = 'shown';
static #swipeState = 'swiped';
static #minRequiredDistance = 50;
+
+ /**
+ * @type {Record}
+ */
+ static #previewSizes = {
+ full: 'Full',
+ large: 'Large',
+ medium: 'Medium',
+ small: 'Small'
+ }
+
+ static #fallbackSize = 'large';
}
diff --git a/src/lib/components/ImageShowFullscreenButton.js b/src/lib/components/ImageShowFullscreenButton.js
index f0cd8c7..2f8c6e3 100644
--- a/src/lib/components/ImageShowFullscreenButton.js
+++ b/src/lib/components/ImageShowFullscreenButton.js
@@ -33,7 +33,7 @@ export class ImageShowFullscreenButton extends BaseComponent {
})
.then(() => {
ImageShowFullscreenButton.#miscSettings.subscribe(settings => {
- this.#isFullscreenButtonEnabled = settings.fullscreenViewer;
+ this.#isFullscreenButtonEnabled = settings.fullscreenViewer ?? true;
this.#updateFullscreenButtonVisibility();
})
})
@@ -47,7 +47,7 @@ export class ImageShowFullscreenButton extends BaseComponent {
#onButtonClicked() {
ImageShowFullscreenButton
.#resolveViewer()
- .show(this.#mediaBoxTools.mediaBox.imageLinks.large);
+ .show(this.#mediaBoxTools.mediaBox.imageLinks);
}
/**
diff --git a/src/lib/components/MediaBoxWrapper.js b/src/lib/components/MediaBoxWrapper.js
index cc4c6a2..5fb1a3f 100644
--- a/src/lib/components/MediaBoxWrapper.js
+++ b/src/lib/components/MediaBoxWrapper.js
@@ -56,7 +56,7 @@ export class MediaBoxWrapper extends BaseComponent {
}
/**
- * @return {ImageURIs}
+ * @return {App.ImageURIs}
*/
get imageLinks() {
return JSON.parse(this.#thumbnailContainer.dataset.uris);
@@ -100,10 +100,3 @@ export function calculateMediaBoxesPositions(mediaBoxesList) {
}
})
}
-
-/**
- * @typedef {Object} ImageURIs
- * @property {string} full
- * @property {string} large
- * @property {string} small
- */
diff --git a/src/lib/components/TagDropdownWrapper.js b/src/lib/components/TagDropdownWrapper.js
index 78961a3..a532a15 100644
--- a/src/lib/components/TagDropdownWrapper.js
+++ b/src/lib/components/TagDropdownWrapper.js
@@ -2,10 +2,12 @@ import {BaseComponent} from "$lib/components/base/BaseComponent.js";
import MaintenanceProfile from "$entities/MaintenanceProfile.ts";
import MaintenanceSettings from "$lib/extension/settings/MaintenanceSettings.ts";
import {getComponent} from "$lib/components/base/ComponentUtils.js";
+import CustomCategoriesResolver from "$lib/extension/CustomCategoriesResolver";
const isTagEditorProcessedKey = Symbol();
+const categoriesResolver = new CustomCategoriesResolver();
-class TagDropdownWrapper extends BaseComponent {
+export class TagDropdownWrapper extends BaseComponent {
/**
* Container with dropdown elements to insert options into.
* @type {HTMLElement}
@@ -36,6 +38,11 @@ class TagDropdownWrapper extends BaseComponent {
*/
#isEntered = false;
+ /**
+ * @type {string|undefined|null}
+ */
+ #originalCategory = null;
+
build() {
this.#dropdownContainer = this.container.querySelector('.dropdown__content');
}
@@ -53,10 +60,45 @@ class TagDropdownWrapper extends BaseComponent {
});
}
- get #tagName() {
+ get tagName() {
return this.container.dataset.tagName;
}
+ /**
+ * @return {string|undefined}
+ */
+ get tagCategory() {
+ return this.container.dataset.tagCategory;
+ }
+
+ /**
+ * @param {string|undefined} targetCategory
+ */
+ set tagCategory(targetCategory) {
+ // Make sure original category is properly stored.
+ this.originalCategory;
+
+ this.container.dataset.tagCategory = targetCategory;
+
+ if (targetCategory) {
+ this.container.setAttribute('data-tag-category', targetCategory);
+ return;
+ }
+
+ this.container.removeAttribute('data-tag-category');
+ }
+
+ /**
+ * @return {string|undefined}
+ */
+ get originalCategory() {
+ if (this.#originalCategory === null) {
+ this.#originalCategory = this.tagCategory;
+ }
+
+ return this.#originalCategory;
+ }
+
#onDropdownEntered() {
this.#isEntered = true;
this.#updateButtons();
@@ -89,7 +131,7 @@ class TagDropdownWrapper extends BaseComponent {
const profileName = this.#activeProfile.settings.name;
let profileSpecificButtonText = `Add to profile "${profileName}"`;
- if (this.#activeProfile.settings.tags.includes(this.#tagName)) {
+ if (this.#activeProfile.settings.tags.includes(this.tagName)) {
profileSpecificButtonText = `Remove from profile "${profileName}"`;
}
@@ -108,7 +150,8 @@ class TagDropdownWrapper extends BaseComponent {
async #onAddToNewClicked() {
const profile = new MaintenanceProfile(crypto.randomUUID(), {
name: 'Temporary Profile (' + (new Date().toISOString()) + ')',
- tags: [this.#tagName]
+ tags: [this.tagName],
+ temporary: true,
});
await profile.save();
@@ -121,7 +164,7 @@ class TagDropdownWrapper extends BaseComponent {
}
const tagsList = new Set(this.#activeProfile.settings.tags);
- const targetTagName = this.#tagName;
+ const targetTagName = this.tagName;
if (tagsList.has(targetTagName)) {
tagsList.delete(targetTagName);
@@ -195,7 +238,10 @@ export function wrapTagDropdown(element) {
return;
}
- new TagDropdownWrapper(element).initialize();
+ const tagDropdown = new TagDropdownWrapper(element);
+ tagDropdown.initialize();
+
+ categoriesResolver.addElement(tagDropdown);
}
export function watchTagDropdownsInTagsEditor() {
diff --git a/src/lib/extension/CustomCategoriesResolver.ts b/src/lib/extension/CustomCategoriesResolver.ts
new file mode 100644
index 0000000..d8fc259
--- /dev/null
+++ b/src/lib/extension/CustomCategoriesResolver.ts
@@ -0,0 +1,110 @@
+import type {TagDropdownWrapper} from "$lib/components/TagDropdownWrapper";
+import TagGroup from "$entities/TagGroup.ts";
+import {escapeRegExp} from "$lib/utils";
+
+export default class CustomCategoriesResolver {
+ #tagCategories = new Map();
+ #compiledRegExps = new Map();
+ #tagDropdowns: TagDropdownWrapper[] = [];
+ #nextQueuedUpdate = -1;
+
+ constructor() {
+ TagGroup.subscribe(this.#onTagGroupsReceived.bind(this));
+ TagGroup.readAll().then(this.#onTagGroupsReceived.bind(this));
+ }
+
+ public addElement(tagDropdown: TagDropdownWrapper): void {
+ this.#tagDropdowns.push(tagDropdown);
+
+ if (!this.#tagCategories.size && !this.#compiledRegExps.size) {
+ return;
+ }
+
+ this.#queueUpdatingTags();
+ }
+
+ #queueUpdatingTags() {
+ clearTimeout(this.#nextQueuedUpdate);
+
+ this.#nextQueuedUpdate = setTimeout(
+ this.#updateUnprocessedTags.bind(this),
+ CustomCategoriesResolver.#unprocessedTagsTimeout
+ );
+ }
+
+ #updateUnprocessedTags() {
+ this.#tagDropdowns
+ .filter(CustomCategoriesResolver.#skipTagsWithOriginalCategory)
+ .filter(this.#applyCustomCategoryForExactMatches.bind(this))
+ .filter(this.#matchCustomCategoryByRegExp.bind(this))
+ .forEach(CustomCategoriesResolver.#resetToOriginalCategory);
+ }
+
+ /**
+ * Apply custom categories for the exact tag names.
+ * @param tagDropdown Element to try applying the category for.
+ * @return {boolean} Will return false when tag is processed and true when it is not found.
+ * @private
+ */
+ #applyCustomCategoryForExactMatches(tagDropdown: TagDropdownWrapper): boolean {
+ const tagName = tagDropdown.tagName!;
+
+ if (!this.#tagCategories.has(tagName)) {
+ return true;
+ }
+
+ tagDropdown.tagCategory = this.#tagCategories.get(tagName)!;
+ return false;
+ }
+
+ #matchCustomCategoryByRegExp(tagDropdown: TagDropdownWrapper) {
+ const tagName = tagDropdown.tagName!;
+
+ for (const targetRegularExpression of this.#compiledRegExps.keys()) {
+ if (!targetRegularExpression.test(tagName)) {
+ continue;
+ }
+
+ tagDropdown.tagCategory = this.#compiledRegExps.get(targetRegularExpression)!;
+ return false;
+ }
+
+ return true;
+ }
+
+ #onTagGroupsReceived(tagGroups: TagGroup[]) {
+ this.#tagCategories.clear();
+ this.#compiledRegExps.clear();
+
+ if (!tagGroups.length) {
+ return;
+ }
+
+ for (const tagGroup of tagGroups) {
+ const categoryName = tagGroup.settings.category;
+
+ for (const tagName of tagGroup.settings.tags) {
+ this.#tagCategories.set(tagName, categoryName);
+ }
+
+ for (const tagPrefix of tagGroup.settings.prefixes) {
+ this.#compiledRegExps.set(
+ new RegExp(`^${escapeRegExp(tagPrefix)}`),
+ categoryName
+ );
+ }
+ }
+
+ this.#queueUpdatingTags();
+ }
+
+ static #skipTagsWithOriginalCategory(tagDropdown: TagDropdownWrapper): boolean {
+ return !tagDropdown.originalCategory;
+ }
+
+ static #resetToOriginalCategory(tagDropdown: TagDropdownWrapper): void {
+ tagDropdown.tagCategory = tagDropdown.originalCategory;
+ }
+
+ static #unprocessedTagsTimeout = 0;
+}
diff --git a/src/lib/extension/entities/MaintenanceProfile.ts b/src/lib/extension/entities/MaintenanceProfile.ts
index ac14352..f4b68c9 100644
--- a/src/lib/extension/entities/MaintenanceProfile.ts
+++ b/src/lib/extension/entities/MaintenanceProfile.ts
@@ -4,6 +4,7 @@ import EntitiesController from "$lib/extension/EntitiesController.ts";
export interface MaintenanceProfileSettings {
name: string;
tags: string[];
+ temporary: boolean;
}
/**
@@ -17,9 +18,18 @@ export default class MaintenanceProfile extends StorageEntity) {
super(id, {
name: settings.name || '',
- tags: settings.tags || []
+ tags: settings.tags || [],
+ temporary: settings.temporary ?? false
});
}
+ async save(): Promise {
+ if (this.settings.temporary && !this.settings.tags?.length) {
+ return this.delete();
+ }
+
+ return super.save();
+ }
+
public static readonly _entityName = "profiles";
}
diff --git a/src/lib/extension/entities/TagGroup.ts b/src/lib/extension/entities/TagGroup.ts
new file mode 100644
index 0000000..95dc44a
--- /dev/null
+++ b/src/lib/extension/entities/TagGroup.ts
@@ -0,0 +1,21 @@
+import StorageEntity from "$lib/extension/base/StorageEntity.ts";
+
+export interface TagGroupSettings {
+ name: string;
+ tags: string[];
+ prefixes: string[];
+ category: string;
+}
+
+export default class TagGroup extends StorageEntity {
+ constructor(id: string, settings: Partial) {
+ super(id, {
+ name: settings.name || '',
+ tags: settings.tags || [],
+ prefixes: settings.prefixes || [],
+ category: settings.category || ''
+ });
+ }
+
+ static _entityName = 'groups';
+}
diff --git a/src/lib/extension/settings/MiscSettings.ts b/src/lib/extension/settings/MiscSettings.ts
index 94ea7d2..1e0e76d 100644
--- a/src/lib/extension/settings/MiscSettings.ts
+++ b/src/lib/extension/settings/MiscSettings.ts
@@ -1,7 +1,10 @@
import CacheableSettings from "$lib/extension/base/CacheableSettings.ts";
+export type FullscreenViewerSize = 'small' | 'medium' | 'large' | 'full';
+
interface MiscSettingsFields {
fullscreenViewer: boolean;
+ fullscreenViewerSize: FullscreenViewerSize;
}
export default class MiscSettings extends CacheableSettings {
@@ -13,7 +16,15 @@ export default class MiscSettings extends CacheableSettings
return this._resolveSetting("fullscreenViewer", true);
}
+ async resolveFullscreenViewerPreviewSize() {
+ return this._resolveSetting('fullscreenViewerSize', 'large');
+ }
+
async setFullscreenViewerEnabled(isEnabled: boolean) {
return this._writeSetting("fullscreenViewer", isEnabled);
}
+
+ async setFullscreenViewerPreviewSize(size: FullscreenViewerSize | string) {
+ return this._writeSetting('fullscreenViewerSize', size as FullscreenViewerSize);
+ }
}
diff --git a/src/lib/extension/transporting/exporters.ts b/src/lib/extension/transporting/exporters.ts
index c2386c4..468df8e 100644
--- a/src/lib/extension/transporting/exporters.ts
+++ b/src/lib/extension/transporting/exporters.ts
@@ -13,6 +13,15 @@ const entitiesExporters: ExportersMap = {
tags: entity.settings.tags,
}
},
+ groups: entity => {
+ return {
+ v: 1,
+ id: entity.id,
+ name: entity.settings.name,
+ tags: entity.settings.tags,
+ prefixes: entity.settings.prefixes,
+ }
+ }
};
export function exportEntityToObject(entityInstance: StorageEntity, entityName: string): Record {
diff --git a/src/lib/utils.js b/src/lib/utils.js
index db17ea5..c7a6531 100644
--- a/src/lib/utils.js
+++ b/src/lib/utils.js
@@ -21,3 +21,23 @@ export function findDeepObject(targetObject, path) {
return result;
}
+
+/**
+ * Matches all the characters needing replacement.
+ *
+ * Gathered from right here: https://stackoverflow.com/a/3561711/16048617. Because I don't want to introduce some
+ * library for that.
+ *
+ * @type {RegExp}
+ */
+const unsafeRegExpCharacters = /[/\-\\^$*+?.()|[\]{}]/g;
+
+/**
+ * Escape all the RegExp syntax-related characters in the following value.
+ * @param {string} value Original value.
+ * @return {string} Resulting value with all needed characters escaped.
+ */
+export function escapeRegExp(value) {
+ unsafeRegExpCharacters.lastIndex = 0;
+ return value.replace(unsafeRegExpCharacters, "\\$&");
+}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 7b1fa3a..49cdecb 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -22,6 +22,7 @@
{/if}
+
diff --git a/src/routes/features/groups/+page.svelte b/src/routes/features/groups/+page.svelte
new file mode 100644
index 0000000..97fc1b7
--- /dev/null
+++ b/src/routes/features/groups/+page.svelte
@@ -0,0 +1,23 @@
+
+
+
diff --git a/src/routes/features/groups/[id]/+page.svelte b/src/routes/features/groups/[id]/+page.svelte
new file mode 100644
index 0000000..fffcfdb
--- /dev/null
+++ b/src/routes/features/groups/[id]/+page.svelte
@@ -0,0 +1,39 @@
+
+
+
+{#if group}
+
+{/if}
+
diff --git a/src/routes/features/groups/[id]/delete/+page.svelte b/src/routes/features/groups/[id]/delete/+page.svelte
new file mode 100644
index 0000000..50f1b1b
--- /dev/null
+++ b/src/routes/features/groups/[id]/delete/+page.svelte
@@ -0,0 +1,41 @@
+
+
+
+{#if targetGroup}
+
+ Do you want to remove group "{targetGroup.settings.name}"? This action is irreversible.
+
+
+{:else}
+ Loading...
+{/if}
diff --git a/src/routes/features/groups/[id]/edit/+page.svelte b/src/routes/features/groups/[id]/edit/+page.svelte
new file mode 100644
index 0000000..746c79d
--- /dev/null
+++ b/src/routes/features/groups/[id]/edit/+page.svelte
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/features/groups/[id]/export/+page.svelte b/src/routes/features/groups/[id]/export/+page.svelte
new file mode 100644
index 0000000..1a2b891
--- /dev/null
+++ b/src/routes/features/groups/[id]/export/+page.svelte
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/features/groups/import/+page.svelte b/src/routes/features/groups/import/+page.svelte
new file mode 100644
index 0000000..01583bc
--- /dev/null
+++ b/src/routes/features/groups/import/+page.svelte
@@ -0,0 +1,134 @@
+
+
+
+{#if errorMessage}
+ Failed to import: {errorMessage}
+
+{/if}
+{#if !candidateGroup}
+
+
+
+
+
+
+{:else}
+ {#if existingGroup}
+
+ This group will replace the existing "{existingGroup.settings.name}" group, since it have the same ID.
+
+ {/if}
+
+
+{/if}
+
+
diff --git a/src/routes/features/maintenance/[id]/+page.svelte b/src/routes/features/maintenance/[id]/+page.svelte
index aa6fa8d..294d229 100644
--- a/src/routes/features/maintenance/[id]/+page.svelte
+++ b/src/routes/features/maintenance/[id]/+page.svelte
@@ -5,18 +5,18 @@
import {goto} from "$app/navigation";
import {activeProfileStore, maintenanceProfilesStore} from "$stores/maintenance-profiles-store.js";
import ProfileView from "$components/maintenance/ProfileView.svelte";
+ import MenuCheckboxItem from "$components/ui/menu/MenuCheckboxItem.svelte";
const profileId = $page.params.id;
/** @type {import('$entities/MaintenanceProfile.ts').default|null} */
let profile = null;
- let isActiveProfile = false;
- if (profileId === 'new') {
+ if (profileId==='new') {
goto('/features/maintenance/new/edit');
}
$: {
- const resolvedProfile = $maintenanceProfilesStore.find(profile => profile.id === profileId);
+ const resolvedProfile = $maintenanceProfilesStore.find(profile => profile.id===profileId);
if (resolvedProfile) {
profile = resolvedProfile;
@@ -26,14 +26,16 @@
}
}
- $: isActiveProfile = $activeProfileStore === profileId;
+ let isActiveProfile = $activeProfileStore===profileId;
- function activateProfile() {
- if (isActiveProfile) {
- return;
+ $: {
+ if (isActiveProfile && $activeProfileStore!==profileId) {
+ $activeProfileStore = profileId;
}
- $activeProfileStore = profileId;
+ if (!isActiveProfile && $activeProfileStore===profileId) {
+ $activeProfileStore = null;
+ }
}
@@ -47,13 +49,9 @@