Compare commits
10 Commits
5a0e1aa9aa
...
2c82f7cfc1
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c82f7cfc1 | |||
| 66a8c1f931 | |||
| bb59e88e8e | |||
| dabb32e8df | |||
| 38bf9ce942 | |||
| ca4c09e55a | |||
| 3a7b21e3e7 | |||
| 7eaafb068d | |||
| f32364c25b | |||
| c8576425a4 |
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>×</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="text-right max-w-[5em]"
|
||||||
|
bind:value={height}
|
||||||
|
oninput={saveTargetSizeDelayed} />
|
||||||
|
<span>px</span>
|
||||||
|
</label>
|
||||||
Reference in New Issue
Block a user