Skip to content

Commit

Permalink
support tag filters and pump to v0.1.1
Browse files Browse the repository at this point in the history
  • Loading branch information
skiars committed Jun 25, 2023
1 parent 703183a commit 23244a8
Show file tree
Hide file tree
Showing 12 changed files with 125 additions and 57 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This is a simple tag editor for stable diffusion datasets. It can be used to edi
- Intelligent prompt for tag input (support fuzzy matching)
- Automatic translation (translation to Chinese is now hard-coded, and may require magic to surf the Internet)
- Insert tags in batches, you can specify the insertion position
- quick response
- Quick response

## Screenshot

Expand All @@ -34,7 +34,7 @@ Enter a tag in the *add tag* input box and click the *Insert* button to insert a
The insertion position is specified by the *position* box. These modes are currently supported:
- **auto**: Insert tags to tail if there is no label to be inserted in the image, otherwise do nothing;
- **Positive number**: Insert tags into the position counting from the head, if the tag already exists in the image, it will be moved to the specified position;
- **Negative numbers**: Similar to positive numbers, but counting from the tail to the front.
- **Negative number**: Similar to positive numbers, but counting from the tail to the front.

The insertion position can exceed the actual number of tags in the image, and the tags will be inserted at the head or tail position at this time.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "sd-tagtool",
"private": true,
"version": "0.1.0",
"version": "0.1.1",
"type": "module",
"scripts": {
"dev": "vite",
Expand Down
1 change: 0 additions & 1 deletion src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ fn main() {
.resolve_resource("shared/tags.db")
.expect("failed to resolve tags.db path");
let state: State<CmdState> = app.state();
println!("tags_db_path: {:?}", tags_db_path);
state.tags_db.lock().unwrap().read_db(tags_db_path);
Ok(())
})
Expand Down
7 changes: 5 additions & 2 deletions src-tauri/src/tagutils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use std::fs;
use std::fs::{File};
use std::path::Path;
use std::path::PathBuf;
use std::time::{SystemTime};
use std::collections::HashMap;
use simsearch::{SimSearch, SearchOptions};

