mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
checkpoint
This commit is contained in:
@@ -1,16 +0,0 @@
|
|||||||
import { toggleTheme, initTheme } from './utils/uiHelpers.js';
|
|
||||||
import { modalManager } from './managers/ModalManager.js';
|
|
||||||
import { updateService } from './managers/UpdateService.js';
|
|
||||||
import { SettingsManager } from './managers/SettingsManager.js';
|
|
||||||
|
|
||||||
// Export common functions
|
|
||||||
export function initializeCommonComponents() {
|
|
||||||
modalManager.initialize();
|
|
||||||
updateService.initialize();
|
|
||||||
initTheme();
|
|
||||||
|
|
||||||
// Initialize common controls
|
|
||||||
window.toggleTheme = toggleTheme;
|
|
||||||
window.modalManager = modalManager;
|
|
||||||
window.settingsManager = new SettingsManager();
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { updateService } from '../managers/UpdateService.js';
|
import { updateService } from '../managers/UpdateService.js';
|
||||||
|
import { toggleTheme } from '../utils/uiHelpers.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Header.js - Manages the application header behavior across different pages
|
* Header.js - Manages the application header behavior across different pages
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
import { state } from '../state/index.js';
|
import { state } from '../state/index.js';
|
||||||
|
import { modalManager } from '../managers/ModalManager.js';
|
||||||
import { NSFW_LEVELS } from '../utils/constants.js';
|
import { NSFW_LEVELS } from '../utils/constants.js';
|
||||||
|
|
||||||
export function showLoraModal(lora) {
|
export function showLoraModal(lora) {
|
||||||
|
|||||||
64
static/js/core.js
Normal file
64
static/js/core.js
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Core application functionality
|
||||||
|
import { state, initSettings } from './state/index.js';
|
||||||
|
import { LoadingManager } from './managers/LoadingManager.js';
|
||||||
|
import { modalManager } from './managers/ModalManager.js';
|
||||||
|
import { updateService } from './managers/UpdateService.js';
|
||||||
|
import { HeaderManager } from './components/Header.js';
|
||||||
|
import { SettingsManager } from './managers/SettingsManager.js';
|
||||||
|
import { showToast, initTheme, initBackToTop, updatePanelPositions } from './utils/uiHelpers.js';
|
||||||
|
|
||||||
|
// Core application class
|
||||||
|
export class AppCore {
|
||||||
|
constructor() {
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize core functionality
|
||||||
|
async initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
// Initialize settings
|
||||||
|
initSettings();
|
||||||
|
|
||||||
|
// Initialize managers
|
||||||
|
state.loadingManager = new LoadingManager();
|
||||||
|
modalManager.initialize();
|
||||||
|
updateService.initialize();
|
||||||
|
window.modalManager = modalManager;
|
||||||
|
window.settingsManager = new SettingsManager();
|
||||||
|
|
||||||
|
// Initialize UI components
|
||||||
|
window.headerManager = new HeaderManager();
|
||||||
|
initTheme();
|
||||||
|
initBackToTop();
|
||||||
|
|
||||||
|
// Set up event listeners
|
||||||
|
window.addEventListener('resize', updatePanelPositions);
|
||||||
|
|
||||||
|
// Initial positioning
|
||||||
|
updatePanelPositions();
|
||||||
|
|
||||||
|
// Mark as initialized
|
||||||
|
this.initialized = true;
|
||||||
|
|
||||||
|
// Return the core instance for chaining
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current page type
|
||||||
|
getPageType() {
|
||||||
|
const body = document.body;
|
||||||
|
return body.dataset.page || 'unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show toast messages
|
||||||
|
showToast(message, type = 'info') {
|
||||||
|
showToast(message, type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export a singleton instance
|
||||||
|
export const appCore = new AppCore();
|
||||||
|
|
||||||
|
// Export common utilities for global use
|
||||||
|
export { showToast, modalManager, state };
|
||||||
107
static/js/loras.js
Normal file
107
static/js/loras.js
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { appCore, state } from './core.js';
|
||||||
|
import { showLoraModal, toggleShowcase, scrollToTop } from './components/LoraModal.js';
|
||||||
|
import { loadMoreLoras, fetchCivitai, deleteModel, replacePreview, resetAndReload, refreshLoras } from './api/loraApi.js';
|
||||||
|
import {
|
||||||
|
lazyLoadImages,
|
||||||
|
restoreFolderFilter,
|
||||||
|
toggleFolder,
|
||||||
|
copyTriggerWord,
|
||||||
|
openCivitai,
|
||||||
|
toggleFolderTags,
|
||||||
|
initFolderTagsVisibility,
|
||||||
|
updatePanelPositions
|
||||||
|
} from './utils/uiHelpers.js';
|
||||||
|
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
||||||
|
import { showDeleteModal, confirmDelete, closeDeleteModal } from './utils/modalUtils.js';
|
||||||
|
import { DownloadManager } from './managers/DownloadManager.js';
|
||||||
|
import { toggleApiKeyVisibility } from './managers/SettingsManager.js';
|
||||||
|
import { LoraContextMenu } from './components/ContextMenu.js';
|
||||||
|
import { moveManager } from './managers/MoveManager.js';
|
||||||
|
import { createLoraCard, updateCardsForBulkMode } from './components/LoraCard.js';
|
||||||
|
import { bulkManager } from './managers/BulkManager.js';
|
||||||
|
|
||||||
|
// Initialize the LoRA page
|
||||||
|
class LoraPageManager {
|
||||||
|
constructor() {
|
||||||
|
// Add bulk mode to state
|
||||||
|
state.bulkMode = false;
|
||||||
|
state.selectedLoras = new Set();
|
||||||
|
|
||||||
|
// Initialize managers
|
||||||
|
this.downloadManager = new DownloadManager();
|
||||||
|
|
||||||
|
// Expose necessary functions to the page
|
||||||
|
this._exposeGlobalFunctions();
|
||||||
|
}
|
||||||
|
|
||||||
|
_exposeGlobalFunctions() {
|
||||||
|
// Only expose what's needed for the page
|
||||||
|
window.loadMoreLoras = loadMoreLoras;
|
||||||
|
window.fetchCivitai = fetchCivitai;
|
||||||
|
window.deleteModel = deleteModel;
|
||||||
|
window.replacePreview = replacePreview;
|
||||||
|
window.toggleFolder = toggleFolder;
|
||||||
|
window.copyTriggerWord = copyTriggerWord;
|
||||||
|
window.showLoraModal = showLoraModal;
|
||||||
|
window.confirmDelete = confirmDelete;
|
||||||
|
window.closeDeleteModal = closeDeleteModal;
|
||||||
|
window.refreshLoras = refreshLoras;
|
||||||
|
window.openCivitai = openCivitai;
|
||||||
|
window.toggleFolderTags = toggleFolderTags;
|
||||||
|
window.toggleApiKeyVisibility = toggleApiKeyVisibility;
|
||||||
|
window.downloadManager = this.downloadManager;
|
||||||
|
window.moveManager = moveManager;
|
||||||
|
window.toggleShowcase = toggleShowcase;
|
||||||
|
window.scrollToTop = scrollToTop;
|
||||||
|
|
||||||
|
// Bulk operations
|
||||||
|
window.toggleBulkMode = () => bulkManager.toggleBulkMode();
|
||||||
|
window.clearSelection = () => bulkManager.clearSelection();
|
||||||
|
window.toggleCardSelection = (card) => bulkManager.toggleCardSelection(card);
|
||||||
|
window.copyAllLorasSyntax = () => bulkManager.copyAllLorasSyntax();
|
||||||
|
window.updateSelectedCount = () => bulkManager.updateSelectedCount();
|
||||||
|
window.bulkManager = bulkManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
// Initialize page-specific components
|
||||||
|
initializeInfiniteScroll();
|
||||||
|
initializeEventListeners();
|
||||||
|
lazyLoadImages();
|
||||||
|
restoreFolderFilter();
|
||||||
|
initFolderTagsVisibility();
|
||||||
|
new LoraContextMenu();
|
||||||
|
|
||||||
|
// Initialize cards for current bulk mode state (should be false initially)
|
||||||
|
updateCardsForBulkMode(state.bulkMode);
|
||||||
|
|
||||||
|
// Initialize the bulk manager
|
||||||
|
bulkManager.initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize event listeners
|
||||||
|
function initializeEventListeners() {
|
||||||
|
const sortSelect = document.getElementById('sortSelect');
|
||||||
|
if (sortSelect) {
|
||||||
|
sortSelect.value = state.sortBy;
|
||||||
|
sortSelect.addEventListener('change', async (e) => {
|
||||||
|
state.sortBy = e.target.value;
|
||||||
|
await resetAndReload();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.folder-tags .tag').forEach(tag => {
|
||||||
|
tag.addEventListener('click', toggleFolder);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize everything when DOM is ready
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
// Initialize core application
|
||||||
|
await appCore.initialize();
|
||||||
|
|
||||||
|
// Initialize page-specific functionality
|
||||||
|
const loraPage = new LoraPageManager();
|
||||||
|
await loraPage.initialize();
|
||||||
|
});
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { debounce } from './utils/debounce.js';
|
|
||||||
import { LoadingManager } from './managers/LoadingManager.js';
|
|
||||||
import { modalManager } from './managers/ModalManager.js';
|
|
||||||
import { updateService } from './managers/UpdateService.js';
|
|
||||||
import { state, initSettings } from './state/index.js';
|
|
||||||
import { initializeCommonComponents } from './common.js';
|
|
||||||
import { showLoraModal } from './components/LoraModal.js';
|
|
||||||
import { toggleShowcase, scrollToTop } from './components/LoraModal.js';
|
|
||||||
import { loadMoreLoras, fetchCivitai, deleteModel, replacePreview, resetAndReload, refreshLoras } from './api/loraApi.js';
|
|
||||||
import {
|
|
||||||
showToast,
|
|
||||||
lazyLoadImages,
|
|
||||||
restoreFolderFilter,
|
|
||||||
initTheme,
|
|
||||||
toggleTheme,
|
|
||||||
toggleFolder,
|
|
||||||
copyTriggerWord,
|
|
||||||
openCivitai,
|
|
||||||
toggleFolderTags,
|
|
||||||
initFolderTagsVisibility,
|
|
||||||
initBackToTop,
|
|
||||||
updatePanelPositions
|
|
||||||
} from './utils/uiHelpers.js';
|
|
||||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
|
||||||
import { showDeleteModal, confirmDelete, closeDeleteModal } from './utils/modalUtils.js';
|
|
||||||
import { DownloadManager } from './managers/DownloadManager.js';
|
|
||||||
import { SettingsManager, toggleApiKeyVisibility } from './managers/SettingsManager.js';
|
|
||||||
import { LoraContextMenu } from './components/ContextMenu.js';
|
|
||||||
import { moveManager } from './managers/MoveManager.js';
|
|
||||||
import { createLoraCard, updateCardsForBulkMode } from './components/LoraCard.js';
|
|
||||||
import { bulkManager } from './managers/BulkManager.js';
|
|
||||||
import { HeaderManager } from './components/Header.js';
|
|
||||||
// Add bulk mode to state
|
|
||||||
state.bulkMode = false;
|
|
||||||
state.selectedLoras = new Set();
|
|
||||||
|
|
||||||
// Export functions to global window object
|
|
||||||
window.loadMoreLoras = loadMoreLoras;
|
|
||||||
window.fetchCivitai = fetchCivitai;
|
|
||||||
window.deleteModel = deleteModel;
|
|
||||||
window.replacePreview = replacePreview;
|
|
||||||
window.toggleTheme = toggleTheme;
|
|
||||||
window.toggleFolder = toggleFolder;
|
|
||||||
window.copyTriggerWord = copyTriggerWord;
|
|
||||||
window.showLoraModal = showLoraModal;
|
|
||||||
window.modalManager = modalManager;
|
|
||||||
window.state = state;
|
|
||||||
window.confirmDelete = confirmDelete;
|
|
||||||
window.closeDeleteModal = closeDeleteModal;
|
|
||||||
window.refreshLoras = refreshLoras;
|
|
||||||
window.openCivitai = openCivitai;
|
|
||||||
window.showToast = showToast
|
|
||||||
window.toggleFolderTags = toggleFolderTags;
|
|
||||||
window.settingsManager = new SettingsManager();
|
|
||||||
window.toggleApiKeyVisibility = toggleApiKeyVisibility;
|
|
||||||
window.moveManager = moveManager;
|
|
||||||
window.toggleShowcase = toggleShowcase;
|
|
||||||
window.scrollToTop = scrollToTop;
|
|
||||||
window.updatePanelPositions = updatePanelPositions;
|
|
||||||
|
|
||||||
// Export bulk manager methods to window
|
|
||||||
window.toggleBulkMode = () => bulkManager.toggleBulkMode();
|
|
||||||
window.clearSelection = () => bulkManager.clearSelection();
|
|
||||||
window.toggleCardSelection = (card) => bulkManager.toggleCardSelection(card);
|
|
||||||
window.copyAllLorasSyntax = () => bulkManager.copyAllLorasSyntax();
|
|
||||||
window.updateSelectedCount = () => bulkManager.updateSelectedCount();
|
|
||||||
window.bulkManager = bulkManager;
|
|
||||||
|
|
||||||
// Initialize everything when DOM is ready
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
|
||||||
// Ensure settings are initialized
|
|
||||||
initSettings();
|
|
||||||
|
|
||||||
initializeCommonComponents();
|
|
||||||
window.headerManager = new HeaderManager();
|
|
||||||
state.loadingManager = new LoadingManager();
|
|
||||||
modalManager.initialize(); // Initialize modalManager after DOM is loaded
|
|
||||||
updateService.initialize(); // Initialize updateService after modalManager
|
|
||||||
window.downloadManager = new DownloadManager(); // Move this after modalManager initialization
|
|
||||||
|
|
||||||
// Initialize state filters from filterManager if available
|
|
||||||
if (window.filterManager && window.filterManager.filters) {
|
|
||||||
state.filters = { ...window.filterManager.filters };
|
|
||||||
}
|
|
||||||
|
|
||||||
initializeInfiniteScroll();
|
|
||||||
initializeEventListeners();
|
|
||||||
lazyLoadImages();
|
|
||||||
restoreFolderFilter();
|
|
||||||
initTheme();
|
|
||||||
initFolderTagsVisibility();
|
|
||||||
initBackToTop();
|
|
||||||
new LoraContextMenu();
|
|
||||||
|
|
||||||
// Initialize cards for current bulk mode state (should be false initially)
|
|
||||||
updateCardsForBulkMode(state.bulkMode);
|
|
||||||
|
|
||||||
// Initialize the bulk manager
|
|
||||||
bulkManager.initialize();
|
|
||||||
|
|
||||||
// Initial positioning
|
|
||||||
updatePanelPositions();
|
|
||||||
|
|
||||||
// Update positions on window resize
|
|
||||||
window.addEventListener('resize', updatePanelPositions);
|
|
||||||
|
|
||||||
// Set the current page for proper context
|
|
||||||
document.body.dataset.page = 'loras';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize event listeners
|
|
||||||
function initializeEventListeners() {
|
|
||||||
const sortSelect = document.getElementById('sortSelect');
|
|
||||||
if (sortSelect) {
|
|
||||||
sortSelect.value = state.sortBy;
|
|
||||||
sortSelect.addEventListener('change', async (e) => {
|
|
||||||
state.sortBy = e.target.value;
|
|
||||||
await resetAndReload();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.querySelectorAll('.folder-tags .tag').forEach(tag => {
|
|
||||||
tag.addEventListener('click', toggleFolder);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { showToast } from '../utils/uiHelpers.js';
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
import { state } from '../state/index.js';
|
||||||
import { resetAndReload } from '../api/loraApi.js';
|
import { resetAndReload } from '../api/loraApi.js';
|
||||||
import { modalManager } from './ModalManager.js';
|
import { modalManager } from './ModalManager.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
// Recipe manager module
|
// Recipe manager module
|
||||||
import { showToast } from './utils/uiHelpers.js';
|
import { appCore } from './core.js';
|
||||||
import { state } from './state/index.js';
|
|
||||||
import { initializeCommonComponents } from './common.js';
|
|
||||||
import { ImportManager } from './managers/ImportManager.js';
|
import { ImportManager } from './managers/ImportManager.js';
|
||||||
import { RecipeCard } from './components/RecipeCard.js';
|
import { RecipeCard } from './components/RecipeCard.js';
|
||||||
import { RecipeModal } from './components/RecipeModal.js';
|
import { RecipeModal } from './components/RecipeModal.js';
|
||||||
import { HeaderManager } from './components/Header.js';
|
|
||||||
|
|
||||||
class RecipeManager {
|
class RecipeManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -19,19 +16,24 @@ class RecipeManager {
|
|||||||
|
|
||||||
// Initialize RecipeModal
|
// Initialize RecipeModal
|
||||||
this.recipeModal = new RecipeModal();
|
this.recipeModal = new RecipeModal();
|
||||||
|
|
||||||
this.init();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
async initialize() {
|
||||||
// Initialize event listeners
|
// Initialize event listeners
|
||||||
this.initEventListeners();
|
this.initEventListeners();
|
||||||
|
|
||||||
// Load initial set of recipes
|
// Load initial set of recipes
|
||||||
this.loadRecipes();
|
await this.loadRecipes();
|
||||||
|
|
||||||
// Set the current page for proper context
|
// Expose necessary functions to the page
|
||||||
document.body.dataset.page = 'recipes';
|
this._exposeGlobalFunctions();
|
||||||
|
}
|
||||||
|
|
||||||
|
_exposeGlobalFunctions() {
|
||||||
|
// Only expose what's needed for the page
|
||||||
|
window.recipeManager = this;
|
||||||
|
window.importRecipes = () => this.importRecipes();
|
||||||
|
window.importManager = this.importManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
initEventListeners() {
|
initEventListeners() {
|
||||||
@@ -103,7 +105,7 @@ class RecipeManager {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading recipes:', error);
|
console.error('Error loading recipes:', error);
|
||||||
showToast('Failed to load recipes', 'error');
|
appCore.showToast('Failed to load recipes', 'error');
|
||||||
} finally {
|
} finally {
|
||||||
// Hide loading indicator
|
// Hide loading indicator
|
||||||
document.body.classList.remove('loading');
|
document.body.classList.remove('loading');
|
||||||
@@ -146,18 +148,13 @@ class RecipeManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize components
|
// Initialize components
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
initializeCommonComponents();
|
// Initialize core application
|
||||||
window.headerManager = new HeaderManager();
|
await appCore.initialize();
|
||||||
window.recipeManager = new RecipeManager();
|
|
||||||
|
|
||||||
// Make importRecipes function available globally
|
// Initialize recipe manager
|
||||||
window.importRecipes = () => {
|
const recipeManager = new RecipeManager();
|
||||||
window.recipeManager.importRecipes();
|
await recipeManager.initialize();
|
||||||
};
|
|
||||||
|
|
||||||
// Expose ImportManager instance globally for the import modal event handlers
|
|
||||||
window.importManager = window.recipeManager.importManager;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Export for use in other modules
|
// Export for use in other modules
|
||||||
|
|||||||
53
static/js/utils/routes.js
Normal file
53
static/js/utils/routes.js
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// API routes configuration
|
||||||
|
export const apiRoutes = {
|
||||||
|
// LoRA routes
|
||||||
|
loras: {
|
||||||
|
list: '/api/loras',
|
||||||
|
detail: (id) => `/api/loras/${id}`,
|
||||||
|
delete: (id) => `/api/loras/${id}`,
|
||||||
|
update: (id) => `/api/loras/${id}`,
|
||||||
|
civitai: (id) => `/api/loras/${id}/civitai`,
|
||||||
|
download: '/api/download-lora',
|
||||||
|
move: '/api/move-lora',
|
||||||
|
scan: '/api/scan-loras'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Recipe routes
|
||||||
|
recipes: {
|
||||||
|
list: '/api/recipes',
|
||||||
|
detail: (id) => `/api/recipes/${id}`,
|
||||||
|
delete: (id) => `/api/recipes/${id}`,
|
||||||
|
update: (id) => `/api/recipes/${id}`,
|
||||||
|
analyze: '/api/analyze-recipe-image',
|
||||||
|
save: '/api/save-recipe'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Checkpoint routes
|
||||||
|
checkpoints: {
|
||||||
|
list: '/api/checkpoints',
|
||||||
|
detail: (id) => `/api/checkpoints/${id}`,
|
||||||
|
delete: (id) => `/api/checkpoints/${id}`,
|
||||||
|
update: (id) => `/api/checkpoints/${id}`
|
||||||
|
},
|
||||||
|
|
||||||
|
// WebSocket routes
|
||||||
|
ws: {
|
||||||
|
fetchProgress: (protocol) => `${protocol}://${window.location.host}/ws/fetch-progress`
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Page routes
|
||||||
|
export const pageRoutes = {
|
||||||
|
loras: '/loras',
|
||||||
|
recipes: '/loras/recipes',
|
||||||
|
checkpoints: '/checkpoints'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper function to get current page type
|
||||||
|
export function getCurrentPageType() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
if (path.includes('/loras/recipes')) return 'recipes';
|
||||||
|
if (path.includes('/checkpoints')) return 'checkpoints';
|
||||||
|
if (path.includes('/loras')) return 'loras';
|
||||||
|
return 'unknown';
|
||||||
|
}
|
||||||
110
templates/base.html
Normal file
110
templates/base.html
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<title>{% block title %}LoRA Manager{% endblock %}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link rel="stylesheet" href="/loras_static/css/style.css">
|
||||||
|
{% block page_css %}{% endblock %}
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
|
||||||
|
crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||||
|
<link rel="icon" type="image/png" sizes="32x32" href="/loras_static/images/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/loras_static/images/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/loras_static/images/site.webmanifest">
|
||||||
|
|
||||||
|
<!-- 预加载关键资源 -->
|
||||||
|
<link rel="preload" href="/loras_static/css/style.css" as="style">
|
||||||
|
{% block preload %}{% endblock %}
|
||||||
|
|
||||||
|
<!-- 优化字体加载 -->
|
||||||
|
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-solid-900.woff2"
|
||||||
|
as="font" type="font/woff2" crossorigin>
|
||||||
|
|
||||||
|
<!-- 添加性能监控 -->
|
||||||
|
<script>
|
||||||
|
performance.mark('page-start');
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
performance.mark('page-end');
|
||||||
|
performance.measure('page-load', 'page-start', 'page-end');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- 添加安全相关的 meta 标签 -->
|
||||||
|
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
|
||||||
|
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
|
||||||
|
|
||||||
|
<!-- 添加资源加载策略 -->
|
||||||
|
<link rel="preconnect" href="https://civitai.com">
|
||||||
|
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 计算滚动条宽度并设置CSS变量
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const scrollDiv = document.createElement('div');
|
||||||
|
scrollDiv.style.cssText = 'width:100px;height:100px;overflow:scroll;position:absolute;top:-9999px;';
|
||||||
|
document.body.appendChild(scrollDiv);
|
||||||
|
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
|
||||||
|
document.body.removeChild(scrollDiv);
|
||||||
|
document.documentElement.style.setProperty('--scrollbar-width', scrollbarWidth + 'px');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% block head_scripts %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body data-page="{% block page_id %}base{% endblock %}">
|
||||||
|
{% include 'components/header.html' %}
|
||||||
|
|
||||||
|
<div class="page-content">
|
||||||
|
{% include 'components/modals.html' %}
|
||||||
|
{% include 'components/loading.html' %}
|
||||||
|
{% include 'components/context_menu.html' %}
|
||||||
|
{% block additional_components %}{% endblock %}
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
{% if is_initializing %}
|
||||||
|
<div class="initialization-notice">
|
||||||
|
<div class="notice-content">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
<h2>{% block init_title %}Initializing{% endblock %}</h2>
|
||||||
|
<p>{% block init_message %}Scanning and building cache. This may take a few minutes...{% endblock %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% block content %}{% endblock %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block overlay %}{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block main_script %}{% endblock %}
|
||||||
|
|
||||||
|
{% if is_initializing %}
|
||||||
|
<script>
|
||||||
|
// 检查初始化状态并设置自动刷新
|
||||||
|
async function checkInitStatus() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('{% block init_check_url %}/api/loras?page=1&page_size=1{% endblock %}');
|
||||||
|
if (response.ok) {
|
||||||
|
// 如果成功获取数据,说明初始化完成,刷新页面
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
// 如果还未完成,继续轮询
|
||||||
|
setTimeout(checkInitStatus, 2000); // 每2秒检查一次
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 如果出错,继续轮询
|
||||||
|
setTimeout(checkInitStatus, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动状态检查
|
||||||
|
checkInitStatus();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block additional_scripts %}{% endblock %}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,104 +1,29 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>LoRA Manager</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="/loras_static/css/style.css">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/loras_static/images/favicon-32x32.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/loras_static/images/favicon-16x16.png">
|
|
||||||
<link rel="manifest" href="/loras_static/images/site.webmanifest">
|
|
||||||
|
|
||||||
<!-- 预加载关键资源 -->
|
|
||||||
<link rel="preload" href="/loras_static/css/style.css" as="style">
|
|
||||||
<link rel="preload" href="/loras_static/js/main.js" as="script" crossorigin="anonymous">
|
|
||||||
|
|
||||||
<!-- 优化字体加载 -->
|
|
||||||
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin>
|
|
||||||
|
|
||||||
<!-- 添加性能监控 -->
|
|
||||||
<script>
|
|
||||||
performance.mark('page-start');
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
performance.mark('page-end');
|
|
||||||
performance.measure('page-load', 'page-start', 'page-end');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- 添加安全相关的 meta 标签 -->
|
|
||||||
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
|
|
||||||
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
|
|
||||||
|
|
||||||
<!-- 添加资源加载策略 -->
|
|
||||||
<link rel="preconnect" href="https://civitai.com">
|
|
||||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
|
||||||
|
|
||||||
<script>
|
{% block title %}LoRA Manager{% endblock %}
|
||||||
// 计算滚动条宽度并设置CSS变量
|
{% block page_id %}loras{% endblock %}
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const scrollDiv = document.createElement('div');
|
|
||||||
scrollDiv.style.cssText = 'width:100px;height:100px;overflow:scroll;position:absolute;top:-9999px;';
|
|
||||||
document.body.appendChild(scrollDiv);
|
|
||||||
const scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
|
|
||||||
document.body.removeChild(scrollDiv);
|
|
||||||
document.documentElement.style.setProperty('--scrollbar-width', scrollbarWidth + 'px');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</head>
|
|
||||||
<body data-page="loras">
|
|
||||||
{% include 'components/header.html' %}
|
|
||||||
|
|
||||||
<div class="page-content">
|
{% block preload %}
|
||||||
{% include 'components/modals.html' %}
|
<link rel="preload" href="/loras_static/js/loras.js" as="script" crossorigin="anonymous">
|
||||||
{% include 'components/loading.html' %}
|
{% endblock %}
|
||||||
{% include 'components/context_menu.html' %}
|
|
||||||
|
|
||||||
<div class="container">
|
{% block init_title %}Initializing LoRA Manager{% endblock %}
|
||||||
{% if is_initializing %}
|
{% block init_message %}Scanning and building LoRA cache. This may take a few minutes...{% endblock %}
|
||||||
<div class="initialization-notice">
|
{% block init_check_url %}/api/loras?page=1&page_size=1{% endblock %}
|
||||||
<div class="notice-content">
|
|
||||||
<div class="loading-spinner"></div>
|
|
||||||
<h2>Initializing LoRA Manager</h2>
|
|
||||||
<p>Scanning and building LoRA cache. This may take a few minutes...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{% include 'components/controls.html' %}
|
|
||||||
<!-- Lora卡片容器 -->
|
|
||||||
<div class="card-grid" id="loraGrid">
|
|
||||||
<!-- Cards will be dynamically inserted here -->
|
|
||||||
</div>
|
|
||||||
<!-- Bulk operations panel will be inserted here by JavaScript -->
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add after the container div -->
|
{% block content %}
|
||||||
<div class="bulk-mode-overlay"></div>
|
{% include 'components/controls.html' %}
|
||||||
|
<!-- Lora卡片容器 -->
|
||||||
|
<div class="card-grid" id="loraGrid">
|
||||||
|
<!-- Cards will be dynamically inserted here -->
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bulk operations panel will be inserted here by JavaScript -->
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<script type="module" src="/loras_static/js/main.js"></script>
|
{% block overlay %}
|
||||||
{% if is_initializing %}
|
<div class="bulk-mode-overlay"></div>
|
||||||
<script>
|
{% endblock %}
|
||||||
// 检查初始化状态并设置自动刷新
|
|
||||||
async function checkInitStatus() {
|
{% block main_script %}
|
||||||
try {
|
<script type="module" src="/loras_static/js/loras.js"></script>
|
||||||
const response = await fetch('/api/loras?page=1&page_size=1');
|
{% endblock %}
|
||||||
if (response.ok) {
|
|
||||||
// 如果成功获取数据,说明初始化完成,刷新页面
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
// 如果还未完成,继续轮询
|
|
||||||
setTimeout(checkInitStatus, 2000); // 每2秒检查一次
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// 如果出错,继续轮询
|
|
||||||
setTimeout(checkInitStatus, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 启动状态检查
|
|
||||||
checkInitStatus();
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,153 +1,98 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "base.html" %}
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>LoRA Recipes</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link rel="stylesheet" href="/loras_static/css/style.css">
|
|
||||||
<link rel="stylesheet" href="/loras_static/css/components/recipe-card.css">
|
|
||||||
<link rel="stylesheet" href="/loras_static/css/components/recipe-modal.css">
|
|
||||||
<link rel="stylesheet" href="/loras_static/css/components/import-modal.css">
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/loras_static/images/favicon-32x32.png">
|
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/loras_static/images/favicon-16x16.png">
|
|
||||||
<link rel="manifest" href="/loras_static/images/site.webmanifest">
|
|
||||||
|
|
||||||
<!-- Preload critical resources -->
|
|
||||||
<link rel="preload" href="/loras_static/css/style.css" as="style">
|
|
||||||
<link rel="preload" href="/loras_static/js/recipes.js" as="script" crossorigin="anonymous">
|
|
||||||
|
|
||||||
<!-- Optimize font loading -->
|
|
||||||
<link rel="preload" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/webfonts/fa-solid-900.woff2" as="font" type="font/woff2" crossorigin>
|
|
||||||
|
|
||||||
<!-- Performance monitoring -->
|
|
||||||
<script>
|
|
||||||
performance.mark('page-start');
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
performance.mark('page-end');
|
|
||||||
performance.measure('page-load', 'page-start', 'page-end');
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Security meta tags -->
|
|
||||||
<meta http-equiv="Cross-Origin-Opener-Policy" content="same-origin">
|
|
||||||
<meta http-equiv="Cross-Origin-Embedder-Policy" content="require-corp">
|
|
||||||
|
|
||||||
<!-- Resource loading strategy -->
|
|
||||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com">
|
|
||||||
</head>
|
|
||||||
<body data-page="recipes">
|
|
||||||
{% include 'components/header.html' %}
|
|
||||||
|
|
||||||
<div class="page-content">
|
{% block title %}LoRA Recipes{% endblock %}
|
||||||
{% include 'components/modals.html' %}
|
{% block page_id %}recipes{% endblock %}
|
||||||
{% include 'components/loading.html' %}
|
|
||||||
{% include 'components/context_menu.html' %}
|
|
||||||
{% include 'components/import_modal.html' %}
|
|
||||||
{% include 'components/recipe_modal.html' %}
|
|
||||||
|
|
||||||
<div class="container">
|
{% block page_css %}
|
||||||
{% if is_initializing %}
|
<link rel="stylesheet" href="/loras_static/css/components/recipe-card.css">
|
||||||
<div class="initialization-notice">
|
<link rel="stylesheet" href="/loras_static/css/components/recipe-modal.css">
|
||||||
<div class="notice-content">
|
<link rel="stylesheet" href="/loras_static/css/components/import-modal.css">
|
||||||
<div class="loading-spinner"></div>
|
{% endblock %}
|
||||||
<h2>Initializing Recipe Manager</h2>
|
|
||||||
<p>Scanning and building recipe cache. This may take a few moments...</p>
|
{% block preload %}
|
||||||
</div>
|
<link rel="preload" href="/loras_static/js/recipes.js" as="script" crossorigin="anonymous">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block additional_components %}
|
||||||
|
{% include 'components/import_modal.html' %}
|
||||||
|
{% include 'components/recipe_modal.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block init_title %}Initializing Recipe Manager{% endblock %}
|
||||||
|
{% block init_message %}Scanning and building recipe cache. This may take a few moments...{% endblock %}
|
||||||
|
{% block init_check_url %}/api/recipes?page=1&page_size=1{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Recipe controls -->
|
||||||
|
<div class="controls">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<div title="Import recipes" class="control-group">
|
||||||
|
<button onclick="importRecipes()"><i class="fas fa-file-import"></i> Import</button>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<!-- Recipe controls - can reuse structure from loras controls -->
|
|
||||||
<div class="controls">
|
|
||||||
<div class="action-buttons">
|
|
||||||
<div title="Import recipes" class="control-group">
|
|
||||||
<button onclick="importRecipes()"><i class="fas fa-file-import"></i> Import</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Recipe grid - similar to lora grid -->
|
|
||||||
<div class="card-grid recipe-grid" id="recipeGrid">
|
|
||||||
{% if recipes and recipes|length > 0 %}
|
|
||||||
{% for recipe in recipes %}
|
|
||||||
<div class="recipe-card" data-file-path="{{ recipe.file_path }}" data-title="{{ recipe.title }}" data-created="{{ recipe.created_date }}">
|
|
||||||
<div class="recipe-indicator" title="Recipe">R</div>
|
|
||||||
<div class="card-preview">
|
|
||||||
<img src="{{ recipe.file_url }}" alt="{{ recipe.title }}">
|
|
||||||
<div class="card-header">
|
|
||||||
<div class="base-model-wrapper">
|
|
||||||
{% if recipe.base_model %}
|
|
||||||
<span class="base-model-label" title="{{ recipe.base_model }}">
|
|
||||||
{{ recipe.base_model }}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<i class="fas fa-share-alt" title="Share Recipe"></i>
|
|
||||||
<i class="fas fa-copy" title="Copy Recipe"></i>
|
|
||||||
<i class="fas fa-trash" title="Delete Recipe"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<div class="model-info">
|
|
||||||
<span class="model-name">{{ recipe.title }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="lora-count" title="Number of LoRAs in this recipe">
|
|
||||||
<i class="fas fa-layer-group"></i> {{ recipe.loras|length }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
|
||||||
<div class="placeholder-message">
|
|
||||||
<p>No recipes found</p>
|
|
||||||
<p>Add recipe images to your recipes folder to see them here.</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Recipe grid -->
|
||||||
|
<div class="card-grid recipe-grid" id="recipeGrid">
|
||||||
|
{% if recipes and recipes|length > 0 %}
|
||||||
|
{% for recipe in recipes %}
|
||||||
|
<div class="recipe-card" data-file-path="{{ recipe.file_path }}" data-title="{{ recipe.title }}" data-created="{{ recipe.created_date }}">
|
||||||
|
<div class="recipe-indicator" title="Recipe">R</div>
|
||||||
|
<div class="card-preview">
|
||||||
|
<img src="{{ recipe.file_url }}" alt="{{ recipe.title }}">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="base-model-wrapper">
|
||||||
|
{% if recipe.base_model %}
|
||||||
|
<span class="base-model-label" title="{{ recipe.base_model }}">
|
||||||
|
{{ recipe.base_model }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<i class="fas fa-share-alt" title="Share Recipe"></i>
|
||||||
|
<i class="fas fa-copy" title="Copy Recipe"></i>
|
||||||
|
<i class="fas fa-trash" title="Delete Recipe"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="model-info">
|
||||||
|
<span class="model-name">{{ recipe.title }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="lora-count" title="Number of LoRAs in this recipe">
|
||||||
|
<i class="fas fa-layer-group"></i> {{ recipe.loras|length }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="placeholder-message">
|
||||||
|
<p>No recipes found</p>
|
||||||
|
<p>Add recipe images to your recipes folder to see them here.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
<script type="module" src="/loras_static/js/recipes.js"></script>
|
{% block main_script %}
|
||||||
{% if is_initializing %}
|
<script type="module" src="/loras_static/js/recipes.js"></script>
|
||||||
<script>
|
{% endblock %}
|
||||||
// Check initialization status and set auto-refresh
|
|
||||||
async function checkInitStatus() {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/recipes?page=1&page_size=1');
|
|
||||||
if (response.ok) {
|
|
||||||
// If data successfully retrieved, initialization is complete, refresh the page
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
// If not yet complete, continue polling
|
|
||||||
setTimeout(checkInitStatus, 2000); // Check every 2 seconds
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// If error, continue polling
|
|
||||||
setTimeout(checkInitStatus, 2000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start status checking
|
|
||||||
checkInitStatus();
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<script>
|
{% block additional_scripts %}
|
||||||
// Refresh recipes
|
<script>
|
||||||
function refreshRecipes() {
|
// Refresh recipes
|
||||||
// Will be implemented in recipes.js
|
function refreshRecipes() {
|
||||||
window.location.reload();
|
// Will be implemented in recipes.js
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Import recipes
|
||||||
|
function importRecipes() {
|
||||||
|
// Show import modal
|
||||||
|
const importModal = document.getElementById('importModal');
|
||||||
|
if (importModal) {
|
||||||
|
importModal.classList.remove('hidden');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Import recipes
|
</script>
|
||||||
function importRecipes() {
|
{% endblock %}
|
||||||
// Show import modal
|
|
||||||
const importModal = document.getElementById('importModal');
|
|
||||||
if (importModal) {
|
|
||||||
importModal.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user