mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
refactor(vue-widgets): adopt DOM widget value persistence best practices for randomizer and cycler
- Replace custom onSetValue with ComfyUI's built-in widget.callback - Remove widget.updateConfig, set widget.value directly - Add isRestoring flag to break callback → watch → widget.value loop - Update ComponentWidget types with generic parameter for type-safe callbacks Refs: docs/dom-widgets/value-persistence-best-practices.md
This commit is contained in:
@@ -25,9 +25,11 @@ import LoraCyclerSettingsView from './lora-cycler/LoraCyclerSettingsView.vue'
|
||||
import { useLoraCyclerState } from '../composables/useLoraCyclerState'
|
||||
import type { ComponentWidget, CyclerConfig, LoraPoolConfig } from '../composables/types'
|
||||
|
||||
type CyclerWidget = ComponentWidget<CyclerConfig>
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
widget: ComponentWidget
|
||||
widget: CyclerWidget
|
||||
node: { id: number; inputs?: any[]; widgets?: any[]; graph?: any }
|
||||
}>()
|
||||
|
||||
@@ -112,19 +114,17 @@ const checkPoolConfigChanges = async () => {
|
||||
|
||||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
// Setup serialization
|
||||
props.widget.serializeValue = async () => {
|
||||
return state.buildConfig()
|
||||
// Setup callback for external value updates (e.g., workflow load, undo/redo)
|
||||
// ComfyUI calls this automatically after setValue() in domWidget.ts
|
||||
props.widget.callback = (v: CyclerConfig) => {
|
||||
if (v) {
|
||||
state.restoreFromConfig(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle external value updates (e.g., loading workflow, paste)
|
||||
props.widget.onSetValue = (v) => {
|
||||
state.restoreFromConfig(v as CyclerConfig)
|
||||
}
|
||||
|
||||
// Restore from saved value
|
||||
// Restore from saved value if workflow was already loaded
|
||||
if (props.widget.value) {
|
||||
state.restoreFromConfig(props.widget.value as CyclerConfig)
|
||||
state.restoreFromConfig(props.widget.value)
|
||||
}
|
||||
|
||||
// Add beforeQueued hook to handle index shifting for batch queue synchronization
|
||||
@@ -141,12 +141,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
// Update the widget value so the indices are included in the serialized config
|
||||
const config = state.buildConfig()
|
||||
if ((props.widget as any).updateConfig) {
|
||||
;(props.widget as any).updateConfig(config)
|
||||
} else {
|
||||
props.widget.value = config
|
||||
}
|
||||
props.widget.value = state.buildConfig()
|
||||
}
|
||||
|
||||
// Mark component as mounted
|
||||
|
||||
@@ -45,9 +45,11 @@ import LoraRandomizerSettingsView from './lora-randomizer/LoraRandomizerSettings
|
||||
import { useLoraRandomizerState } from '../composables/useLoraRandomizerState'
|
||||
import type { ComponentWidget, RandomizerConfig, LoraEntry } from '../composables/types'
|
||||
|
||||
type RandomizerWidget = ComponentWidget<RandomizerConfig>
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
widget: ComponentWidget
|
||||
widget: RandomizerWidget
|
||||
node: { id: number; inputs?: any[]; widgets?: any[]; graph?: any }
|
||||
}>()
|
||||
|
||||
@@ -177,20 +179,17 @@ onMounted(async () => {
|
||||
// Mark component as mounted so watch can now respond to changes
|
||||
isMounted.value = true
|
||||
|
||||
// Setup serialization
|
||||
props.widget.serializeValue = async () => {
|
||||
const config = state.buildConfig()
|
||||
return config
|
||||
// Setup callback for external value updates (e.g., workflow load, undo/redo)
|
||||
// ComfyUI calls this automatically after setValue() in domWidget.ts
|
||||
props.widget.callback = (v: RandomizerConfig) => {
|
||||
if (v) {
|
||||
state.restoreFromConfig(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle external value updates (e.g., loading workflow, paste)
|
||||
props.widget.onSetValue = (v) => {
|
||||
state.restoreFromConfig(v as RandomizerConfig)
|
||||
}
|
||||
|
||||
// Restore from saved value
|
||||
// Restore from saved value if workflow was already loaded
|
||||
if (props.widget.value) {
|
||||
state.restoreFromConfig(props.widget.value as RandomizerConfig)
|
||||
state.restoreFromConfig(props.widget.value)
|
||||
}
|
||||
|
||||
// Add beforeQueued hook to handle seed shifting for batch queue synchronization
|
||||
@@ -209,12 +208,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
// Update the widget value so the seeds are included in the serialized config
|
||||
const config = state.buildConfig()
|
||||
if ((props.widget as any).updateConfig) {
|
||||
;(props.widget as any).updateConfig(config)
|
||||
} else {
|
||||
props.widget.value = config
|
||||
}
|
||||
props.widget.value = state.buildConfig()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ export interface CyclerLoraItem {
|
||||
model_name: string
|
||||
}
|
||||
|
||||
export function useLoraCyclerState(widget: ComponentWidget) {
|
||||
export function useLoraCyclerState(widget: ComponentWidget<CyclerConfig>) {
|
||||
// Flag to prevent infinite loops during config restoration
|
||||
// callback → restoreFromConfig → watch → widget.value = config → callback → ...
|
||||
let isRestoring = false
|
||||
|
||||
// State refs
|
||||
const currentIndex = ref(1) // 1-based
|
||||
const totalCount = ref(0)
|
||||
@@ -26,33 +30,58 @@ export function useLoraCyclerState(widget: ComponentWidget) {
|
||||
const nextIndex = ref<number | null>(null)
|
||||
|
||||
// Build config object from current state
|
||||
const buildConfig = (): CyclerConfig => ({
|
||||
current_index: currentIndex.value,
|
||||
total_count: totalCount.value,
|
||||
pool_config_hash: poolConfigHash.value,
|
||||
model_strength: modelStrength.value,
|
||||
clip_strength: clipStrength.value,
|
||||
use_same_clip_strength: !useCustomClipRange.value,
|
||||
sort_by: sortBy.value,
|
||||
current_lora_name: currentLoraName.value,
|
||||
current_lora_filename: currentLoraFilename.value,
|
||||
execution_index: executionIndex.value,
|
||||
next_index: nextIndex.value,
|
||||
})
|
||||
const buildConfig = (): CyclerConfig => {
|
||||
// Skip updating widget.value during restoration to prevent infinite loops
|
||||
if (isRestoring) {
|
||||
return {
|
||||
current_index: currentIndex.value,
|
||||
total_count: totalCount.value,
|
||||
pool_config_hash: poolConfigHash.value,
|
||||
model_strength: modelStrength.value,
|
||||
clip_strength: clipStrength.value,
|
||||
use_same_clip_strength: !useCustomClipRange.value,
|
||||
sort_by: sortBy.value,
|
||||
current_lora_name: currentLoraName.value,
|
||||
current_lora_filename: currentLoraFilename.value,
|
||||
execution_index: executionIndex.value,
|
||||
next_index: nextIndex.value,
|
||||
}
|
||||
}
|
||||
return {
|
||||
current_index: currentIndex.value,
|
||||
total_count: totalCount.value,
|
||||
pool_config_hash: poolConfigHash.value,
|
||||
model_strength: modelStrength.value,
|
||||
clip_strength: clipStrength.value,
|
||||
use_same_clip_strength: !useCustomClipRange.value,
|
||||
sort_by: sortBy.value,
|
||||
current_lora_name: currentLoraName.value,
|
||||
current_lora_filename: currentLoraFilename.value,
|
||||
execution_index: executionIndex.value,
|
||||
next_index: nextIndex.value,
|
||||
}
|
||||
}
|
||||
|
||||
// Restore state from config object
|
||||
const restoreFromConfig = (config: CyclerConfig) => {
|
||||
currentIndex.value = config.current_index || 1
|
||||
totalCount.value = config.total_count || 0
|
||||
poolConfigHash.value = config.pool_config_hash || ''
|
||||
modelStrength.value = config.model_strength ?? 1.0
|
||||
clipStrength.value = config.clip_strength ?? 1.0
|
||||
useCustomClipRange.value = !(config.use_same_clip_strength ?? true)
|
||||
sortBy.value = config.sort_by || 'filename'
|
||||
currentLoraName.value = config.current_lora_name || ''
|
||||
currentLoraFilename.value = config.current_lora_filename || ''
|
||||
// Note: execution_index and next_index are not restored from config
|
||||
// as they are transient values used only during batch execution
|
||||
// Set flag to prevent buildConfig from triggering widget.value updates during restoration
|
||||
isRestoring = true
|
||||
|
||||
try {
|
||||
currentIndex.value = config.current_index || 1
|
||||
totalCount.value = config.total_count || 0
|
||||
poolConfigHash.value = config.pool_config_hash || ''
|
||||
modelStrength.value = config.model_strength ?? 1.0
|
||||
clipStrength.value = config.clip_strength ?? 1.0
|
||||
useCustomClipRange.value = !(config.use_same_clip_strength ?? true)
|
||||
sortBy.value = config.sort_by || 'filename'
|
||||
currentLoraName.value = config.current_lora_name || ''
|
||||
currentLoraFilename.value = config.current_lora_filename || ''
|
||||
// Note: execution_index and next_index are not restored from config
|
||||
// as they are transient values used only during batch execution
|
||||
} finally {
|
||||
isRestoring = false
|
||||
}
|
||||
}
|
||||
|
||||
// Shift indices for batch queue synchronization
|
||||
@@ -208,12 +237,7 @@ export function useLoraCyclerState(widget: ComponentWidget) {
|
||||
currentLoraName,
|
||||
currentLoraFilename,
|
||||
], () => {
|
||||
const config = buildConfig()
|
||||
if (widget.updateConfig) {
|
||||
widget.updateConfig(config)
|
||||
} else {
|
||||
widget.value = config
|
||||
}
|
||||
widget.value = buildConfig()
|
||||
}, { deep: true })
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import type { ComponentWidget, RandomizerConfig, LoraEntry } from './types'
|
||||
|
||||
export function useLoraRandomizerState(widget: ComponentWidget) {
|
||||
export function useLoraRandomizerState(widget: ComponentWidget<RandomizerConfig>) {
|
||||
// Flag to prevent infinite loops during config restoration
|
||||
// callback → restoreFromConfig → watch → widget.value = config → callback → ...
|
||||
let isRestoring = false
|
||||
|
||||
// State refs
|
||||
const countMode = ref<'fixed' | 'range'>('range')
|
||||
const countFixed = ref(3)
|
||||
@@ -28,7 +32,29 @@ export function useLoraRandomizerState(widget: ComponentWidget) {
|
||||
const nextSeed = ref<number | null>(null)
|
||||
|
||||
// Build config object from current state
|
||||
const buildConfig = (): RandomizerConfig => ({
|
||||
const buildConfig = (): RandomizerConfig => {
|
||||
// Skip updating widget.value during restoration to prevent infinite loops
|
||||
if (isRestoring) {
|
||||
return {
|
||||
count_mode: countMode.value,
|
||||
count_fixed: countFixed.value,
|
||||
count_min: countMin.value,
|
||||
count_max: countMax.value,
|
||||
model_strength_min: modelStrengthMin.value,
|
||||
model_strength_max: modelStrengthMax.value,
|
||||
use_same_clip_strength: !useCustomClipRange.value,
|
||||
clip_strength_min: clipStrengthMin.value,
|
||||
clip_strength_max: clipStrengthMax.value,
|
||||
roll_mode: rollMode.value,
|
||||
last_used: lastUsed.value,
|
||||
use_recommended_strength: useRecommendedStrength.value,
|
||||
recommended_strength_scale_min: recommendedStrengthScaleMin.value,
|
||||
recommended_strength_scale_max: recommendedStrengthScaleMax.value,
|
||||
execution_seed: executionSeed.value,
|
||||
next_seed: nextSeed.value,
|
||||
}
|
||||
}
|
||||
return {
|
||||
count_mode: countMode.value,
|
||||
count_fixed: countFixed.value,
|
||||
count_min: countMin.value,
|
||||
@@ -45,7 +71,8 @@ export function useLoraRandomizerState(widget: ComponentWidget) {
|
||||
recommended_strength_scale_max: recommendedStrengthScaleMax.value,
|
||||
execution_seed: executionSeed.value,
|
||||
next_seed: nextSeed.value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Shift seeds for batch queue synchronization
|
||||
// Previous next_seed becomes current execution_seed, and generate a new next_seed
|
||||
@@ -63,30 +90,37 @@ export function useLoraRandomizerState(widget: ComponentWidget) {
|
||||
|
||||
// Restore state from config object
|
||||
const restoreFromConfig = (config: RandomizerConfig) => {
|
||||
countMode.value = config.count_mode || 'range'
|
||||
countFixed.value = config.count_fixed || 3
|
||||
countMin.value = config.count_min || 2
|
||||
countMax.value = config.count_max || 5
|
||||
modelStrengthMin.value = config.model_strength_min ?? 0.0
|
||||
modelStrengthMax.value = config.model_strength_max ?? 1.0
|
||||
useCustomClipRange.value = !(config.use_same_clip_strength ?? true)
|
||||
clipStrengthMin.value = config.clip_strength_min ?? 0.0
|
||||
clipStrengthMax.value = config.clip_strength_max ?? 1.0
|
||||
// Migrate old roll_mode values to new ones
|
||||
const rawRollMode = (config as any).roll_mode as string
|
||||
if (rawRollMode === 'frontend') {
|
||||
rollMode.value = 'fixed'
|
||||
} else if (rawRollMode === 'backend') {
|
||||
rollMode.value = 'always'
|
||||
} else if (rawRollMode === 'fixed' || rawRollMode === 'always') {
|
||||
rollMode.value = rawRollMode as 'fixed' | 'always'
|
||||
} else {
|
||||
rollMode.value = 'fixed'
|
||||
// Set flag to prevent buildConfig from triggering widget.value updates during restoration
|
||||
isRestoring = true
|
||||
|
||||
try {
|
||||
countMode.value = config.count_mode || 'range'
|
||||
countFixed.value = config.count_fixed || 3
|
||||
countMin.value = config.count_min || 2
|
||||
countMax.value = config.count_max || 5
|
||||
modelStrengthMin.value = config.model_strength_min ?? 0.0
|
||||
modelStrengthMax.value = config.model_strength_max ?? 1.0
|
||||
useCustomClipRange.value = !(config.use_same_clip_strength ?? true)
|
||||
clipStrengthMin.value = config.clip_strength_min ?? 0.0
|
||||
clipStrengthMax.value = config.clip_strength_max ?? 1.0
|
||||
// Migrate old roll_mode values to new ones
|
||||
const rawRollMode = (config as any).roll_mode as string
|
||||
if (rawRollMode === 'frontend') {
|
||||
rollMode.value = 'fixed'
|
||||
} else if (rawRollMode === 'backend') {
|
||||
rollMode.value = 'always'
|
||||
} else if (rawRollMode === 'fixed' || rawRollMode === 'always') {
|
||||
rollMode.value = rawRollMode as 'fixed' | 'always'
|
||||
} else {
|
||||
rollMode.value = 'fixed'
|
||||
}
|
||||
lastUsed.value = config.last_used || null
|
||||
useRecommendedStrength.value = config.use_recommended_strength ?? false
|
||||
recommendedStrengthScaleMin.value = config.recommended_strength_scale_min ?? 0.5
|
||||
recommendedStrengthScaleMax.value = config.recommended_strength_scale_max ?? 1.0
|
||||
} finally {
|
||||
isRestoring = false
|
||||
}
|
||||
lastUsed.value = config.last_used || null
|
||||
useRecommendedStrength.value = config.use_recommended_strength ?? false
|
||||
recommendedStrengthScaleMin.value = config.recommended_strength_scale_min ?? 0.5
|
||||
recommendedStrengthScaleMax.value = config.recommended_strength_scale_max ?? 1.0
|
||||
}
|
||||
|
||||
// Roll loras - call API to get random selection
|
||||
@@ -182,12 +216,7 @@ export function useLoraRandomizerState(widget: ComponentWidget) {
|
||||
recommendedStrengthScaleMin,
|
||||
recommendedStrengthScaleMax,
|
||||
], () => {
|
||||
const config = buildConfig()
|
||||
if (widget.updateConfig) {
|
||||
widget.updateConfig(config)
|
||||
} else {
|
||||
widget.value = config
|
||||
}
|
||||
widget.value = buildConfig()
|
||||
}, { deep: true })
|
||||
|
||||
return {
|
||||
|
||||
@@ -157,10 +157,8 @@ function createLoraRandomizerWidget(node) {
|
||||
},
|
||||
setValue(v: RandomizerConfig) {
|
||||
internalValue = v
|
||||
console.log('randomizer widget value update: ', internalValue)
|
||||
if (typeof widget.onSetValue === 'function') {
|
||||
widget.onSetValue(v)
|
||||
}
|
||||
// ComfyUI automatically calls widget.callback after setValue
|
||||
// No need for custom onSetValue mechanism
|
||||
},
|
||||
serialize: true,
|
||||
getMinHeight() {
|
||||
@@ -169,10 +167,6 @@ function createLoraRandomizerWidget(node) {
|
||||
}
|
||||
)
|
||||
|
||||
widget.updateConfig = (v: RandomizerConfig) => {
|
||||
internalValue = v
|
||||
}
|
||||
|
||||
// Add method to get pool config from connected node
|
||||
node.getPoolConfig = () => getPoolConfigFromConnectedNode(node)
|
||||
|
||||
@@ -242,9 +236,8 @@ function createLoraCyclerWidget(node) {
|
||||
setValue(v: CyclerConfig) {
|
||||
const oldFilename = internalValue?.current_lora_filename
|
||||
internalValue = v
|
||||
if (typeof widget.onSetValue === 'function') {
|
||||
widget.onSetValue(v)
|
||||
}
|
||||
// ComfyUI automatically calls widget.callback after setValue
|
||||
// No need for custom onSetValue mechanism
|
||||
// Update downstream loaders when the active LoRA filename changes
|
||||
if (oldFilename !== v?.current_lora_filename) {
|
||||
updateDownstreamLoaders(node)
|
||||
@@ -257,15 +250,6 @@ function createLoraCyclerWidget(node) {
|
||||
}
|
||||
)
|
||||
|
||||
widget.updateConfig = (v: CyclerConfig) => {
|
||||
const oldFilename = internalValue?.current_lora_filename
|
||||
internalValue = v
|
||||
// Update downstream loaders when the active LoRA filename changes
|
||||
if (oldFilename !== v?.current_lora_filename) {
|
||||
updateDownstreamLoaders(node)
|
||||
}
|
||||
}
|
||||
|
||||
// Add method to get pool config from connected node
|
||||
node.getPoolConfig = () => getPoolConfigFromConnectedNode(node)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user