Files
ComfyUI-Lora-Manager/static/js/state/index.js
T
Will Miao f5e983eaaa feat: optional centralized storage for sidecar metadata and previews (#1045)
Add an opt-in 'centralized' sidecar storage mode alongside the default
'alongside' layout. In centralized mode, .metadata.json sidecars and
preview assets live under a configurable root (sidecar_storage_path,
default <settings_dir>/sidecars), mirroring the library-relative
directory structure: <root>/<library>/<root_basename>/<rel_dir>/.

Backend:
- settings: sidecar_storage_mode / sidecar_storage_path with validation;
  changing either refreshes the preview allowlist
- config: centralized root added to preview-serving allowlist
- lifecycle: delete / move / rename / folder-rename / folder-delete and
  undoable-delete staging all operate on the mirror tree in centralized
  mode (model files themselves never move); EXDEV-tolerant cross-
  filesystem moves
- scanners: pending-hash filesystem scan walks the mirror tree in
  centralized mode; preview discovery reads from the sidecar dir;
  .civitai.info stays co-located in both modes
- migration: SidecarMigrationUseCase moves sidecars+previews between
  layouts both directions (keep-newer conflict resolution, preview_url
  rewriting, WebSocket progress), exposed as POST+GET
  /api/lm/sidecars/migrate with a mode guard (force=true for the
  settings-first flow)

Frontend:
- settings modal: sidecar storage section (mode select + path input with
  browse/validation), mode-change confirmation offering immediate
  migration (force=true), and a 'Migrate Sidecars Now' action
- i18n keys synced to all locales ([TODO: Translate] placeholders)

Docs: metadata-json-schema.md gains a storage-location section;
AGENTS.md records the sidecar_paths helper convention.
2026-09-26 10:00:58 +08:00

400 lines
13 KiB
JavaScript

// Create the new hierarchical state structure
import { getStorageItem, getMapFromStorage } from '../utils/storageHelpers.js';
import { MODEL_TYPES } from '../api/apiConfig.js';
import { DEFAULT_PATH_TEMPLATES, DEFAULT_FILENAME_TEMPLATES, DEFAULT_PRIORITY_TAG_CONFIG } from '../utils/constants.js';
const DEFAULT_SETTINGS_BASE = Object.freeze({
civitai_api_key: '',
civitai_api_key_set: false,
huggingface_api_key: '',
huggingface_api_key_set: false,
civitai_host: 'civitai.com',
download_backend: 'python',
aria2c_path: '',
use_portable_settings: false,
language: 'en',
show_only_sfw: false,
enable_metadata_archive_db: false,
enable_civarchive_api: true,
metadata_provider_order: 'civitai_archive_sqlite',
proxy_enabled: false,
proxy_type: 'http',
proxy_host: '',
proxy_port: '',
proxy_username: '',
proxy_password: '',
default_lora_root: '',
default_checkpoint_root: '',
default_embedding_root: '',
default_other_roots: {},
enable_other_models: false,
enabled_other_sub_types: ['vae', 'upscaler', 'text_encoder'],
recipes_path: '',
base_model_path_mappings: {},
download_path_templates: {},
download_filename_templates: {},
example_images_path: '',
example_images_open_mode: 'system',
example_images_local_root: '',
example_images_open_uri_template: '',
optimize_example_images: true,
auto_download_example_images: false,
blur_mature_content: true,
mature_blur_level: 'R',
card_blur_amount: 8,
autoplay_on_hover: false,
display_density: 'default',
recipes_layout: 'grid',
card_info_display: 'always',
model_name_display: 'model_name',
lora_syntax_format: 'legacy',
model_card_footer_action: 'example_images',
show_version_on_card: true,
include_trigger_words: false,
compact_mode: false,
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
version_grouping: 'same_base',
hide_early_access_updates: false,
hide_paid_updates: false,
auto_organize_exclusions: [],
metadata_refresh_skip_paths: [],
skip_previously_downloaded_model_versions: false,
download_skip_base_models: [],
backup_auto_enabled: true,
backup_retention_count: 5,
sidecar_storage_mode: 'alongside',
sidecar_storage_path: '',
strip_lora_on_copy: false,
use_new_license_icons: true,
group_by_model: false,
sticky_controls: false,
llm_provider: 'openai',
llm_api_key: '',
llm_api_base: '',
llm_model: '',
});
export function createDefaultSettings() {
return {
...DEFAULT_SETTINGS_BASE,
base_model_path_mappings: {},
download_path_templates: { ...DEFAULT_PATH_TEMPLATES },
download_filename_templates: { ...DEFAULT_FILENAME_TEMPLATES },
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
default_other_roots: {},
enabled_other_sub_types: ['vae', 'upscaler', 'text_encoder'],
// Standalone-only fields populated by GET /api/lm/settings; in plugin
// mode the backend omits folder_paths/folder_path_schema and these
// defaults apply.
standalone_mode: false,
folder_paths: {},
folder_path_schema: [],
};
}
// Load preview versions from localStorage for each model type
const loraPreviewVersions = getMapFromStorage('loras_preview_versions');
const checkpointPreviewVersions = getMapFromStorage('checkpoints_preview_versions');
const embeddingPreviewVersions = getMapFromStorage('embeddings_preview_versions');
const otherPreviewVersions = getMapFromStorage('other_preview_versions');
export const state = {
// Global state
global: {
settings: createDefaultSettings(),
loadingManager: null,
observer: null,
},
// Page-specific states
pages: {
[MODEL_TYPES.LORA]: {
currentPage: 1,
isLoading: false,
hasMore: true,
sortBy: 'name',
activeFolder: getStorageItem(`${MODEL_TYPES.LORA}_activeFolder`),
activeLetterFilter: null,
previewVersions: loraPreviewVersions,
searchManager: null,
searchOptions: {
filename: true,
modelname: true,
tags: false,
creator: false,
hash: false,
recursive: getStorageItem(`${MODEL_TYPES.LORA}_recursiveSearch`, true),
},
filters: {
baseModel: [],
tags: {},
license: {},
modelTypes: [],
search: '',
tagLogic: 'any',
},
bulkMode: false,
selectedLoras: new Set(),
loraMetadataCache: new Map(),
showFavoritesOnly: false,
showUpdateAvailableOnly: false,
duplicatesMode: false,
viewMode: 'active',
excludedViewState: {
sortBy: 'name:asc',
search: '',
},
activeViewSnapshot: null,
},
recipes: {
currentPage: 1,
isLoading: false,
hasMore: true,
sortBy: 'date:desc',
activeFolder: getStorageItem('recipes_activeFolder'),
searchManager: null,
searchOptions: {
title: true,
tags: true,
loraName: true,
loraModel: true,
prompt: true,
recursive: getStorageItem('recipes_recursiveSearch', true),
},
filters: {
baseModel: [],
tags: {},
license: {},
modelTypes: [],
loraAvailability: [],
search: ''
},
pageSize: 20,
showFavoritesOnly: false,
duplicatesMode: false,
bulkMode: false,
selectedModels: new Set(),
},
[MODEL_TYPES.CHECKPOINT]: {
currentPage: 1,
isLoading: false,
hasMore: true,
sortBy: 'name',
activeFolder: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_activeFolder`),
previewVersions: checkpointPreviewVersions,
searchManager: null,
searchOptions: {
filename: true,
modelname: true,
creator: false,
hash: false,
recursive: getStorageItem(`${MODEL_TYPES.CHECKPOINT}_recursiveSearch`, true),
},
filters: {
baseModel: [],
tags: {},
license: {},
modelTypes: [],
search: '',
tagLogic: 'any',
},
modelType: 'checkpoint', // 'checkpoint' or 'diffusion_model'
bulkMode: false,
selectedModels: new Set(),
metadataCache: new Map(),
showFavoritesOnly: false,
showUpdateAvailableOnly: false,
duplicatesMode: false,
viewMode: 'active',
excludedViewState: {
sortBy: 'name:asc',
search: '',
},
activeViewSnapshot: null,
},
[MODEL_TYPES.EMBEDDING]: {
currentPage: 1,
isLoading: false,
hasMore: true,
sortBy: 'name',
activeFolder: getStorageItem(`${MODEL_TYPES.EMBEDDING}_activeFolder`),
activeLetterFilter: null,
previewVersions: embeddingPreviewVersions,
searchManager: null,
searchOptions: {
filename: true,
modelname: true,
tags: false,
creator: false,
hash: false,
recursive: getStorageItem(`${MODEL_TYPES.EMBEDDING}_recursiveSearch`, true),
},
filters: {
baseModel: [],
tags: {},
license: {},
modelTypes: [],
search: '',
tagLogic: 'any',
},
bulkMode: false,
selectedModels: new Set(),
metadataCache: new Map(),
showFavoritesOnly: false,
showUpdateAvailableOnly: false,
duplicatesMode: false,
viewMode: 'active',
excludedViewState: {
sortBy: 'name:asc',
search: '',
},
activeViewSnapshot: null,
},
[MODEL_TYPES.OTHER]: {
currentPage: 1,
isLoading: false,
hasMore: true,
sortBy: 'name',
activeFolder: getStorageItem(`${MODEL_TYPES.OTHER}_activeFolder`),
previewVersions: otherPreviewVersions,
searchManager: null,
searchOptions: {
filename: true,
modelname: true,
tags: false,
creator: false,
hash: false,
recursive: getStorageItem(`${MODEL_TYPES.OTHER}_recursiveSearch`, true),
},
filters: {
baseModel: [],
tags: {},
license: {},
modelTypes: [],
search: '',
tagLogic: 'any',
},
bulkMode: false,
selectedModels: new Set(),
metadataCache: new Map(),
showFavoritesOnly: false,
showUpdateAvailableOnly: false,
duplicatesMode: false,
viewMode: 'active',
excludedViewState: {
sortBy: 'name:asc',
search: '',
},
activeViewSnapshot: null,
}
},
// Current active page - use MODEL_TYPES constants
currentPageType: MODEL_TYPES.LORA,
// Backward compatibility - proxy properties
get currentPage() { return this.pages[this.currentPageType].currentPage; },
set currentPage(value) { this.pages[this.currentPageType].currentPage = value; },
get isLoading() { return this.pages[this.currentPageType].isLoading; },
set isLoading(value) { this.pages[this.currentPageType].isLoading = value; },
get hasMore() { return this.pages[this.currentPageType].hasMore; },
set hasMore(value) { this.pages[this.currentPageType].hasMore = value; },
get sortBy() { return this.pages[this.currentPageType].sortBy; },
set sortBy(value) { this.pages[this.currentPageType].sortBy = value; },
get activeFolder() { return this.pages[this.currentPageType].activeFolder; },
set activeFolder(value) { this.pages[this.currentPageType].activeFolder = value; },
get loadingManager() { return this.global.loadingManager; },
set loadingManager(value) { this.global.loadingManager = value; },
get observer() { return this.global.observer; },
set observer(value) { this.global.observer = value; },
get previewVersions() { return this.pages.loras.previewVersions; },
set previewVersions(value) { this.pages.loras.previewVersions = value; },
get searchManager() { return this.pages[this.currentPageType].searchManager; },
set searchManager(value) { this.pages[this.currentPageType].searchManager = value; },
get searchOptions() { return this.pages[this.currentPageType].searchOptions; },
set searchOptions(value) { this.pages[this.currentPageType].searchOptions = value; },
get filters() { return this.pages[this.currentPageType].filters; },
set filters(value) { this.pages[this.currentPageType].filters = value; },
get bulkMode() {
const currentType = this.currentPageType;
if (currentType === MODEL_TYPES.LORA) {
return this.pages.loras.bulkMode;
} else {
return this.pages[currentType].bulkMode;
}
},
set bulkMode(value) {
const currentType = this.currentPageType;
if (currentType === MODEL_TYPES.LORA) {
this.pages.loras.bulkMode = value;
} else {
this.pages[currentType].bulkMode = value;
}
},
get selectedLoras() { return this.pages.loras.selectedLoras; },
set selectedLoras(value) { this.pages.loras.selectedLoras = value; },
get selectedModels() {
const currentType = this.currentPageType;
if (currentType === MODEL_TYPES.LORA) {
return this.pages.loras.selectedLoras;
} else {
return this.pages[currentType].selectedModels;
}
},
set selectedModels(value) {
const currentType = this.currentPageType;
if (currentType === MODEL_TYPES.LORA) {
this.pages.loras.selectedLoras = value;
} else {
this.pages[currentType].selectedModels = value;
}
},
get loraMetadataCache() { return this.pages.loras.loraMetadataCache; },
set loraMetadataCache(value) { this.pages.loras.loraMetadataCache = value; },
get settings() { return this.global.settings; },
set settings(value) { this.global.settings = value; }
};
// Get the current page state
export function getCurrentPageState() {
return state.pages[state.currentPageType];
}
// Set the current page type
export function setCurrentPageType(pageType) {
if (state.pages[pageType]) {
state.currentPageType = pageType;
return true;
}
console.warn(`Unknown page type: ${pageType}`);
return false;
}
// Initialize page state when a page loads
export function initPageState(pageType) {
if (setCurrentPageType(pageType)) {
console.log(`Initialized state for page: ${pageType}`);
return getCurrentPageState();
}
return null;
}