1
0
mirror of https://github.com/koloml/furbooru-tagging-assistant.git synced 2025-12-24 07:12:57 +00:00

Merge remote-tracking branch 'origin/release/0.4' into feature/auto-remove-temporary-profiles

# Conflicts:
#	src/lib/components/TagDropdownWrapper.js
This commit is contained in:
2025-01-03 19:35:58 +04:00
23 changed files with 979 additions and 21 deletions

View File

@@ -22,6 +22,7 @@
<hr>
{/if}
<MenuItem href="/features/maintenance">Tagging Profiles</MenuItem>
<MenuItem href="/features/groups">Tag Groups</MenuItem>
<hr>
<MenuItem href="/preferences">Preferences</MenuItem>
<MenuItem href="/about">About</MenuItem>

View File

@@ -0,0 +1,23 @@
<script>
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroupsStore } from "$stores/tag-groups-store.js";
/** @type {import('$entities/TagGroup.ts').default[]} */
let groups = [];
$: groups = $tagGroupsStore.sort((a, b) => a.settings.name.localeCompare(b.settings.name));
</script>
<Menu>
<MenuItem href="/" icon="arrow-left">Back</MenuItem>
<MenuItem href="/features/groups/new/edit" icon="plus">Create New</MenuItem>
{#if groups.length}
<hr>
{#each groups as group}
<MenuItem href="/features/groups/{group.id}">{group.settings.name}</MenuItem>
{/each}
{/if}
<hr>
<MenuItem href="/features/groups/import">Import Group</MenuItem>
</Menu>

View File

@@ -0,0 +1,39 @@
<script>
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import GroupView from "$components/features/GroupView.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import { tagGroupsStore } from "$stores/tag-groups-store.js";
const groupId = $page.params.id;
/** @type {import('$entities/TagGroup.ts').default|null} */
let group = null;
if (groupId==='new') {
goto('/features/groups/new/edit');
}
$: {
group = $tagGroupsStore.find(group => group.id===groupId) || null;
if (!group) {
console.warn(`Group ${ groupId } not found.`);
goto('/features/groups');
}
}
</script>
<Menu>
<MenuItem href="/features/groups" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
{#if group}
<GroupView {group}/>
{/if}
<Menu>
<hr>
<MenuItem href="/features/groups/{groupId}/edit" icon="wrench">Edit Group</MenuItem>
<MenuItem href="/features/groups/{groupId}/export" icon="file-export">Export Group</MenuItem>
<MenuItem href="/features/groups/{groupId}/delete" icon="trash">Delete Group</MenuItem>
</Menu>

View File

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

View File

@@ -0,0 +1,82 @@
<script>
import {goto} from "$app/navigation";
import {page} from "$app/stores";
import TagsColorContainer from "$components/tags/TagsColorContainer.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import TagCategorySelectField from "$components/ui/forms/TagCategorySelectField.svelte";
import TextField from "$components/ui/forms/TextField.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagsEditor from "$components/web-components/TagsEditor.svelte";
import TagGroup from "$entities/TagGroup.ts";
import {tagGroupsStore} from "$stores/tag-groups-store.js";
const groupId = $page.params.id;
/** @type {TagGroup|null} */
let targetGroup = null;
let groupName = '';
/** @type {string[]} */
let tagsList = [];
/** @type {string[]} */
let prefixesList = [];
let tagCategory = '';
if (groupId==='new') {
targetGroup = new TagGroup(crypto.randomUUID(), {});
} else {
targetGroup = $tagGroupsStore.find(group => group.id===groupId) || null;
if (targetGroup) {
groupName = targetGroup.settings.name;
tagsList = [...targetGroup.settings.tags].sort((a, b) => a.localeCompare(b));
prefixesList = [...targetGroup.settings.prefixes].sort((a, b) => a.localeCompare(b));
tagCategory = targetGroup.settings.category;
} else {
goto('/features/groups');
}
}
async function saveGroup() {
if (!targetGroup) {
console.warn('Attempting to save group, but group is not loaded yet.');
return;
}
targetGroup.settings.name = groupName;
targetGroup.settings.tags = [...tagsList];
targetGroup.settings.prefixes = [...prefixesList];
targetGroup.settings.category = tagCategory;
await targetGroup.save();
await goto(`/features/groups/${targetGroup.id}`);
}
</script>
<Menu>
<MenuItem href="/features/groups/${groupId}" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
<FormContainer>
<FormControl label="Group Name">
<TextField bind:value={groupName} placeholder="Group Name"></TextField>
</FormControl>
<FormControl label="Group Color">
<TagCategorySelectField bind:value={tagCategory}/>
</FormControl>
<TagsColorContainer targetCategory="{tagCategory}">
<FormControl label="Tags">
<TagsEditor bind:tags={tagsList}/>
</FormControl>
</TagsColorContainer>
<TagsColorContainer targetCategory="{tagCategory}">
<FormControl label="Tag Prefixes">
<TagsEditor bind:tags={prefixesList}/>
</FormControl>
</TagsColorContainer>
</FormContainer>
<Menu>
<hr>
<MenuItem on:click={saveGroup}>Save Group</MenuItem>
</Menu>

View File

@@ -0,0 +1,50 @@
<script>
import { goto } from "$app/navigation";
import { page } from "$app/stores";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagGroup from "$entities/TagGroup.ts";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter.ts";
import { tagGroupsStore } from "$stores/tag-groups-store.js";
const groupId = $page.params.id;
const groupTransporter = new EntitiesTransporter(TagGroup);
const group = $tagGroupsStore.find(group => group.id===groupId);
/** @type {string} */
let rawExportedGroup;
/** @type {string} */
let encodedExportedGroup;
if (!group) {
goto('/features/groups');
} else {
rawExportedGroup = groupTransporter.exportToJSON(group);
encodedExportedGroup = groupTransporter.exportToCompressedJSON(group);
}
let isEncodedGroupShown = true;
</script>
<Menu>
<MenuItem href="/features/groups/{groupId}" icon="arrow-left">Back</MenuItem>
<hr>
</Menu>
<FormContainer>
<FormControl label="Export string">
<textarea readonly rows="6">{isEncodedGroupShown ? encodedExportedGroup : rawExportedGroup}</textarea>
</FormControl>
</FormContainer>
<Menu>
<hr>
<MenuItem on:click={() => isEncodedGroupShown = !isEncodedGroupShown}>
Switch Format:
{#if isEncodedGroupShown}
Base64-Encoded
{:else}
Raw JSON
{/if}
</MenuItem>
</Menu>

View File

@@ -0,0 +1,134 @@
<script>
import { goto } from "$app/navigation";
import GroupView from "$components/features/GroupView.svelte";
import FormContainer from "$components/ui/forms/FormContainer.svelte";
import FormControl from "$components/ui/forms/FormControl.svelte";
import Menu from "$components/ui/menu/Menu.svelte";
import MenuItem from "$components/ui/menu/MenuItem.svelte";
import TagGroup from "$entities/TagGroup.ts";
import EntitiesTransporter from "$lib/extension/EntitiesTransporter.ts";
import { tagGroupsStore } from "$stores/tag-groups-store.js";
const groupTransporter = new EntitiesTransporter(TagGroup);
/** @type {string} */
let importedString = '';
/** @type {string} */
let errorMessage = '';
/** @type {TagGroup|null} */
let candidateGroup = null;
/** @type {TagGroup|null} */
let existingGroup = null;
function tryImportingGroup() {
candidateGroup = null;
existingGroup = null;
errorMessage = '';
importedString = importedString.trim();
if (!importedString) {
errorMessage = 'Nothing to import.';
return;
}
try {
if (importedString.trim().startsWith('{')) {
candidateGroup = groupTransporter.importFromJSON(importedString);
}
candidateGroup = groupTransporter.importFromCompressedJSON(importedString);
} catch (error) {
errorMessage = error instanceof Error
? error.message
:'Unknown error';
}
if (candidateGroup) {
existingGroup = $tagGroupsStore.find(group => group.id===candidateGroup?.id) ?? null;
}
}
function saveGroup() {
if (!candidateGroup) {
return;
}
candidateGroup.save().then(() => {
goto(`/features/groups`);
});
}
function cloneAndSaveGroup() {
if (!candidateGroup) {
return;
}
const clonedProfile = new TagGroup(crypto.randomUUID(), candidateGroup.settings);
clonedProfile.settings.name += ` (Clone ${ new Date().toISOString() })`;
clonedProfile.save().then(() => {
goto(`/features/groups`);
});
}
</script>
<Menu>
<MenuItem icon="arrow-left" href="/features/groups">Back</MenuItem>
<hr>
</Menu>
{#if errorMessage}
<p class="error">Failed to import: {errorMessage}</p>
<Menu>
<hr>
</Menu>
{/if}
{#if !candidateGroup}
<FormContainer>
<FormControl label="Import string">
<textarea bind:value={importedString} rows="6"></textarea>
</FormControl>
</FormContainer>
<Menu>
<hr>
<MenuItem on:click={tryImportingGroup}>Import</MenuItem>
</Menu>
{:else}
{#if existingGroup}
<p class="warning">
This group will replace the existing "{existingGroup.settings.name}" group, since it have the same ID.
</p>
{/if}
<GroupView group="{candidateGroup}"></GroupView>
<Menu>
<hr>
{#if existingGroup}
<MenuItem on:click={saveGroup}>Replace Existing Group</MenuItem>
<MenuItem on:click={cloneAndSaveGroup}>Save as New Group</MenuItem>
{:else}
<MenuItem on:click={saveGroup}>Import New Group</MenuItem>
{/if}
<MenuItem on:click={() => candidateGroup = null}>Cancel</MenuItem>
</Menu>
{/if}
<style lang="scss">
@use '../../../../styles/colors';
.error, .warning {
padding: 5px 24px;
margin: {
left: -24px;
right: -24px;
}
}
.error {
background: colors.$error-background;
}
.warning {
background: colors.$warning-background;
margin-bottom: .5em;
}
</style>