Files
2026-03-06 04:01:15 +00:00

458 lines
16 KiB
JavaScript

/**
* Unified Data Management Module for LoRA and Model Previews
* Eliminates code duplication across power_lora_loader.js, power_lora_loader_v2.js, and power_model_loader_v2.js
*/
import { rgthreeApi as wanvidApi } from "./rgthree_api.js";
// ============================================================================
// DIRECTORY STRUCTURE MANAGER
// ============================================================================
class DirectoryStructureManager {
/**
* Get the base directory for the given item type
* @param {string} type - 'loras' or 'checkpoints'
* @returns {string} The base directory name
*/
static getBaseDirectory(type) {
return type === 'checkpoints' ? 'checkpoints' : 'loras';
}
/**
* Get the preview directory for the given item type
* @returns {string} The preview directory path
*/
static getPreviewDirectory() {
// Both types use _power_preview in their respective directories
return '_power_preview';
}
/**
* Get API parameters for the given type
* @param {string} type - 'loras' or 'checkpoints'
* @returns {Object} API parameters
*/
static getApiParams(type) {
return {
isModel: type === 'checkpoints',
is_model: type === 'checkpoints'
};
}
}
// ============================================================================
// UI STATE MANAGER
// ============================================================================
class UIManager {
/**
* Get localStorage key for the given type and suffix
* @param {string} type - 'loras' or 'checkpoints'
* @param {string} suffix - 'Cache', 'favorites', 'foldersVisible', etc.
* @returns {string} localStorage key
*/
static getStorageKey(type, suffix) {
const prefix = type === 'checkpoints' ? 'powerModelLoaderV2' : 'wanVideoPowerLoraLoader';
return `${prefix}.${suffix}`;
}
/**
* Save favorites to localStorage
* @param {Array} favorites - Array of favorite item names
* @param {string} type - 'loras' or 'checkpoints'
*/
static saveFavorites(favorites, type) {
const key = this.getStorageKey(type, 'favorites');
localStorage.setItem(key, JSON.stringify(favorites));
}
/**
* Load favorites from localStorage
* @param {string} type - 'loras' or 'checkpoints'
* @returns {Array} Array of favorite item names
*/
static loadFavorites(type) {
const key = this.getStorageKey(type, 'favorites');
const favorites = localStorage.getItem(key);
return favorites !== null ? JSON.parse(favorites) : [];
}
/**
* Save UI state to localStorage
* @param {Object} state - UI state object
* @param {string} type - 'loras' or 'checkpoints'
*/
static saveUIState(state, type) {
const key = this.getStorageKey(type, 'uiState');
localStorage.setItem(key, JSON.stringify(state));
}
/**
* Load UI state from localStorage
* @param {string} type - 'loras' or 'checkpoints'
* @returns {Object} UI state object
*/
static loadUIState(type) {
const key = this.getStorageKey(type, 'uiState');
const state = localStorage.getItem(key);
return state !== null ? JSON.parse(state) : {};
}
/**
* Save individual UI state properties
* @param {string} property - Property name
* @param {any} value - Property value
* @param {string} type - 'loras' or 'checkpoints'
*/
static saveUIProperty(property, value, type) {
const key = this.getStorageKey(type, property);
localStorage.setItem(key, JSON.stringify(value));
}
/**
* Load individual UI state properties
* @param {string} property - Property name
* @param {string} type - 'loras' or 'checkpoints'
* @param {any} defaultValue - Default value if not found
* @returns {any} Property value or default
*/
static loadUIProperty(property, type, defaultValue = null) {
const key = this.getStorageKey(type, property);
const value = localStorage.getItem(key);
return value !== null ? JSON.parse(value) : defaultValue;
}
}
// ============================================================================
// MODEL DATA MANAGER
// ============================================================================
class ModelDataManager {
constructor() {
this.caches = {
loras: null,
checkpoints: null
};
this.lastLoadTimes = {
loras: 0,
checkpoints: 0
};
this.previewLoadPromises = {
loras: null,
checkpoints: null
};
this.previewAvailabilityCaches = {
loras: new Set(),
checkpoints: new Set()
};
}
/**
* Get items (LoRAs or Models) with caching
* @param {string} type - 'loras' or 'checkpoints'
* @param {boolean} forceRefresh - Force refresh from API
* @returns {Promise<Array>} Array of items with name and mtime
*/
async getItems(type = 'loras', forceRefresh = false) {
// Check if we have cached data and it's not expired
const now = Date.now();
const cacheKey = type;
const storageKey = UIManager.getStorageKey(type, 'Cache');
if (!forceRefresh && this.caches[cacheKey]) {
const cachedData = localStorage.getItem(storageKey);
if (cachedData) {
try {
const parsed = JSON.parse(cachedData);
if (parsed && parsed.length > 0) {
return this.caches[cacheKey];
}
} catch (e) {
console.warn(`[${type.toUpperCase()}] Failed to parse cached data:`, e);
}
}
}
// Clear cache if force refresh is requested
if (forceRefresh) {
this.caches[cacheKey] = null;
localStorage.removeItem(storageKey);
}
try {
let itemsData;
// Use appropriate rgthreeApi method
if (type === 'checkpoints') {
itemsData = await wanvidApi.getCheckpoints(forceRefresh);
} else {
itemsData = await wanvidApi.getLoras(forceRefresh);
}
// Convert rgthreeApi format to our expected format
if (!itemsData || !Array.isArray(itemsData)) {
this.caches[cacheKey] = [];
} else {
this.caches[cacheKey] = itemsData
.filter(item => item != null)
.map(item => {
if (typeof item === 'string') {
return { name: item, mtime: 0 };
}
return {
name: item.name || item.filename || item.file || JSON.stringify(item),
mtime: item.mtime || item.modified || item.modified_time || 0
};
});
}
// Cache the data in localStorage
localStorage.setItem(storageKey, JSON.stringify(this.caches[cacheKey]));
this.lastLoadTimes[cacheKey] = now;
return this.caches[cacheKey];
} catch (error) {
console.error(`[${type.toUpperCase()}] Failed to fetch items:`, error);
return [];
}
}
/**
* Get preview availability with caching and debouncing
* @param {string} type - 'loras' or 'checkpoints'
* @returns {Promise<Set>} Set of item names that have previews
*/
async getPreviewAvailability(type = 'loras') {
// Prevent multiple simultaneous calls only
const cacheKey = type;
if (this.previewLoadPromises[cacheKey]) {
console.log(`[Preview] Preview load already in progress for ${type}, waiting...`);
return this.previewLoadPromises[cacheKey];
}
this.previewLoadPromises[cacheKey] = this._loadPreviewAvailabilityInternal(type);
try {
const result = await this.previewLoadPromises[cacheKey];
return result;
} finally {
this.previewLoadPromises[cacheKey] = null;
}
}
/**
* Internal method to load preview availability
* @param {string} type - 'loras' or 'checkpoints'
* @returns {Promise<Set>} Set of item names that have previews
*/
async _loadPreviewAvailabilityInternal(type) {
try {
const apiParams = DirectoryStructureManager.getApiParams(type);
const url = `/wanvid/api/loras/previews${apiParams.isModel ? '?is_model=true' : ''}`;
const response = await fetch(url);
if (response.ok) {
const data = await response.json();
if (data.status === 200 && data.previews && Array.isArray(data.previews)) {
this.previewAvailabilityCaches[type].clear();
data.previews.forEach(preview => {
if (preview && preview.lora) {
this.previewAvailabilityCaches[type].add(preview.lora);
}
});
return this.previewAvailabilityCaches[type];
}
} else {
console.warn(`[Preview] API returned non-OK status for ${type}:`, response.status);
}
} catch (error) {
console.error(`[Preview] Error loading preview availability for ${type}:`, error);
// Don't clear cache on error, keep existing data
}
return this.previewAvailabilityCaches[type];
}
/**
* Check if an item has preview available
* @param {string} itemName - Name of the item
* @param {string} type - 'loras' or 'checkpoints'
* @returns {boolean} True if preview is available
*/
hasPreview(itemName, type = 'loras') {
return this.previewAvailabilityCaches[type].has(itemName);
}
/**
* Save a preview image for an item
* @param {File} imageFile - The image file to save
* @param {string} itemName - Name of the item
* @param {string} type - 'loras' or 'checkpoints'
* @param {string} suffix - Optional suffix for the image
* @param {string} subfolder - Optional subfolder path
* @returns {Promise<Object>} Response from the API
*/
async savePreviewImage(imageFile, itemName, type, suffix = '', subfolder = '') {
const formData = new FormData();
formData.append('image', imageFile);
if (type === 'checkpoints') {
formData.append('model_name', itemName);
formData.append('model_path', subfolder);
} else {
formData.append('lora_name', itemName);
formData.append('lora_path', subfolder);
}
formData.append('suffix', suffix);
formData.append('is_model', type === 'checkpoints' ? 'true' : 'false');
const response = await fetch('/wanvid/api/lora/preview-image', {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Failed to save preview image: ${response.statusText}`);
}
return response.json();
}
/**
* Save JSON data for an item
* @param {Object} jsonData - JSON data to save
* @param {string} itemName - Name of the item
* @param {string} type - 'loras' or 'checkpoints'
* @param {string} subfolder - Optional subfolder path
* @returns {Promise<Object>} Response from the API
*/
async saveJsonData(jsonData, itemName, type, subfolder = '') {
const formData = new FormData();
formData.append('json', JSON.stringify(jsonData));
formData.append(type === 'checkpoints' ? 'model_name' : 'lora_name', itemName);
formData.append(type === 'checkpoints' ? 'model_path' : 'lora_path', subfolder);
const endpoint = type === 'checkpoints' ?
'/wanvid/api/model/preview-json' :
'/wanvid/api/lora/preview-json';
const response = await fetch(endpoint, {
method: 'POST',
body: formData
});
if (!response.ok) {
throw new Error(`Failed to save JSON data: ${response.statusText}`);
}
return response.json();
}
/**
* Get preview image URL
* @param {string} itemName - Name of the item (without extension)
* @param {string} type - 'loras' or 'checkpoints'
* @param {string} suffix - Optional suffix
* @param {string} subfolder - Optional subfolder
* @returns {string} Preview image URL
*/
getPreviewImageUrl(itemName, type, suffix = '', subfolder = '') {
const apiParams = DirectoryStructureManager.getApiParams(type);
const url = new URL('/wanvid/api/loras/preview', window.location.origin);
url.searchParams.set('file', itemName);
if (subfolder) {
url.searchParams.set('subfolder', subfolder);
}
if (suffix) {
url.searchParams.set('suffix', suffix);
}
if (apiParams.isModel) {
url.searchParams.set('is_model', 'true');
}
const finalUrl = url.toString();
return finalUrl;
}
/**
* Get JSON data for an item
* @param {string} itemName - Name of the item (without extension)
* @param {string} type - 'loras' or 'checkpoints'
* @param {string} subfolder - Optional subfolder
* @returns {Promise<Object|null>} JSON data or null if not found
*/
async getJsonData(itemName, type, subfolder = '') {
const apiParams = DirectoryStructureManager.getApiParams(type);
const jsonFilename = itemName + '.json';
const attempts = [
// Method 1: Try the modified preview endpoint that now supports JSON
`/wanvid/api/loras/preview?file=${jsonFilename}${subfolder ? '&subfolder=' + subfolder : ''}${apiParams.isModel ? '&is_model=true' : ''}`,
];
// Add type-specific fallbacks
if (apiParams.isModel) {
attempts.push(
`/api/view?filename=${jsonFilename}&subfolder=_power_preview&type=models`,
`/api/view?filename=${jsonFilename}&subfolder=_power_preview/${subfolder}&type=models`,
`/api/view?filename=${jsonFilename}&subfolder=_power_preview&type=models`
);
} else {
attempts.push(
`/api/view?filename=${jsonFilename}&subfolder=${subfolder}/_power_preview&type=models`,
`/api/view?filename=${jsonFilename}&subfolder=_power_preview/${subfolder}&type=models`,
`/api/view?filename=${jsonFilename}&subfolder=_power_preview&type=models`
);
}
for (const attempt of attempts) {
try {
const response = await fetch(attempt);
if (response.ok) {
const jsonData = await response.json();
return jsonData;
}
} catch (attemptError) {
console.log(`[PullInfo Debug] JSON attempt failed: ${attemptError}`);
continue;
}
}
return null;
}
/**
* Clear all caches
* @param {string} type - 'loras', 'checkpoints', or 'all'
*/
clearCaches(type = 'all') {
if (type === 'all' || type === 'loras') {
this.caches.loras = null;
this.previewAvailabilityCaches.loras.clear();
localStorage.removeItem(UIManager.getStorageKey('loras', 'Cache'));
}
if (type === 'all' || type === 'checkpoints') {
this.caches.checkpoints = null;
this.previewAvailabilityCaches.checkpoints.clear();
localStorage.removeItem(UIManager.getStorageKey('checkpoints', 'Cache'));
}
}
}
// ============================================================================
// EXPORTS
// ============================================================================
// Create singleton instance
const modelDataManager = new ModelDataManager();
export {
ModelDataManager,
UIManager,
DirectoryStructureManager,
modelDataManager
};