refactor: centralize filename extraction, remote URL normalization, and storage abstractions
This commit is contained in:
+2
-2
@@ -855,7 +855,7 @@ def _resolve_aria2c_executable(settings: Optional[Dict[str, Any]] = None) -> str
|
||||
configured = str(active_settings.get("aria2c_path") or "").strip()
|
||||
candidate = os.path.expandvars(os.path.expanduser(configured or "aria2c"))
|
||||
expected_names = {"aria2c", "aria2c.exe"}
|
||||
candidate_name = os.path.basename(candidate).lower()
|
||||
candidate_name = get_filename_from_path(candidate).lower()
|
||||
has_path_component = bool(
|
||||
os.path.isabs(candidate)
|
||||
or os.path.dirname(candidate)
|
||||
@@ -877,7 +877,7 @@ def _resolve_aria2c_executable(settings: Optional[Dict[str, Any]] = None) -> str
|
||||
|
||||
if candidate_name in expected_names:
|
||||
resolved = shutil.which(candidate)
|
||||
if resolved and os.path.basename(resolved).lower() in expected_names:
|
||||
if resolved and get_filename_from_path(resolved).lower() in expected_names:
|
||||
return os.path.realpath(os.path.abspath(resolved))
|
||||
|
||||
raise Aria2Error(
|
||||
|
||||
+6
-4
@@ -13,6 +13,8 @@ from difflib import SequenceMatcher
|
||||
from .log_system import create_module_logger
|
||||
log = create_module_logger(__name__)
|
||||
|
||||
from .path_utils import get_filename_from_path
|
||||
|
||||
try:
|
||||
from rapidfuzz import fuzz as rapidfuzz_fuzz
|
||||
except ImportError:
|
||||
@@ -121,7 +123,7 @@ def find_matches(
|
||||
|
||||
# Extract just the filename from target_model (remove any subfolder paths)
|
||||
# target_model might be just a filename or might include subfolder paths
|
||||
target_filename = os.path.basename(target_model_normalized)
|
||||
target_filename = get_filename_from_path(target_model_normalized)
|
||||
|
||||
# Normalize target filename once for exact match comparisons
|
||||
target_norm = normalize_filename(target_filename)
|
||||
@@ -135,7 +137,7 @@ def find_matches(
|
||||
# If no filename key, try to extract from path or relative_path
|
||||
if not candidate_filename:
|
||||
if candidate_path:
|
||||
candidate_filename = os.path.basename(candidate_path)
|
||||
candidate_filename = get_filename_from_path(candidate_path)
|
||||
|
||||
if not candidate_filename:
|
||||
continue
|
||||
@@ -402,7 +404,7 @@ def clean_filename_for_search(filename: str) -> str:
|
||||
"""
|
||||
if not filename:
|
||||
return ""
|
||||
basename = os.path.basename(filename).strip()
|
||||
basename = get_filename_from_path(filename).strip()
|
||||
base = os.path.splitext(basename)[0]
|
||||
pattern = r"[-_]?(" + "|".join(PRECISION_FORMAT_SUFFIXES) + r").*$"
|
||||
return re.sub(pattern, "", base, flags=re.IGNORECASE).strip(" -_")
|
||||
@@ -413,7 +415,7 @@ def build_filename_search_queries(filename: str) -> List[str]:
|
||||
Build a list of candidate search queries from a filename by stripping
|
||||
extensions and common precision/format suffixes (e.g. fp16, bf16, pruned).
|
||||
"""
|
||||
basename = os.path.basename(filename or "").strip()
|
||||
basename = get_filename_from_path(filename).strip()
|
||||
if not basename:
|
||||
return []
|
||||
|
||||
|
||||
+3
-3
@@ -12,7 +12,7 @@ from .log_system import create_module_logger
|
||||
log = create_module_logger(__name__)
|
||||
|
||||
|
||||
from .path_utils import get_path_identity
|
||||
from .path_utils import get_path_identity, get_filename_from_path
|
||||
# Import folder_paths lazily - it may not be available until ComfyUI is initialized
|
||||
try:
|
||||
import folder_paths
|
||||
@@ -139,11 +139,11 @@ def scan_directory(
|
||||
try:
|
||||
relative_path = os.path.relpath(root, base_directory)
|
||||
except ValueError:
|
||||
relative_path = os.path.basename(root)
|
||||
relative_path = get_filename_from_path(root)
|
||||
|
||||
models.append(
|
||||
{
|
||||
"filename": os.path.basename(root),
|
||||
"filename": get_filename_from_path(root),
|
||||
"path": root,
|
||||
"relative_path": relative_path,
|
||||
"category": category,
|
||||
|
||||
@@ -45,6 +45,7 @@ from ..type_utils import (
|
||||
normalize_model_file_info,
|
||||
build_search_result,
|
||||
normalize_sha256,
|
||||
prepare_remote_size_probe_url,
|
||||
)
|
||||
from ..progress import report_progress, get_progress_reporter
|
||||
from ..log_system import create_module_logger
|
||||
@@ -541,16 +542,14 @@ def _prepare_size_probe_url(url: Any) -> Optional[str]:
|
||||
if not normalized:
|
||||
return None
|
||||
|
||||
normalized = prepare_remote_size_probe_url(normalized)
|
||||
if not normalized:
|
||||
return None
|
||||
|
||||
parsed = urlparse(normalized)
|
||||
host = parsed.hostname
|
||||
path = parsed.path or ""
|
||||
|
||||
if host_matches_domain(host, "huggingface.co") and "/blob/" in path:
|
||||
normalized = normalized.replace("/blob/", "/resolve/", 1)
|
||||
parsed = urlparse(normalized)
|
||||
host = parsed.hostname
|
||||
path = parsed.path or ""
|
||||
|
||||
if host_matches_domain(host, "civarchive.com") and not path.startswith("/api/download/"):
|
||||
return None
|
||||
if host_matches_domain(host, "civitai.com", "civitai.red") and not path.startswith("/api/download/"):
|
||||
|
||||
+10
-18
@@ -17,9 +17,9 @@ from ..progress import report_progress, get_progress_reporter
|
||||
from ..log_system import create_module_logger
|
||||
log = create_module_logger(__name__)
|
||||
|
||||
from ..path_utils import write_json_atomic, METADATA_DIR, read_json_safe
|
||||
from ..path_utils import write_json_atomic, METADATA_DIR, read_json_safe, get_filename_from_path
|
||||
from ..network_utils import host_matches_domain, request_source_response, request_source_json
|
||||
from ..type_utils import check_credential_http, parse_size_header as _parse_size_header, fetch_remote_file_size, fetch_remote_file_size_cached, clear_remote_size_cache, extract_file_size
|
||||
from ..type_utils import check_credential_http, parse_size_header as _parse_size_header, fetch_remote_file_size, fetch_remote_file_size_cached, clear_remote_size_cache, extract_file_size, prepare_remote_size_probe_url
|
||||
from ..matcher import build_filename_search_queries, clean_filename_for_search
|
||||
|
||||
HF_API_URL = "https://huggingface.co/api"
|
||||
@@ -171,7 +171,7 @@ def _build_author_index_from_models(
|
||||
{
|
||||
"repo_id": repo_id,
|
||||
"path": file_path,
|
||||
"filename": os.path.basename(file_path),
|
||||
"filename": get_filename_from_path(file_path),
|
||||
"size": sibling.get("size"),
|
||||
}
|
||||
)
|
||||
@@ -330,15 +330,7 @@ def get_huggingface_download_url(repo: str, filename: str, branch: str = "main")
|
||||
|
||||
|
||||
def _normalize_huggingface_size_probe_url(url: str) -> Optional[str]:
|
||||
if not isinstance(url, str) or not url.strip():
|
||||
return None
|
||||
value = url.strip()
|
||||
parsed = urlparse(value)
|
||||
if not host_matches_domain(parsed.hostname, "huggingface.co"):
|
||||
return None
|
||||
if "/blob/" in parsed.path:
|
||||
value = value.replace("/blob/", "/resolve/", 1)
|
||||
return value
|
||||
return prepare_remote_size_probe_url(url)
|
||||
|
||||
|
||||
|
||||
@@ -375,7 +367,7 @@ def _build_huggingface_result(
|
||||
return {
|
||||
"source": "huggingface",
|
||||
"repo_id": repo_id,
|
||||
"filename": os.path.basename(file_path),
|
||||
"filename": get_filename_from_path(file_path),
|
||||
"path": file_path,
|
||||
"url": download_url,
|
||||
"size": size,
|
||||
@@ -424,7 +416,7 @@ def _find_matching_file_in_repo(
|
||||
if not file_path:
|
||||
continue
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
file_name = get_filename_from_path(file_path)
|
||||
file_name_lower = file_name.lower()
|
||||
file_base = os.path.splitext(file_name_lower)[0]
|
||||
|
||||
@@ -445,7 +437,7 @@ def _find_matching_file_in_repo(
|
||||
if not file_path:
|
||||
continue
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
file_name = get_filename_from_path(file_path)
|
||||
file_name_lower = file_name.lower()
|
||||
file_base = os.path.splitext(file_name_lower)[0]
|
||||
|
||||
@@ -481,7 +473,7 @@ def _find_matching_file_in_author_index(
|
||||
if not repo_id or not file_path:
|
||||
continue
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
file_name = get_filename_from_path(file_path)
|
||||
file_name_lower = file_name.lower()
|
||||
if file_name_lower == filename_lower or file_path.lower().endswith(
|
||||
filename_lower
|
||||
@@ -503,7 +495,7 @@ def _find_matching_file_in_author_index(
|
||||
if not repo_id or not file_path:
|
||||
continue
|
||||
|
||||
file_name = os.path.basename(file_path)
|
||||
file_name = get_filename_from_path(file_path)
|
||||
file_name_lower = file_name.lower()
|
||||
file_base = os.path.splitext(file_name_lower)[0]
|
||||
file_path_lower = file_path.lower()
|
||||
@@ -1021,7 +1013,7 @@ def get_repo_files(repo: str, token: Optional[str] = None) -> List[Dict[str, Any
|
||||
size = _fetch_remote_file_size_bytes(url, headers=headers)
|
||||
files.append(
|
||||
{
|
||||
"filename": os.path.basename(path),
|
||||
"filename": get_filename_from_path(path),
|
||||
"path": path,
|
||||
"url": url,
|
||||
"size": size,
|
||||
|
||||
@@ -19,6 +19,7 @@ from ..matcher import (
|
||||
calculate_candidate_rank,
|
||||
should_update_best_match,
|
||||
)
|
||||
from ..path_utils import get_filename_from_path
|
||||
from ..type_utils import (
|
||||
select_primary_model_file,
|
||||
get_generic_filename_tokens,
|
||||
@@ -113,7 +114,7 @@ def _normalize_model_type(model_type: Optional[str]) -> str:
|
||||
|
||||
def _extract_search_tokens(query: str) -> Dict[str, List[str]]:
|
||||
"""Split filename/model query into useful content/version tokens."""
|
||||
base_query = os.path.splitext(os.path.basename(query or ""))[0]
|
||||
base_query = os.path.splitext(get_filename_from_path(query))[0]
|
||||
raw_tokens = [
|
||||
token.lower()
|
||||
for token in re.split(r"[^a-zA-Z0-9]+", base_query)
|
||||
@@ -662,7 +663,7 @@ def search_lora_manager_archive_for_file(
|
||||
"""
|
||||
Search the archive for the best model/version match for a filename.
|
||||
"""
|
||||
search_query = os.path.splitext(os.path.basename(filename or ""))[0]
|
||||
search_query = os.path.splitext(get_filename_from_path(filename))[0]
|
||||
if not search_query:
|
||||
return None
|
||||
|
||||
|
||||
+31
-2
@@ -605,6 +605,7 @@ def looks_like_model_file(url: str, expected_filename: str = "") -> bool:
|
||||
import os
|
||||
from urllib.parse import unquote, urlparse
|
||||
from .network_utils import host_matches_domain
|
||||
from .path_utils import get_filename_from_path
|
||||
|
||||
text = str(url or "").strip()
|
||||
if not text.startswith(("http://", "https://", "hf://")):
|
||||
@@ -628,8 +629,8 @@ def looks_like_model_file(url: str, expected_filename: str = "") -> bool:
|
||||
) and "/api/download/" in path:
|
||||
return True
|
||||
|
||||
basename = os.path.basename(path).lower()
|
||||
expected = os.path.basename(str(expected_filename or "")).lower()
|
||||
basename = get_filename_from_path(path).lower()
|
||||
expected = get_filename_from_path(str(expected_filename or "")).lower()
|
||||
if expected and basename == expected:
|
||||
return True
|
||||
|
||||
@@ -829,6 +830,34 @@ def extract_trained_words(*values: Any) -> List[str]:
|
||||
_remote_size_cache: Dict[tuple[str, Optional[str]], Optional[int]] = {}
|
||||
|
||||
|
||||
def prepare_remote_size_probe_url(url: Any) -> Optional[str]:
|
||||
"""
|
||||
Normalize a remote URL to be suitable for file size probing.
|
||||
Converts HuggingFace '/blob/' URLs to '/resolve/' URLs.
|
||||
"""
|
||||
from urllib.parse import urlparse
|
||||
from .network_utils import host_matches_domain
|
||||
|
||||
if not isinstance(url, str) or not url.strip():
|
||||
return None
|
||||
|
||||
value = url.strip()
|
||||
if value.startswith("//"):
|
||||
value = f"https:{value}"
|
||||
|
||||
try:
|
||||
parsed = urlparse(value)
|
||||
host = parsed.hostname
|
||||
path = parsed.path or ""
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if host and host_matches_domain(host, "huggingface.co") and "/blob/" in path:
|
||||
value = value.replace("/blob/", "/resolve/", 1)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def fetch_remote_file_size_cached(
|
||||
url: str,
|
||||
headers: Optional[Dict[str, str]] = None,
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Dict, Any, List, Optional
|
||||
|
||||
from .log_system import create_module_logger
|
||||
log = create_module_logger(__name__)
|
||||
from .path_utils import is_path_within
|
||||
from .path_utils import is_path_within, get_filename_from_path
|
||||
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ def convert_to_relative_path(
|
||||
pass
|
||||
|
||||
# Fallback: return just the filename
|
||||
return os.path.basename(absolute_path)
|
||||
return get_filename_from_path(absolute_path)
|
||||
|
||||
|
||||
def get_base_directory_for_model(
|
||||
|
||||
@@ -212,6 +212,14 @@ class TestRefactoringUnification(unittest.TestCase):
|
||||
self.assertEqual(state["status"], "running")
|
||||
self.assertEqual(state["percent"], 50)
|
||||
|
||||
def test_get_filename_from_path(self):
|
||||
from core.path_utils import get_filename_from_path
|
||||
self.assertEqual(get_filename_from_path("some/path/to/model.safetensors"), "model.safetensors")
|
||||
self.assertEqual(get_filename_from_path("some\\path\\to\\model.safetensors"), "model.safetensors")
|
||||
self.assertEqual(get_filename_from_path("model.safetensors"), "model.safetensors")
|
||||
self.assertEqual(get_filename_from_path(""), "")
|
||||
self.assertEqual(get_filename_from_path(None), "")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createModuleLogger } from "../../log_system/log_funcs.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
import { getCivitaiModelUrl } from "../globals.js";
|
||||
import { normalizePathIdentity } from "../utils/html_utils.js";
|
||||
|
||||
const log = createModuleLogger('resolve_download_methods');
|
||||
|
||||
@@ -595,11 +596,7 @@ export const resolveDownloadMethods = {
|
||||
},
|
||||
|
||||
normalizeLocalMatchPathIdentity(value = '') {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/\/+/g, '/')
|
||||
.toLowerCase();
|
||||
return normalizePathIdentity(value);
|
||||
},
|
||||
|
||||
getLocalMatchAbsolutePathIdentity(match = {}) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
|
||||
import { api } from "../../../../scripts/api.js";
|
||||
import { safeStorage } from "./utils/html_utils.js";
|
||||
import { dialogShellMethods } from "./shell/dialog_shell_methods.js";
|
||||
import { lifecycleGraphMethods } from "./shell/lifecycle_graph_methods.js";
|
||||
import { workflowStateMethods } from "./shell/workflow_state_methods.js";
|
||||
@@ -73,24 +74,12 @@ export class ResolverManagerDialog extends ComfyDialog {
|
||||
this.showResolvedModelsStorageKey = "model_resolver_show_resolved_models";
|
||||
this.showAutoDownloadModelsStorageKey = "model_resolver_show_auto_download_models";
|
||||
this.localMatchAlternativesCollapsedStorageKey = "model_resolver_local_match_alternatives_collapsed";
|
||||
try {
|
||||
this.showResolvedModels = localStorage.getItem(this.showResolvedModelsStorageKey) === '1';
|
||||
} catch (e) {
|
||||
this.showResolvedModels = false;
|
||||
}
|
||||
try {
|
||||
const storedShowAutoDownload = localStorage.getItem(this.showAutoDownloadModelsStorageKey);
|
||||
this.showAutoDownloadModels = storedShowAutoDownload === null
|
||||
? true
|
||||
: storedShowAutoDownload === '1';
|
||||
} catch (e) {
|
||||
this.showAutoDownloadModels = true;
|
||||
}
|
||||
try {
|
||||
this.localMatchAlternativesCollapsed = localStorage.getItem(this.localMatchAlternativesCollapsedStorageKey) === '1';
|
||||
} catch (e) {
|
||||
this.localMatchAlternativesCollapsed = false;
|
||||
}
|
||||
this.showResolvedModels = safeStorage.getItem(this.showResolvedModelsStorageKey) === '1';
|
||||
const storedShowAutoDownload = safeStorage.getItem(this.showAutoDownloadModelsStorageKey);
|
||||
this.showAutoDownloadModels = storedShowAutoDownload === null
|
||||
? true
|
||||
: storedShowAutoDownload === '1';
|
||||
this.localMatchAlternativesCollapsed = safeStorage.getItem(this.localMatchAlternativesCollapsedStorageKey) === '1';
|
||||
this.dockButton = null;
|
||||
this.undockButton = null;
|
||||
this._floatingRectBeforeDock = null;
|
||||
|
||||
@@ -3,6 +3,8 @@ import { api } from "../../../../../scripts/api.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { createFloatingTreePicker } from "../utils/tree_picker.js";
|
||||
import { safeStorage } from "../utils/html_utils.js";
|
||||
const localStorage = safeStorage;
|
||||
export const downloadTargetMethods = {
|
||||
/**
|
||||
* Ensure all models are loaded for the dropdown.
|
||||
|
||||
@@ -4,6 +4,8 @@ import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
import { getCivitaiModelUrl } from "../globals.js";
|
||||
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
|
||||
const localStorage = safeStorage;
|
||||
export const searchPanelMethods = {
|
||||
/**
|
||||
* Build stable cache key for a missing model entry
|
||||
@@ -2458,7 +2460,7 @@ export const searchPanelMethods = {
|
||||
|
||||
getActiveDownloadInfoForLocalMatch(match = {}) {
|
||||
const model = match.model || {};
|
||||
const normalizePath = (value = '') => this.normalizeLocalMatchPathIdentity?.(value) || String(value || '').trim().replace(/\\/g, '/').replace(/\/+/g, '/').toLowerCase();
|
||||
const normalizePath = (value = '') => normalizePathIdentity(value);
|
||||
const joinPath = (...parts) => normalizePath(parts.filter(Boolean).join('/'));
|
||||
const activeStatuses = new Set(['starting', 'downloading', 'paused', 'cancelling']);
|
||||
const matchAbsolutePaths = [
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { app } from "../../../../../scripts/app.js";
|
||||
import { app } from "../../../../../scripts/app.js";
|
||||
import { api } from "../../../../../scripts/api.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { safeStorage } from "../utils/html_utils.js";
|
||||
export const dialogShellMethods = {
|
||||
createHeader() {
|
||||
// Create tabs
|
||||
@@ -141,22 +142,18 @@ export const dialogShellMethods = {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const wh = JSON.parse(localStorage.getItem('model_resolver_modal_size_before_fs') || 'null');
|
||||
if (wh?.w && wh?.h) {
|
||||
el.style.width = `${wh.w}px`;
|
||||
el.style.height = `${wh.h}px`;
|
||||
}
|
||||
const wh = JSON.parse(safeStorage.getItem('model_resolver_modal_size_before_fs') || 'null');
|
||||
if (wh?.w && wh?.h) {
|
||||
el.style.width = `${wh.w}px`;
|
||||
el.style.height = `${wh.h}px`;
|
||||
}
|
||||
|
||||
const pos = JSON.parse(localStorage.getItem('model_resolver_modal_pos') || 'null');
|
||||
if (pos && Number.isFinite(pos.top) && Number.isFinite(pos.left)) {
|
||||
el.style.top = `${pos.top}px`;
|
||||
el.style.left = `${pos.left}px`;
|
||||
el.style.transform = 'none';
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
// Fall back to centered floating geometry below.
|
||||
const pos = JSON.parse(safeStorage.getItem('model_resolver_modal_pos') || 'null');
|
||||
if (pos && Number.isFinite(pos.top) && Number.isFinite(pos.left)) {
|
||||
el.style.top = `${pos.top}px`;
|
||||
el.style.left = `${pos.left}px`;
|
||||
el.style.transform = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.top = '50%';
|
||||
@@ -165,17 +162,11 @@ export const dialogShellMethods = {
|
||||
},
|
||||
|
||||
rememberSidebarOpenMode(mode) {
|
||||
try {
|
||||
localStorage.setItem(this.sidebarOpenModeStorageKey, mode === 'floating' ? 'floating' : 'docked');
|
||||
} catch (e) {}
|
||||
safeStorage.setItem(this.sidebarOpenModeStorageKey, mode === 'floating' ? 'floating' : 'docked');
|
||||
},
|
||||
|
||||
shouldOpenFromSidebarFloating() {
|
||||
try {
|
||||
return localStorage.getItem(this.sidebarOpenModeStorageKey) === 'floating';
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return safeStorage.getItem(this.sidebarOpenModeStorageKey) === 'floating';
|
||||
},
|
||||
|
||||
isVisible() {
|
||||
@@ -415,10 +406,8 @@ export const dialogShellMethods = {
|
||||
const btn = document.getElementById('model-resolver-fullscreen-toggle');
|
||||
if (enable) {
|
||||
// Save current size
|
||||
try {
|
||||
const rect = el.getBoundingClientRect();
|
||||
localStorage.setItem('model_resolver_modal_size_before_fs', JSON.stringify({ w: Math.round(rect.width), h: Math.round(rect.height) }));
|
||||
} catch (e) {}
|
||||
const rect = el.getBoundingClientRect();
|
||||
safeStorage.setItem('model_resolver_modal_size_before_fs', JSON.stringify({ w: Math.round(rect.width), h: Math.round(rect.height) }));
|
||||
el.style.top = '0';
|
||||
el.style.left = '0';
|
||||
el.style.transform = 'none';
|
||||
@@ -433,7 +422,7 @@ export const dialogShellMethods = {
|
||||
btn.setAttribute('aria-label', 'Exit full screen');
|
||||
this.setTooltip(btn, 'Exit full screen');
|
||||
}
|
||||
try { localStorage.setItem('model_resolver_modal_fullscreen', '1'); } catch (e) {}
|
||||
safeStorage.setItem('model_resolver_modal_fullscreen', '1');
|
||||
} else {
|
||||
this.returnToDockedAfterFullscreen = false;
|
||||
// Restore centered sizing
|
||||
@@ -443,7 +432,7 @@ export const dialogShellMethods = {
|
||||
el.style.resize = 'both';
|
||||
// Restore saved pre-FS size if available
|
||||
let wh = null;
|
||||
try { wh = JSON.parse(localStorage.getItem('model_resolver_modal_size_before_fs') || 'null'); } catch (e) {}
|
||||
try { wh = JSON.parse(safeStorage.getItem('model_resolver_modal_size_before_fs') || 'null'); } catch (e) {}
|
||||
if (wh && wh.w && wh.h) {
|
||||
el.style.width = `${wh.w}px`;
|
||||
el.style.height = `${wh.h}px`;
|
||||
@@ -452,18 +441,12 @@ export const dialogShellMethods = {
|
||||
el.style.height = '700px';
|
||||
}
|
||||
// Restore last known position if available, else center
|
||||
try {
|
||||
const pos = JSON.parse(localStorage.getItem('model_resolver_modal_pos') || 'null');
|
||||
if (pos && Number.isFinite(pos.top) && Number.isFinite(pos.left)) {
|
||||
el.style.top = `${pos.top}px`;
|
||||
el.style.left = `${pos.left}px`;
|
||||
el.style.transform = 'none';
|
||||
} else {
|
||||
el.style.top = '50%';
|
||||
el.style.left = '50%';
|
||||
el.style.transform = 'translate(-50%, -50%)';
|
||||
}
|
||||
} catch (e) {
|
||||
const pos = JSON.parse(safeStorage.getItem('model_resolver_modal_pos') || 'null');
|
||||
if (pos && Number.isFinite(pos.top) && Number.isFinite(pos.left)) {
|
||||
el.style.top = `${pos.top}px`;
|
||||
el.style.left = `${pos.left}px`;
|
||||
el.style.transform = 'none';
|
||||
} else {
|
||||
el.style.top = '50%';
|
||||
el.style.left = '50%';
|
||||
el.style.transform = 'translate(-50%, -50%)';
|
||||
@@ -474,7 +457,7 @@ export const dialogShellMethods = {
|
||||
this.setTooltip(btn, 'Enter full screen');
|
||||
}
|
||||
this.ensureModalHandleInViewport({ persist: true });
|
||||
try { localStorage.setItem('model_resolver_modal_fullscreen', '0'); } catch (e) {}
|
||||
safeStorage.setItem('model_resolver_modal_fullscreen', '0');
|
||||
|
||||
if (shouldReturnToDocked) {
|
||||
this.dockToSidebar();
|
||||
@@ -518,12 +501,10 @@ export const dialogShellMethods = {
|
||||
saveModalPosition() {
|
||||
if (this.docked) return;
|
||||
|
||||
try {
|
||||
const el = this.element;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
localStorage.setItem('model_resolver_modal_pos', JSON.stringify({ top: Math.round(rect.top), left: Math.round(rect.left) }));
|
||||
} catch (e) { /* ignore */ }
|
||||
const el = this.element;
|
||||
if (!el) return;
|
||||
const rect = el.getBoundingClientRect();
|
||||
safeStorage.setItem('model_resolver_modal_pos', JSON.stringify({ top: Math.round(rect.top), left: Math.round(rect.left) }));
|
||||
},
|
||||
|
||||
ensureModalHandleInViewport({ persist = false } = {}) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { app } from "../../../../../scripts/app.js";
|
||||
import { api } from "../../../../../scripts/api.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { safeStorage } from "../utils/html_utils.js";
|
||||
export const lifecycleGraphMethods = {
|
||||
async show(workflow = null) {
|
||||
this.undockToFloating({ persist: false });
|
||||
@@ -36,11 +37,8 @@ export const lifecycleGraphMethods = {
|
||||
await this.ensureAllModelsLoaded();
|
||||
await this.ensureDownloadDirectoriesLoaded();
|
||||
|
||||
// Restore fullscreen state if enabled
|
||||
try {
|
||||
const fs = localStorage.getItem('model_resolver_modal_fullscreen');
|
||||
if (!this.docked && restoreFullscreen && fs === '1') this.setFullScreen(true);
|
||||
} catch (e) { }
|
||||
const fs = safeStorage.getItem('model_resolver_modal_fullscreen');
|
||||
if (!this.docked && restoreFullscreen && fs === '1') this.setFullScreen(true);
|
||||
|
||||
// Attach drag handle event listener (only once)
|
||||
this.attachDragHandleIfNeeded();
|
||||
|
||||
@@ -159,3 +159,67 @@ export async function pollBackgroundTask({
|
||||
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
||||
}
|
||||
}
|
||||
|
||||
const memoryStorage = new Map();
|
||||
let isLocalStorageAvailable = null;
|
||||
|
||||
function checkLocalStorage() {
|
||||
if (isLocalStorageAvailable !== null) return isLocalStorageAvailable;
|
||||
try {
|
||||
localStorage.setItem('__test_storage__', '1');
|
||||
localStorage.removeItem('__test_storage__');
|
||||
isLocalStorageAvailable = true;
|
||||
} catch (e) {
|
||||
isLocalStorageAvailable = false;
|
||||
console.warn('LocalStorage is not available, falling back to memory storage.', e);
|
||||
}
|
||||
return isLocalStorageAvailable;
|
||||
}
|
||||
|
||||
export const safeStorage = {
|
||||
getItem(key, defaultValue = null) {
|
||||
if (checkLocalStorage()) {
|
||||
try {
|
||||
const val = localStorage.getItem(key);
|
||||
return val !== null ? val : defaultValue;
|
||||
} catch (e) {
|
||||
console.warn(`safeStorage.getItem failed for key ${key}:`, e);
|
||||
}
|
||||
}
|
||||
return memoryStorage.has(key) ? memoryStorage.get(key) : defaultValue;
|
||||
},
|
||||
setItem(key, value) {
|
||||
const valStr = String(value);
|
||||
if (checkLocalStorage()) {
|
||||
try {
|
||||
localStorage.setItem(key, valStr);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn(`safeStorage.setItem failed for key ${key}:`, e);
|
||||
}
|
||||
}
|
||||
memoryStorage.set(key, valStr);
|
||||
return true;
|
||||
},
|
||||
removeItem(key) {
|
||||
if (checkLocalStorage()) {
|
||||
try {
|
||||
localStorage.removeItem(key);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.warn(`safeStorage.removeItem failed for key ${key}:`, e);
|
||||
}
|
||||
}
|
||||
memoryStorage.delete(key);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
export function normalizePathIdentity(value = '') {
|
||||
return String(value || '')
|
||||
.trim()
|
||||
.replace(/[\\/]+/g, '/')
|
||||
.replace(/\/+$/g, '')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ import { createFloatingTreePicker } from "../utils/tree_picker.js";
|
||||
import { showNotification as showNotificationUtils } from "../../utils/notification_utils.js";
|
||||
import { startSplitterDrag } from "../utils/splitter_drag.js";
|
||||
import { getCivitaiModelUrl } from "../globals.js";
|
||||
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
|
||||
const localStorage = safeStorage;
|
||||
|
||||
export const missingBrowserMethods = {
|
||||
getMissingFilename(missing = {}) {
|
||||
@@ -180,10 +182,7 @@ export const missingBrowserMethods = {
|
||||
const seenPaths = new Set();
|
||||
const details = [];
|
||||
|
||||
const normalizePathIdentity = (value = '') => String(value || '')
|
||||
.replace(/[\\/]+/g, '/')
|
||||
.replace(/\/+$/g, '')
|
||||
.toLowerCase();
|
||||
// Uses imported normalizePathIdentity from html_utils.js
|
||||
|
||||
folderKeys.forEach(folderKey => {
|
||||
const normalizedCategory = this.normalizeDownloadCategory?.(folderKey) || folderKey;
|
||||
|
||||
@@ -2,7 +2,8 @@ import { app } from "../../../../../scripts/app.js";
|
||||
import { api } from "../../../../../scripts/api.js";
|
||||
import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { sanitizeDescriptionHtml } from "../utils/html_utils.js";
|
||||
import { sanitizeDescriptionHtml, pollBackgroundTask, safeStorage } from "../utils/html_utils.js";
|
||||
const localStorage = safeStorage;
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
import { getCivitaiModelUrl } from "../globals.js";
|
||||
export const modelInfoMethods = {
|
||||
@@ -691,30 +692,43 @@ export const modelInfoMethods = {
|
||||
|
||||
onStart(progressId);
|
||||
|
||||
for (;;) {
|
||||
await new Promise(resolve => setTimeout(resolve, 250));
|
||||
if (checkAbort()) {
|
||||
await onAbort(progressId);
|
||||
throw new Error('Hash calculation cancelled');
|
||||
}
|
||||
const progress = await this.fetchJson(
|
||||
`/model_resolver/calculate-file-hash/progress/${encodeURIComponent(progressId)}`,
|
||||
{ silent: true },
|
||||
'Get model hash progress'
|
||||
);
|
||||
let finalResult = null;
|
||||
|
||||
onProgress(progress?.percent || 0, progress?.message || '');
|
||||
await pollBackgroundTask({
|
||||
endpoint: `/model_resolver/calculate-file-hash/progress/${encodeURIComponent(progressId)}`,
|
||||
tokenCheck: () => {
|
||||
if (checkAbort()) {
|
||||
onAbort(progressId);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onProgress: (progress) => {
|
||||
onProgress(progress?.percent || 0, progress?.message || '');
|
||||
},
|
||||
isTerminal: (progress) => {
|
||||
return ['done', 'cancelled', 'error'].includes(progress?.status);
|
||||
},
|
||||
onTerminal: (progress) => {
|
||||
if (progress.status === 'done') {
|
||||
finalResult = progress;
|
||||
} else if (progress.status === 'cancelled') {
|
||||
throw new Error('Hash calculation cancelled');
|
||||
} else if (progress.status === 'error') {
|
||||
throw new Error(progress.error || progress.message || 'Hash calculation failed');
|
||||
}
|
||||
},
|
||||
onError: (err) => {
|
||||
throw err;
|
||||
},
|
||||
fetchJson: this.fetchJson.bind(this)
|
||||
});
|
||||
|
||||
if (progress?.status === 'done') {
|
||||
return progress;
|
||||
}
|
||||
if (progress?.status === 'cancelled') {
|
||||
throw new Error('Hash calculation cancelled');
|
||||
}
|
||||
if (progress?.status === 'error') {
|
||||
throw new Error(progress.error || progress.message || 'Hash calculation failed');
|
||||
}
|
||||
if (checkAbort()) {
|
||||
throw new Error('Hash calculation cancelled');
|
||||
}
|
||||
|
||||
return finalResult;
|
||||
},
|
||||
|
||||
async calculateLocalHashCandidatesForCompare(model = {}) {
|
||||
|
||||
@@ -4,7 +4,8 @@ import { $el } from "../../../../../scripts/ui.js";
|
||||
import { getSvgIcon } from "../../utils/icon_utils.js";
|
||||
import { LOG_LEVEL as DEFAULT_FRONTEND_LOG_LEVEL } from "../../log_system/config.js";
|
||||
import { logger as frontendLogger } from "../../log_system/logger.js";
|
||||
import { pollBackgroundTask } from "../utils/html_utils.js";
|
||||
import { pollBackgroundTask, safeStorage } from "../utils/html_utils.js";
|
||||
const localStorage = safeStorage;
|
||||
const SETTINGS_MAP = [
|
||||
{ serverKey: 'civitai_key', localKey: 'ModelResolver.civitaiApiKey', type: 'string', default: '' },
|
||||
{ serverKey: 'civitai_session_token', localKey: 'ModelResolver.civitaiSessionToken', type: 'string', default: '' },
|
||||
|
||||
Reference in New Issue
Block a user