Compare commits

..

10 Commits

11 changed files with 367 additions and 4 deletions
+8
View File
@@ -4,3 +4,11 @@
html, body { html, body {
@apply size-full; @apply size-full;
} }
input {
&[type='number']::-webkit-outer-spin-button,
&[type='number']::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
}
+3 -1
View File
@@ -1,6 +1,8 @@
import type { TargetModels } from "$lib/types/models";
export type DatasetMeta = { export type DatasetMeta = {
name: string; name: string;
targetModel: string; targetModel: TargetModels;
loraType: string; loraType: string;
unifiedImageSize: boolean; unifiedImageSize: boolean;
unifiedImageRatio: boolean; unifiedImageRatio: boolean;
+11
View File
@@ -12,3 +12,14 @@ export const ModelChoices: Option<TargetModels>[] = [
{ label: 'Flux', value: 'flux' }, { label: 'Flux', value: 'flux' },
{ label: 'Flux 2', value: 'flux2' }, { label: 'Flux 2', value: 'flux2' },
] ]
export type LoRATypes = 'char' | 'clothing' | 'style' | 'concept' | 'scene' | 'other';
export const LoRATypeChoices: Option<LoRATypes>[] = [
{ label: 'Character', value: 'char' },
{ label: 'Clothing', value: 'clothing' },
{ label: 'Style', value: 'style' },
{ label: 'Concept', value: 'concept' },
{ label: 'Scene', value: 'scene' },
{ label: 'Other', value: 'other' },
]
+55
View File
@@ -0,0 +1,55 @@
export type SaveFeedbackState = 'idle' | 'updated' | 'not-updated';
export function createSaveFeedbackController(
setState: (state: SaveFeedbackState) => void,
resetDelayMs = 2000,
) {
let feedbackTimer: ReturnType<typeof setTimeout> | null = null;
const resetLater = () => {
if (feedbackTimer) {
clearTimeout(feedbackTimer);
}
feedbackTimer = setTimeout(() => {
setState('idle');
}, resetDelayMs);
};
return {
markUpdated() {
setState('updated');
resetLater();
},
markNotUpdated() {
setState('not-updated');
resetLater();
},
dispose() {
if (feedbackTimer) {
clearTimeout(feedbackTimer);
}
},
};
}
export function createDebouncedTrigger(callback: () => void, delayMs: number) {
let timer: ReturnType<typeof setTimeout> | null = null;
return {
trigger() {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
callback();
}, delayMs);
},
cancel() {
if (timer) {
clearTimeout(timer);
}
},
};
}
+11 -1
View File
@@ -4,6 +4,7 @@ import Close from '$lib/components/icons/Close.svelte';
import { activeDatasetMeta, openedDatasetDir, updateActiveDatasetMeta } from '$lib/stores/dataset'; import { activeDatasetMeta, openedDatasetDir, updateActiveDatasetMeta } from '$lib/stores/dataset';
import { currentActivate } from '$lib/stores/navigate'; import { currentActivate } from '$lib/stores/navigate';
import type { DatasetMeta } from '$lib/types/meta'; import type { DatasetMeta } from '$lib/types/meta';
import { ModelChoices } from '$lib/types/models';
import { message, open } from '@tauri-apps/plugin-dialog'; import { message, open } from '@tauri-apps/plugin-dialog';
import { readDir } from '@tauri-apps/plugin-fs'; import { readDir } from '@tauri-apps/plugin-fs';
import { isNil } from 'es-toolkit'; import { isNil } from 'es-toolkit';
@@ -14,6 +15,7 @@ afterNavigate(() => {
let storePath = $state(''); let storePath = $state('');
let datasetName = $state(''); let datasetName = $state('');
let targetModel = $state(ModelChoices[0].value);
async function backToBoot() { async function backToBoot() {
await goto('/boot'); await goto('/boot');
@@ -58,7 +60,7 @@ async function createDataset() {
if (!isNil(storePath) && !isNil(datasetName) && storePath.trim() && datasetName.trim()) { if (!isNil(storePath) && !isNil(datasetName) && storePath.trim() && datasetName.trim()) {
const datasetMeta: DatasetMeta = { const datasetMeta: DatasetMeta = {
name: datasetName.trim(), name: datasetName.trim(),
targetModel: '', targetModel: targetModel,
loraType: '', loraType: '',
unifiedImageSize: true, unifiedImageSize: true,
unifiedImageRatio: true, unifiedImageRatio: true,
@@ -109,5 +111,13 @@ async function createDataset() {
<span class="label min-w-[10em]">Dataset Name</span> <span class="label min-w-[10em]">Dataset Name</span>
<input type="text" class="min-w-[20em]" bind:value={datasetName} /> <input type="text" class="min-w-[20em]" bind:value={datasetName} />
</label> </label>
<label class="input input-md focus-within:outline-0 w-fit">
<span class="label min-w-[10em]">Target Model</span>
<select class="min-w-[20em] focus:outline-0" bind:value={targetModel}>
{#each ModelChoices as model}
<option value={model.value}>{model.label}</option>
{/each}
</select>
</label>
<button class="btn btn-primary btn-md w-fit self-center" onclick={createDataset}>Create</button> <button class="btn btn-primary btn-md w-fit self-center" onclick={createDataset}>Create</button>
</main> </main>
+19 -2
View File
@@ -3,18 +3,35 @@ import { afterNavigate } from '$app/navigation';
import { activeDatasetMeta } from '$lib/stores/dataset'; import { activeDatasetMeta } from '$lib/stores/dataset';
import { currentActivate } from '$lib/stores/navigate'; import { currentActivate } from '$lib/stores/navigate';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { get } from 'svelte/store'; import { onDestroy } from 'svelte';
import { type Unsubscriber } from 'svelte/store';
import DatasetInfoForm from './DatasetInfoForm.svelte';
let unsubscribeMeta: Unsubscriber | null = null;
function bindWindowTitleToMeta() {
unsubscribeMeta = activeDatasetMeta.subscribe((meta) => {
void invoke('set_window_title', { title: meta?.name ?? '' });
});
}
bindWindowTitleToMeta();
afterNavigate(() => { afterNavigate(() => {
currentActivate.set('dataset'); currentActivate.set('dataset');
});
invoke('set_window_title', { title: get(activeDatasetMeta)?.name }); onDestroy(() => {
if (unsubscribeMeta) {
unsubscribeMeta();
}
}); });
</script> </script>
<main class="px-4 py-4 size-full flex flex-col items-stretch gap-1 overflow-hidden"> <main class="px-4 py-4 size-full flex flex-col items-stretch gap-1 overflow-hidden">
<h2>Dataset Settings</h2> <h2>Dataset Settings</h2>
<hr class="border-zinc-500" /> <hr class="border-zinc-500" />
<DatasetInfoForm />
<h2>Environment Check</h2> <h2>Environment Check</h2>
<hr class="border-zinc-500" /> <hr class="border-zinc-500" />
</main> </main>
+13
View File
@@ -0,0 +1,13 @@
<script lang="ts">
import DatasetName from './meta-form/DatasetName.svelte';
import SelectLoraType from './meta-form/SelectLoraType.svelte';
import SelectModel from './meta-form/SelectModel.svelte';
import TargetSize from './meta-form/TargetSize.svelte';
</script>
<div class="flex flex-col gap-2 px-4 py-4">
<DatasetName />
<SelectModel />
<SelectLoraType />
<TargetSize />
</div>
@@ -0,0 +1,65 @@
<script lang="ts">
import { activeDatasetMeta, updateActiveDatasetMeta } from '$lib/stores/dataset';
import {
createDebouncedTrigger,
createSaveFeedbackController,
type SaveFeedbackState,
} from '$lib/utils/form-save';
import { onDestroy, onMount } from 'svelte';
import { get } from 'svelte/store';
let name = $state('');
let saveFeedback = $state<SaveFeedbackState>('idle');
const feedback = createSaveFeedbackController((state) => {
saveFeedback = state;
});
onMount(() => {
name = get(activeDatasetMeta)?.name ?? '';
});
onDestroy(() => {
saveLater.cancel();
feedback.dispose();
});
async function saveDatasetName(value: string) {
const nextName = value.trim();
if (!nextName) {
feedback.markNotUpdated();
return;
}
try {
await updateActiveDatasetMeta({ name: nextName });
feedback.markUpdated();
} catch (error) {
console.error('Failed to save dataset name:', error);
feedback.markNotUpdated();
}
}
const saveLater = createDebouncedTrigger(() => {
void saveDatasetName(name);
}, 3000);
function saveNameDelayed() {
saveLater.trigger();
}
</script>
<label
class={[
'input w-fit in-focus-within:outline-none transition-colors duration-300 ease-out',
saveFeedback === 'updated' && 'border-green-500',
saveFeedback === 'not-updated' && 'border-red-500',
]}>
<span class="label min-w-[10em]">Dataset Name</span>
<input
type="text"
bind:value={name}
oninput={saveNameDelayed}
class="min-w-[20em] focus:outline-none" />
</label>
@@ -0,0 +1,53 @@
<script lang="ts">
import { activeDatasetMeta, updateActiveDatasetMeta } from '$lib/stores/dataset';
import { LoRATypeChoices, type LoRATypes } from '$lib/types/models';
import { createSaveFeedbackController, type SaveFeedbackState } from '$lib/utils/form-save';
import { onDestroy, onMount } from 'svelte';
import { get } from 'svelte/store';
let selected: LoRATypes | null = $state(null);
let saveFeedback = $state<SaveFeedbackState>('idle');
const feedback = createSaveFeedbackController((state) => {
saveFeedback = state;
});
onMount(() => {
const currentType = get(activeDatasetMeta)?.loraType;
const match = LoRATypeChoices.find((item) => item.value === currentType);
selected = match?.value ?? null;
});
onDestroy(() => {
feedback.dispose();
});
async function saveLoraType() {
if (!selected) {
feedback.markNotUpdated();
return;
}
try {
await updateActiveDatasetMeta({ loraType: selected });
feedback.markUpdated();
} catch (error) {
console.error('Failed to save lora type:', error);
feedback.markNotUpdated();
}
}
</script>
<label
class={[
'input w-fit in-focus-within:outline-none transition-colors duration-300 ease-out',
saveFeedback === 'updated' && 'border-green-500',
saveFeedback === 'not-updated' && 'border-red-500',
]}>
<span class="label min-w-[10em]">LoRA Type</span>
<select bind:value={selected} onchange={saveLoraType} class="min-w-[20em] focus:outline-0">
{#each LoRATypeChoices as loraType}
<option value={loraType.value}>{loraType.label}</option>
{/each}
</select>
</label>
@@ -0,0 +1,51 @@
<script lang="ts">
import { activeDatasetMeta, updateActiveDatasetMeta } from '$lib/stores/dataset';
import { ModelChoices, type TargetModels } from '$lib/types/models';
import { createSaveFeedbackController, type SaveFeedbackState } from '$lib/utils/form-save';
import { onDestroy, onMount } from 'svelte';
import { get } from 'svelte/store';
let selected: TargetModels | null = $state(null);
let saveFeedback = $state<SaveFeedbackState>('idle');
const feedback = createSaveFeedbackController((state) => {
saveFeedback = state;
});
onMount(() => {
selected = get(activeDatasetMeta)?.targetModel ?? null;
});
onDestroy(() => {
feedback.dispose();
});
async function saveTargetModel() {
if (!selected) {
feedback.markNotUpdated();
return;
}
try {
await updateActiveDatasetMeta({ targetModel: selected });
feedback.markUpdated();
} catch (error) {
console.error('Failed to save target model:', error);
feedback.markNotUpdated();
}
}
</script>
<label
class={[
'input w-fit in-focus-within:outline-none transition-colors duration-300 ease-out',
saveFeedback === 'updated' && 'border-green-500',
saveFeedback === 'not-updated' && 'border-red-500',
]}>
<span class="label min-w-[10em]">Target Model</span>
<select bind:value={selected} onchange={saveTargetModel} class="min-w-[20em] focus:outline-0">
{#each ModelChoices as model}
<option value={model.value}>{model.label}</option>
{/each}
</select>
</label>
@@ -0,0 +1,78 @@
<script lang="ts">
import { activeDatasetMeta, updateActiveDatasetMeta } from '$lib/stores/dataset';
import {
createDebouncedTrigger,
createSaveFeedbackController,
type SaveFeedbackState,
} from '$lib/utils/form-save';
import { onDestroy, onMount } from 'svelte';
import { get } from 'svelte/store';
let width = $state<number>(512);
let height = $state<number>(512);
let saveFeedback = $state<SaveFeedbackState>('idle');
const feedback = createSaveFeedbackController((state) => {
saveFeedback = state;
});
onMount(() => {
const meta = get(activeDatasetMeta);
if (meta?.imageSize) {
width = meta.imageSize?.[0] ?? 512;
height = meta.imageSize?.[1] ?? 512;
}
});
onDestroy(() => {
saveLater.cancel();
feedback.dispose();
});
async function saveTargetSize(nextWidth: number, nextHeight: number) {
const normalizedWidth = Number.isFinite(nextWidth) ? Math.floor(nextWidth) : 0;
const normalizedHeight = Number.isFinite(nextHeight) ? Math.floor(nextHeight) : 0;
if (normalizedWidth <= 0 || normalizedHeight <= 0) {
feedback.markNotUpdated();
return;
}
try {
await updateActiveDatasetMeta({ imageSize: [normalizedWidth, normalizedHeight] });
feedback.markUpdated();
} catch (error) {
console.error('Failed to save target size:', error);
feedback.markNotUpdated();
}
}
const saveLater = createDebouncedTrigger(() => {
void saveTargetSize(width, height);
}, 3000);
function saveTargetSizeDelayed() {
saveLater.trigger();
}
</script>
<label
class={[
'input w-fit focus-within:outline-none transition-colors duration-300 ease-out',
saveFeedback === 'updated' && 'border-green-500',
saveFeedback === 'not-updated' && 'border-red-500',
]}>
<span class="label min-w-[10em]">Scale To</span>
<input
type="number"
class="text-right max-w-[5em]"
bind:value={width}
oninput={saveTargetSizeDelayed} />
<span>&times;</span>
<input
type="number"
class="text-right max-w-[5em]"
bind:value={height}
oninput={saveTargetSizeDelayed} />
<span>px</span>
</label>