Expand Down Expand Up @@ -155,11 +156,13 @@ impl TagHintDB {
}
}
pub fn read_db<P: AsRef<Path>>(&mut self, db_path: P) {
let start_time = SystemTime::now();
let csv_list = list_csv(db_path).unwrap_or(Vec::new());
for p in csv_list {
println!("find tags.db: {}", p.to_str().unwrap());
println!("load tags.db: {}", p.to_str().unwrap());
read_tag_csv(self, p).unwrap_or(());
}
println!("scanned {:?} tags", self.database.len());
println!("-- scanned {} tags took {} ms", self.database.len(),
SystemTime::now().duration_since(start_time).unwrap().as_millis());
}
}
2 changes: 1 addition & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
"package": {
"productName": "sd-tagtool",
"version": "0.1.0"
"version": "0.1.1"
},
"tauri": {
"allowlist": {
Expand Down
92 changes: 55 additions & 37 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
<script setup lang="ts">
import {ref} from 'vue'
import Splitter from 'primevue/splitter';
import Splitter from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import ImageList from './components/ImageList.vue'
import TagList from './components/TagList.vue'
import TagInput from "./components/TagEditor.vue";
import TagEditor from './components/TagEditor.vue'
import ImageFilter from './components/ImageFilter.vue'
import {TagData} from './lib/types'
import {CollectTags, TagEditor, collectTags, deleteTags, insertTags} from './lib/utils'
import {CollectTags, EditorHistory, collectTags, deleteTags, insertTags} from './lib/utils'
import {open} from '@tauri-apps/api/dialog'
import {invoke} from '@tauri-apps/api/tauri'
Expand All @@ -17,10 +18,11 @@ import {join} from '@tauri-apps/api/path'
import {convertFileSrc} from '@tauri-apps/api/tauri'
import {platform} from '@tauri-apps/api/os'
let tagEditor: TagEditor = new TagEditor
let history: EditorHistory = new EditorHistory
let tagInsPos: number | undefined = undefined
let workDir: string = ''
const dataset = ref<TagData[]>([])
const filteredDataset = ref<TagData[]>([])
const selected = ref<number[]>([])
const selTags = ref(collectTags())
const allTags = ref(collectTags())
Expand Down Expand Up @@ -50,9 +52,10 @@ async function openFolder(path?: string) {
}
workDir = path
dataset.value = data
filteredDataset.value = data
selected.value = []
updateTags(data)
tagEditor = new TagEditor(dataset.value)
history = new EditorHistory(dataset.value)
}
function selectedTags(d: { index: number }[]) {
Expand All @@ -62,7 +65,7 @@ function selectedTags(d: { index: number }[]) {
function onTagsChange(x: string[]) {
if (selected.value.length == 1) {
const d = tagEditor.edit([{index: selected.value[0], tags: x}])
const d = history.edit([{index: selected.value[0], tags: x}])
updateTags(d)
}
}
Expand All @@ -77,14 +80,30 @@ function updateTags(d: TagData[] | undefined) {
function onDeleteTags(collect: CollectTags, tags: string[]) {
const edit = deleteTags(dataset.value, collect, tags)
updateTags(tagEditor.edit(edit))
updateTags(history.edit(edit))
}
function onInsertTags(tags: string[]) {
const sel: Set<number> = new Set(selected.value)
const data = dataset.value.filter(x => sel.has(x.key))
const edit = insertTags(data, tags, tagInsPos)
updateTags(tagEditor.edit(edit))
updateTags(history.edit(edit))
}
function onFilterApply(e: { tags: string[], exclude: boolean }) {
if (e.tags) {
function include(x: TagData): boolean {
const s = new Set(x.tags)
return e.tags.every(a => s.has(a))
}
function exclude(x: TagData): boolean {
const s = new Set(x.tags)
return !e.tags.some(a => s.has(a))
}
filteredDataset.value = dataset.value.filter(e.exclude ? exclude : include)
} else {
filteredDataset.value = dataset.value
}
}
async function menuAction(menu: string) {
Expand All @@ -102,10 +121,10 @@ async function menuAction(menu: string) {
alert('All content has been saved!')
break
case 'undo':
updateTags(tagEditor.undo())
updateTags(history.undo())
break
case 'redo':
updateTags(tagEditor.redo())
updateTags(history.redo())
break
}
}
Expand Down Expand Up @@ -133,33 +152,32 @@ listen('translate', event => {
</script>

<template>
<Splitter class="main-content">
<SplitterPanel :size="20">
<ImageList :dataset="dataset"
v-on:select="selectedTags($event)"
v-on:openFolder="openFolder()"/>
</SplitterPanel>
<SplitterPanel :size="80">
<Splitter layout="vertical">
<SplitterPanel class="column-flex">
<TagList style="flex-grow: 1" :tags="selTags.tags"
editable :nodrag="selected.length > 1" :translate="translatedTags"
v-on:sorted="onTagsChange"
v-on:delete="x => onDeleteTags(selTags, x)"/>
<TagInput style="flex-shrink: 0" :translate="translatedTags"
v-model:editAllTags="editAllTags"
v-on:updatePosition="x => tagInsPos = x"
v-on:updateTags="onInsertTags"/>
</SplitterPanel>
<SplitterPanel class="column-flex">
<TagList style="flex-grow: 1" :tags="allTags.tags"
:editable="editAllTags" nodrag :translate="translatedTags"
v-on:delete="onDeleteTags(allTags, $event)"
v-on:active="onInsertTags($event)"/>
</SplitterPanel>
</Splitter>
</SplitterPanel>
</Splitter>
<splitter class="main-content">
<splitter-panel :size="20">
<image-list :dataset="filteredDataset" v-on:select="selectedTags"/>
</splitter-panel>
<splitter-panel :size="80">
<splitter layout="vertical">
<splitter-panel class="column-flex">
<image-filter v-on:filter="onFilterApply"/>
<tag-list style="flex-grow: 1" :tags="selTags.tags"
editable :nodrag="selected.length > 1" :translate="translatedTags"
v-on:sorted="onTagsChange"
v-on:delete="x => onDeleteTags(selTags, x)"/>
<tag-editor style="flex-shrink: 0" :translate="translatedTags"
v-model:editAllTags="editAllTags"
v-on:updatePosition="x => tagInsPos = x"
v-on:updateTags="onInsertTags"/>
</splitter-panel>
<splitter-panel class="column-flex">
<tag-list style="flex-grow: 1" :tags="allTags.tags"
:editable="editAllTags" nodrag :translate="translatedTags"
v-on:delete="e => onDeleteTags(allTags, e)"
v-on:active="onInsertTags"/>
</splitter-panel>
</splitter>
</splitter-panel>
</splitter>
</template>

<style scoped>
Expand Down
45 changes: 45 additions & 0 deletions src/components/ImageFilter.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup lang="ts">
import {ref} from 'vue'
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import TagInput from './TagInput.vue'
const emit = defineEmits<{
(e: 'filter', value: { tags: string[], exclude: boolean }): void
}>()
let tags: string[] = []
let exclude = ref(false)
</script>

<template>
<div class="tag-input-container">
<tag-input class="tag-input"
placeholder="Enter tags and filter images"
v-on:updateTags="x => tags = x"/>
<checkbox v-model="exclude" :binary="true" inputId="image-filter-checkbox"/>
<label for="image-filter-checkbox">exclude</label>
<Button rounded v-on:click="emit('filter', {tags: tags, exclude: exclude})">
Filter
</Button>
</div>
</template>

<style scoped>
.tag-input-container {
display: flex;
align-items: center;
gap: 0.5em;
}
.tag-input {
flex-grow: 1;
}
label {
-webkit-user-select: none; /* Safari */
-ms-user-select: none; /* IE 10 and IE 11 */
user-select: none; /* Standard syntax */
}
</style>
14 changes: 5 additions & 9 deletions src/components/ImageList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ const selected = ref<TagData[]>([])
const emit = defineEmits<{
(e: 'select', value: { index: number }[]): void
(e: 'openFolder'): void
}>()
watch(selected, value => {
Expand All @@ -21,26 +20,23 @@ watch(selected, value => {
</script>

<template>
<DataTable :value="props.dataset"
<data-table :value="props.dataset"
v-model:selection="selected" selection-mode="multiple" class="image-list">
<template #empty>
<div class="empty-list" v-on:click="emit('openFolder')">
Open a folder and continue.
</div>
<div class="empty-list">There are no images to show.</div>
</template>
<Column field="url">
<column field="url">
<template #body="{ data }">
<div>
<img class="image" :src="data.url" :alt="data.url"/>
</div>
</template>
</Column>
</DataTable>
</column>
</data-table>
</template>

<style scoped>
.empty-list {
cursor: pointer;
text-align: center;
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/TagEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ const editAllTags = computed<boolean>({
v-on:update:modelValue="emit('updatePosition', $event)"
:inputStyle="{ padding: '0.25em', width: '5em' }"/>
<span>add tag</span>
<tag-input class="tag-input" :translate="props.translate" v-on:updateTags="x => tags = x"/>
<tag-input class="tag-input" :translate="props.translate"
placeholder="Separate tags with ',' or press Enter"
v-on:updateTags="x => tags = x"/>
<Button rounded v-on:click="emit('updateTags', tags)">Insert</Button>
<span>edit all tags</span>
<input-switch v-model="editAllTags"></input-switch>
Expand Down
5 changes: 3 additions & 2 deletions src/components/TagInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import AutoComplete, {AutoCompleteCompleteEvent} from "primevue/autocomplete";
const props = defineProps<{
translate?: true | boolean
placeholder?: string
}>()
const emit = defineEmits<{
Expand Down Expand Up @@ -40,7 +41,7 @@ async function search(event: AutoCompleteCompleteEvent) {
if (props.translate) {
suggestions.value.forEach(x => {
invoke('translate_tag', {text: x.tag})
.then(tr => x.translate = tr as string)
.then(tr => x.translate = tr as string)
})
}
} else {
Expand All @@ -57,7 +58,7 @@ function readableNumber(x: number): string {
<auto-complete v-model="tags" multiple :suggestions="suggestions"
:option-label="optionLabel" v-on:complete="search"
v-on:change="emit('updateTags', tags.map(optionLabel))"
:placeholder="!tags.length ? 'Separate tags with \',\' or press Enter' : ''">
:placeholder="!tags.length ? placeholder : ''">
<template #option="{option}: {option: TagHint}">
<span>{{ option.tag }}</span>
<span v-if="option.suggest">&nbsp;→ {{ option.suggest }}</span>
Expand Down
2 changes: 1 addition & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ interface EditAction {
tags: string[]
}

export class TagEditor {
export class EditorHistory {
constructor(dataset: TagData[] = []) {
this.dataset = dataset
}
Expand Down
4 changes: 4 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ html {
border-radius: 0.4em;
}

.p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-input-token input {
padding: 0.1em 0;
}

.p-autocomplete .p-autocomplete-multiple-container .p-autocomplete-input-token {
padding: 0;
}

0 comments on commit 23244a8

Please sign in to comment.