Skip to content

Commit b992850

Browse files
committed
v0.1.1
* Added automatic counting and adding and removing of individual reoccuring tags * The most frequent tags are shown on the side, however it is possible to filter for tags (it can be filtered for multiple tags at once by separating them with `|`, if the first character is a `-`, the tag will be excluded from the results, if the first character (after the optional `-`) is `^` or the last character is `$` the current search term will be parsed as regex instead of a simple string search) * Commonly in the context of Stable Diffusion for example tags are comma separated, so this is also the default string which is used for splitting the tags
1 parent 549c209 commit b992850

File tree

7 files changed

+185
-15
lines changed

7 files changed

+185
-15
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
## 0.1.1
2+
3+
* Added automatic counting and adding and removing of individual reoccuring tags
4+
* The most frequent tags are shown on the side, however it is possible to filter for tags (it can be filtered for multiple tags at once by separating them with `|`, if the first character is a `-`, the tag will be excluded from the results, if the first character (after the optional `-`) is `^` or the last character is `$` the current search term will be parsed as regex instead of a simple string search)
5+
* Commonly in the context of Stable Diffusion for example tags are comma separated, so this is also the default string which is used for splitting the tags

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "image-set-tag-editor",
3-
"version": "0.1.0",
3+
"version": "0.1.1",
44
"private": true,
55
"scripts": {
66
"dev": "vite dev",

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
},
99
"package": {
1010
"productName": "image-set-tag-editor",
11-
"version": "0.1.0"
11+
"version": "0.1.1"
1212
},
1313
"tauri": {
1414
"allowlist": {

src/lib/components/TagHelper.svelte

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<script lang="ts">
2+
import { single_tag_delimiter, active_image, tagCounts, sortedTags } from '$lib/stores';
3+
4+
let custom_css_classes = '';
5+
6+
let tag_filter_string = '';
7+
$: tag_white_list = tag_filter_string
8+
.split('|')
9+
.filter((tag) => tag != '' && !tag.startsWith('-'))
10+
.map((tag) => {
11+
if (tag.startsWith('^') || tag.endsWith('$')) {
12+
return new RegExp(tag);
13+
}
14+
return tag;
15+
});
16+
$: tag_black_list = tag_filter_string
17+
.split('|')
18+
.filter((tag) => tag != '' && tag.startsWith('-'))
19+
.map((tag) => {
20+
tag = tag.slice(1);
21+
if (tag.startsWith('^') || tag.endsWith('$')) {
22+
return new RegExp(tag);
23+
}
24+
return tag;
25+
});
26+
27+
$: fitting_tags = $sortedTags
28+
.filter((tag) => {
29+
// Or when the filter string is empty
30+
if (tag_filter_string == '') return true;
31+
// Only return if any keyword in the white list is in the tag
32+
return (
33+
tag_white_list.some((keyword) => {
34+
if (keyword instanceof RegExp) {
35+
return keyword.test(tag);
36+
}
37+
return tag.includes(keyword);
38+
}) &&
39+
!tag_black_list.some((keyword) => {
40+
if (keyword instanceof RegExp) {
41+
return keyword.test(tag);
42+
}
43+
return tag.includes(keyword);
44+
})
45+
);
46+
})
47+
.slice(0, 50)
48+
.sort((a, b) => a.localeCompare(b));
49+
</script>
50+
51+
<hr class="my-2" />
52+
<div>
53+
<label class="label">
54+
<span>Single Tag Delimiter</span>
55+
<input
56+
class="input variant-form-material"
57+
type="search"
58+
placeholder="Single Tag Delimiter"
59+
title="Single Tag Delimiter"
60+
on:keydown={(event) => {
61+
event.stopPropagation();
62+
}}
63+
bind:value={$single_tag_delimiter}
64+
/>
65+
</label>
66+
</div>
67+
<div>
68+
<label class="label">
69+
<span>Filter tags</span>
70+
<input
71+
class="input variant-form-material"
72+
type="search"
73+
placeholder="Filter tags"
74+
title="Filter tags"
75+
on:keydown={(event) => {
76+
event.stopPropagation();
77+
}}
78+
bind:value={tag_filter_string}
79+
/>
80+
</label>
81+
</div>
82+
<div class="overflow-y-scroll pl-1">
83+
{#each fitting_tags as tag}
84+
<label class="flex items-center space-x-2">
85+
<input
86+
class="checkbox"
87+
type="checkbox"
88+
checked={($active_image?.caption.split($single_tag_delimiter) || [])
89+
.map((tag) => tag.trim() + $single_tag_delimiter)
90+
.includes(tag)}
91+
on:click={(event) => {
92+
if ($active_image) {
93+
if (
94+
($active_image?.caption.split($single_tag_delimiter) || [])
95+
.map((tag) => tag.trim() + $single_tag_delimiter)
96+
.includes(tag)
97+
) {
98+
$active_image.caption = $active_image.caption.replace(new RegExp(`${tag} ?`), '');
99+
} else {
100+
console.log('Adding tag ' + tag);
101+
$active_image.caption = tag + ' ' + $active_image.caption;
102+
$active_image = $active_image;
103+
}
104+
}
105+
}}
106+
/>
107+
<div class="tag-display" data-tag={tag}>
108+
{tag}
109+
<div class="inline text-gray opacity-60">{$tagCounts[tag]}</div>
110+
</div>
111+
</label>
112+
{/each}
113+
</div>
114+
<hr class="my-2" />
115+
116+
<svelte:head>
117+
<!-- Dynamically insert a custom css style tag -->
118+
<svelte:element this={'style'} type="text/css">{custom_css_classes}</svelte:element>
119+
</svelte:head>

src/lib/stores.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { writable, derived, get } from 'svelte/store';
33
export const working_folder = writable('');
44
export const file_server_port = writable(0);
55
export const available_files = writable<string[]>([]);
6+
export const single_tag_delimiter = writable(',');
67

78
const VALID_IMAGE_FORMATS = ['.jpg', '.png', '.gif', '.bmp', '.webp', '.jpeg', '.avif'];
89

@@ -11,6 +12,37 @@ export const available_images = writable<
1112
{ image: string; caption: string; caption_file: string; viewed: boolean }[]
1213
>([]);
1314
export const is_loading_image_data = writable(false);
15+
export const active_image = writable<
16+
{ image: string; caption: string; caption_file: string; viewed: boolean } | undefined
17+
>(undefined);
18+
19+
export const tagCounts = derived(
20+
[available_images, single_tag_delimiter],
21+
([$available_images, $single_tag_delimiter]) => {
22+
console.log('counting tags');
23+
const data: Record<string, number> = {};
24+
$available_images.forEach((image) => {
25+
const tags = (image.caption.split($single_tag_delimiter) || []).map(
26+
(tag) => tag.trim() + get(single_tag_delimiter) // Ad the delimiter back for better searching, inserting and removing
27+
);
28+
29+
tags.forEach((tag) => {
30+
if (data[tag]) {
31+
data[tag] += 1;
32+
} else {
33+
data[tag] = 1;
34+
}
35+
});
36+
});
37+
return data;
38+
}
39+
);
40+
41+
export const sortedTags = derived(tagCounts, ($tagCounts) => {
42+
console.log('sorting tags');
43+
return Object.keys($tagCounts).sort((a, b) => $tagCounts[b] - $tagCounts[a]);
44+
});
45+
export const current_caption = writable('');
1446

1547
const update_available_files = async () => {
1648
const image_map = new Map<

src/routes/+layout.svelte

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import { toastStore } from '@skeletonlabs/skeleton';
1919
import { onMount } from 'svelte';
2020
import { get } from 'svelte/store';
21+
import TagHelper from '$lib/components/TagHelper.svelte';
2122
2223
async function select_new_folder() {
2324
try {
@@ -122,6 +123,10 @@
122123

123124
<div class="flex-1" />
124125

126+
<TagHelper />
127+
128+
<div class="flex-1" />
129+
125130
<label class="label text-center">
126131
<span>Apply all caption changes.</span>
127132
<button

src/routes/+page.svelte

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
working_folder,
44
available_images,
55
is_loading_image_data,
6-
file_server_port
6+
file_server_port,
7+
current_caption,
8+
active_image
79
} from '$lib/stores';
810
import { ProgressRadial } from '@skeletonlabs/skeleton';
911
import { dragscroll } from '@svelte-put/dragscroll';
@@ -91,6 +93,11 @@
9193
behavior: 'smooth'
9294
});
9395
};
96+
97+
$: {
98+
$current_caption = $available_images[current_file]?.caption || '';
99+
$active_image = $available_images[current_file];
100+
}
94101
</script>
95102

96103
<svelte:window on:keydown={keydownHandler} />
@@ -141,18 +148,20 @@
141148

142149
<div class="px-2">
143150
<label class="label">
144-
<span
145-
>Caption <div class="inline text-gray-500 italic">
146-
({$available_images[current_file].image})
147-
</div></span
148-
>
149-
<textarea
150-
class="textarea"
151-
on:keydown|stopPropagation={(event) => keydownHandler(event, true)}
152-
rows="2"
153-
placeholder="Write your caption for the image here."
154-
bind:value={$available_images[current_file].caption}
155-
/>
151+
{#if $active_image}
152+
<span
153+
>Caption <div class="inline text-gray-500 italic">
154+
({$active_image.image})
155+
</div></span
156+
>
157+
<textarea
158+
class="textarea"
159+
on:keydown|stopPropagation={(event) => keydownHandler(event, true)}
160+
rows="2"
161+
placeholder="Write your caption for the image here."
162+
bind:value={$active_image.caption}
163+
/>
164+
{/if}
156165
</label>
157166
</div>
158167

0 commit comments

Comments
 (0)