refactor: centralize shared utilities across backend and frontend
Backend: - Move SHA256 normalization logic from path_utils._normalize_header_sha256 into core/type_utils.normalize_sha256, adding support for 0x-prefixed hashes and unifying the normalization path used by extract_safetensors_header_sha256. - Add tests for the 0x prefix normalization cases. Frontend: - Introduce copyTextWithFeedback in web/resolver/utils/html_utils.js as a shared clipboard helper with configurable feedback text, CSS class, and duration, replacing repeated inline clipboard copy logic in globals.js, model_info_methods.js, and the notification utility. - Centralize showNotification on ResolverManagerDialog and remove the duplicate implementation from missingBrowserMethods. - Replace local const localStorage = safeStorage aliases in model_info_methods.js, missing_browser_methods.js, and options_methods.js with direct safeStorage usage.
This commit is contained in:
+4
-25
@@ -292,29 +292,6 @@ SAFETENSORS_METADATA_TEXT_MAX_CHARS = 20000
|
||||
SAFETENSORS_METADATA_THUMBNAIL_MAX_CHARS = 512 * 1024
|
||||
|
||||
|
||||
def _normalize_header_sha256(value: Any) -> str:
|
||||
if not isinstance(value, str):
|
||||
return ""
|
||||
|
||||
candidate = value.strip()
|
||||
lower_candidate = candidate.lower()
|
||||
if lower_candidate.startswith("sha256:") or lower_candidate.startswith("sha256="):
|
||||
if ":" in candidate:
|
||||
candidate = candidate.split(":", 1)[-1]
|
||||
else:
|
||||
candidate = candidate.split("=", 1)[-1]
|
||||
candidate = candidate.strip()
|
||||
lower_candidate = candidate.lower()
|
||||
|
||||
if lower_candidate.startswith("0x"):
|
||||
candidate = candidate[2:].strip()
|
||||
|
||||
if len(candidate) != 64:
|
||||
return ""
|
||||
if any(char not in _SHA256_HEX_CHARS for char in candidate):
|
||||
return ""
|
||||
return candidate.lower()
|
||||
|
||||
|
||||
def read_safetensors_header(
|
||||
file_path: str,
|
||||
@@ -368,6 +345,8 @@ def extract_safetensors_header_sha256(
|
||||
max_header_size: int = SAFETENSORS_HEADER_MAX_BYTES,
|
||||
) -> Optional[str]:
|
||||
"""Return an embedded SHA256 from a safetensors metadata header when present."""
|
||||
from .type_utils import normalize_sha256
|
||||
|
||||
header_json = read_safetensors_header(file_path, max_header_size=max_header_size)
|
||||
if not isinstance(header_json, dict):
|
||||
return None
|
||||
@@ -382,7 +361,7 @@ def extract_safetensors_header_sha256(
|
||||
"sha256",
|
||||
"SHA256",
|
||||
):
|
||||
sha256 = _normalize_header_sha256(metadata.get(key))
|
||||
sha256 = normalize_sha256(metadata.get(key))
|
||||
if sha256:
|
||||
return sha256
|
||||
|
||||
@@ -396,7 +375,7 @@ def extract_safetensors_header_sha256(
|
||||
continue
|
||||
if any(part in key_lower for part in _NON_SHA256_HASH_KEY_PARTS):
|
||||
continue
|
||||
sha256 = _normalize_header_sha256(value)
|
||||
sha256 = normalize_sha256(value)
|
||||
if sha256:
|
||||
return sha256
|
||||
|
||||
|
||||
@@ -708,6 +708,9 @@ def normalize_sha256(value: Any) -> str:
|
||||
if text.lower().startswith(prefix):
|
||||
text = text[len(prefix):].strip()
|
||||
|
||||
if text.lower().startswith("0x"):
|
||||
text = text[2:].strip()
|
||||
|
||||
return text.lower() if SHA256_PATTERN.match(text) else ""
|
||||
|
||||
|
||||
|
||||
@@ -911,6 +911,9 @@ class UnifiedHelpersTests(unittest.TestCase):
|
||||
self.assertEqual(normalize_sha256("sha256:" + h), h)
|
||||
self.assertEqual(normalize_sha256("sha256=" + h), h)
|
||||
self.assertEqual(normalize_sha256("SHA256:" + h.upper()), h)
|
||||
self.assertEqual(normalize_sha256("0x" + h), h)
|
||||
self.assertEqual(normalize_sha256("sha256:0x" + h), h)
|
||||
self.assertEqual(normalize_sha256("SHA256:0X" + h.upper()), h)
|
||||
self.assertEqual(normalize_sha256("invalid_hash"), "")
|
||||
self.assertEqual(normalize_sha256(None), "")
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { copyTextWithFeedback } from "./utils/html_utils.js";
|
||||
|
||||
export function registerGlobalHelpers() {
|
||||
// Global helper functions for inline onclick handlers
|
||||
window.MLToggleHidden = function(id, trigger, collapsedText, expandedText) {
|
||||
@@ -97,17 +99,15 @@ export function registerGlobalHelpers() {
|
||||
};
|
||||
|
||||
window.MLCopy = function(text, btn) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
window.MLSetCopyButtonFeedback(btn, 'Copied');
|
||||
});
|
||||
copyTextWithFeedback(text, btn, { successText: 'Copied', successClass: '' });
|
||||
};
|
||||
|
||||
window.MLCopyCode = function(sectionId, btn) {
|
||||
const section = document.getElementById(sectionId);
|
||||
const codeEl = section.querySelector('code');
|
||||
navigator.clipboard.writeText(codeEl.textContent).then(() => {
|
||||
window.MLSetCopyButtonFeedback(btn, 'Copied');
|
||||
});
|
||||
const codeEl = section ? section.querySelector('code') : null;
|
||||
if (codeEl) {
|
||||
copyTextWithFeedback(codeEl.textContent, btn, { successText: 'Copied', successClass: '' });
|
||||
}
|
||||
};
|
||||
|
||||
window.MLOpenContextMenu = function(event, element) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { $el, ComfyDialog } from "../../../../scripts/ui.js";
|
||||
import { api } from "../../../../scripts/api.js";
|
||||
import { safeStorage } from "./utils/html_utils.js";
|
||||
import { showNotification as showNotificationUtils } from "../utils/notification_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";
|
||||
@@ -258,6 +259,15 @@ export class ResolverManagerDialog extends ComfyDialog {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
showNotification(message, type = 'success', options = {}) {
|
||||
const duration = options?.duration || (type === 'success' ? 4000 : (type === 'error' ? 6000 : 3000));
|
||||
return showNotificationUtils(message, type, {
|
||||
...options,
|
||||
duration,
|
||||
deduplicate: options?.deduplicate || false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -223,3 +223,44 @@ export function normalizePathIdentity(value = '') {
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies text to clipboard and handles time-based button visual feedback.
|
||||
* @param {string} text - The text content to copy.
|
||||
* @param {HTMLElement} button - The button to apply feedback to.
|
||||
* @param {Object} options - Customization parameters.
|
||||
*/
|
||||
export async function copyTextWithFeedback(text, button, options = {}) {
|
||||
if (!button) return;
|
||||
const successText = options.successText || 'Copied';
|
||||
const errorText = options.errorText || 'Failed';
|
||||
const duration = options.duration || 1200;
|
||||
const successClass = options.successClass || 'is-copied';
|
||||
const successHtml = options.successHtml || null;
|
||||
|
||||
const originalHtml = button.innerHTML;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (successClass) {
|
||||
button.classList.add(successClass);
|
||||
}
|
||||
if (successHtml) {
|
||||
button.innerHTML = successHtml;
|
||||
} else {
|
||||
button.textContent = successText;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Model Resolver: Copy failed:', err);
|
||||
button.textContent = errorText;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (button.isConnected) {
|
||||
if (successClass) {
|
||||
button.classList.remove(successClass);
|
||||
}
|
||||
button.innerHTML = originalHtml;
|
||||
}
|
||||
}, duration);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,9 @@ 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 { 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 = {}) {
|
||||
@@ -678,7 +676,7 @@ export const missingBrowserMethods = {
|
||||
showResolvedToggle.addEventListener('change', () => {
|
||||
this.showResolvedModels = Boolean(showResolvedToggle.checked);
|
||||
try {
|
||||
localStorage.setItem(this.showResolvedModelsStorageKey, this.showResolvedModels ? '1' : '0');
|
||||
safeStorage.setItem(this.showResolvedModelsStorageKey, this.showResolvedModels ? '1' : '0');
|
||||
} catch (e) {}
|
||||
this.displayMissingModels(container, data);
|
||||
});
|
||||
@@ -690,7 +688,7 @@ export const missingBrowserMethods = {
|
||||
showAutoDownloadToggle.addEventListener('change', () => {
|
||||
this.showAutoDownloadModels = Boolean(showAutoDownloadToggle.checked);
|
||||
try {
|
||||
localStorage.setItem(this.showAutoDownloadModelsStorageKey, this.showAutoDownloadModels ? '1' : '0');
|
||||
safeStorage.setItem(this.showAutoDownloadModelsStorageKey, this.showAutoDownloadModels ? '1' : '0');
|
||||
} catch (e) {}
|
||||
this.displayMissingModels(container, data);
|
||||
});
|
||||
@@ -811,7 +809,7 @@ export const missingBrowserMethods = {
|
||||
this._missingBrowserLastDetailWidth = null;
|
||||
this.cancelMissingBrowserSplitWidthPersist();
|
||||
try {
|
||||
localStorage.removeItem(this.missingBrowserSplitStorageKey);
|
||||
safeStorage.removeItem(this.missingBrowserSplitStorageKey);
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
@@ -863,7 +861,7 @@ export const missingBrowserMethods = {
|
||||
|
||||
let savedWidth = null;
|
||||
try {
|
||||
const storedWidth = parseInt(localStorage.getItem(this.missingBrowserSplitStorageKey) || '', 10);
|
||||
const storedWidth = parseInt(safeStorage.getItem(this.missingBrowserSplitStorageKey) || '', 10);
|
||||
savedWidth = Number.isFinite(storedWidth) && storedWidth > 0 ? storedWidth : null;
|
||||
} catch (e) {}
|
||||
|
||||
@@ -1228,7 +1226,7 @@ export const missingBrowserMethods = {
|
||||
this._missingBrowserSplitPersistIdle = null;
|
||||
this._missingBrowserSplitPersistTimer = null;
|
||||
try {
|
||||
localStorage.setItem(this.missingBrowserSplitStorageKey, String(nextWidth));
|
||||
safeStorage.setItem(this.missingBrowserSplitStorageKey, String(nextWidth));
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
@@ -2174,17 +2172,7 @@ export const missingBrowserMethods = {
|
||||
return html;
|
||||
},
|
||||
|
||||
/**
|
||||
* Show a notification banner (similar to ComfyUI's "Reconnecting" banner)
|
||||
*/
|
||||
showNotification(message, type = 'success', options = {}) {
|
||||
const duration = options?.duration || (type === 'success' ? 4000 : (type === 'error' ? 6000 : 3000));
|
||||
return showNotificationUtils(message, type, {
|
||||
...options,
|
||||
duration,
|
||||
deduplicate: options?.deduplicate || false
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
refreshMissingListRow(missing, options = {}) {
|
||||
if (!missing || !this.contentElement) return;
|
||||
|
||||
@@ -2,8 +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 { sanitizeDescriptionHtml, pollBackgroundTask, safeStorage } from "../utils/html_utils.js";
|
||||
const localStorage = safeStorage;
|
||||
import { sanitizeDescriptionHtml, pollBackgroundTask, safeStorage, copyTextWithFeedback } from "../utils/html_utils.js";
|
||||
import { getModelCardUrl } from "../utils/url_utils.js";
|
||||
import { getCivitaiModelUrl } from "../globals.js";
|
||||
export const modelInfoMethods = {
|
||||
@@ -1758,19 +1757,12 @@ export const modelInfoMethods = {
|
||||
|
||||
if (!words.length) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(words.join(', '));
|
||||
copyBtn.textContent = 'Copied';
|
||||
} catch (error) {
|
||||
console.error('Model Resolver: Failed to copy trained words:', error);
|
||||
copyBtn.textContent = 'Failed';
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (copyBtn.isConnected) {
|
||||
copyBtn.textContent = 'Copy';
|
||||
}
|
||||
}, 1200);
|
||||
await copyTextWithFeedback(words.join(', '), copyBtn, {
|
||||
successText: 'Copied',
|
||||
errorText: 'Failed',
|
||||
duration: 1200,
|
||||
successClass: ''
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1794,7 +1786,7 @@ export const modelInfoMethods = {
|
||||
if (!panel) return;
|
||||
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem('model_resolver_info_dialog_size') || 'null');
|
||||
const saved = JSON.parse(safeStorage.getItem('model_resolver_info_dialog_size') || 'null');
|
||||
if (!saved || typeof saved !== 'object') return;
|
||||
|
||||
const width = Number(saved.w);
|
||||
@@ -1823,7 +1815,7 @@ export const modelInfoMethods = {
|
||||
if (!width || !height) return;
|
||||
|
||||
try {
|
||||
localStorage.setItem('model_resolver_info_dialog_size', JSON.stringify({ w: width, h: height }));
|
||||
safeStorage.setItem('model_resolver_info_dialog_size', JSON.stringify({ w: width, h: height }));
|
||||
} catch (error) {
|
||||
console.warn('Model Resolver: Failed to save info dialog size:', error);
|
||||
}
|
||||
@@ -2593,21 +2585,12 @@ export const modelInfoMethods = {
|
||||
|
||||
if (!text) return;
|
||||
|
||||
try {
|
||||
await navigator.clipboard.writeText(String(text));
|
||||
button.classList.add('is-copied');
|
||||
button.innerHTML = `${getSvgIcon('copy')} Copied`;
|
||||
} catch (error) {
|
||||
console.error('Model Resolver: Failed to copy image metadata:', error);
|
||||
button.innerHTML = `${getSvgIcon('copy')} Failed`;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (button.isConnected) {
|
||||
button.classList.remove('is-copied');
|
||||
button.innerHTML = `${getSvgIcon('copy')} Copy${key === 'all' ? ' all' : ''}`;
|
||||
}
|
||||
}, 1200);
|
||||
await copyTextWithFeedback(String(text), button, {
|
||||
successClass: 'is-copied',
|
||||
successHtml: `${getSvgIcon('copy')} Copied`,
|
||||
errorText: 'Failed',
|
||||
duration: 1200
|
||||
});
|
||||
},
|
||||
|
||||
closeInfoImagePreview(preview = null) {
|
||||
|
||||
@@ -5,7 +5,6 @@ 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, 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: '' },
|
||||
@@ -1222,7 +1221,7 @@ export const optionsMethods = {
|
||||
}
|
||||
if (persist) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
safeStorage.setItem(
|
||||
this.metadataSizeControlsCollapsedStorageKey || 'model_resolver_metadata_size_controls_collapsed',
|
||||
isCollapsed ? '1' : '0'
|
||||
);
|
||||
@@ -1237,7 +1236,7 @@ export const optionsMethods = {
|
||||
}
|
||||
if (typeof this.metadataSizeControlsCollapsed !== 'boolean') {
|
||||
try {
|
||||
this.metadataSizeControlsCollapsed = localStorage.getItem(this.metadataSizeControlsCollapsedStorageKey) === '1';
|
||||
this.metadataSizeControlsCollapsed = safeStorage.getItem(this.metadataSizeControlsCollapsedStorageKey) === '1';
|
||||
} catch (error) {
|
||||
this.metadataSizeControlsCollapsed = false;
|
||||
}
|
||||
@@ -1427,7 +1426,7 @@ export const optionsMethods = {
|
||||
}
|
||||
if (persist) {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
safeStorage.setItem(
|
||||
this.metadataBuildControlsCollapsedStorageKey || 'model_resolver_metadata_build_controls_collapsed',
|
||||
isCollapsed ? '1' : '0'
|
||||
);
|
||||
@@ -1442,7 +1441,7 @@ export const optionsMethods = {
|
||||
}
|
||||
if (typeof this.metadataBuildControlsCollapsed !== 'boolean') {
|
||||
try {
|
||||
this.metadataBuildControlsCollapsed = localStorage.getItem(this.metadataBuildControlsCollapsedStorageKey) === '1';
|
||||
this.metadataBuildControlsCollapsed = safeStorage.getItem(this.metadataBuildControlsCollapsedStorageKey) === '1';
|
||||
} catch (error) {
|
||||
this.metadataBuildControlsCollapsed = false;
|
||||
}
|
||||
@@ -2304,8 +2303,8 @@ export const optionsMethods = {
|
||||
|
||||
tokens.aria2c_path = data.aria2c_path;
|
||||
tokens.download_backend = 'aria2';
|
||||
localStorage.setItem('ModelResolver.aria2cPath', data.aria2c_path);
|
||||
localStorage.setItem('ModelResolver.downloadBackend', 'aria2');
|
||||
safeStorage.setItem('ModelResolver.aria2cPath', data.aria2c_path);
|
||||
safeStorage.setItem('ModelResolver.downloadBackend', 'aria2');
|
||||
|
||||
setAria2Status('');
|
||||
renderAria2Summary({
|
||||
@@ -3469,25 +3468,25 @@ export const optionsMethods = {
|
||||
search_source_enabled: sourceEnabled,
|
||||
};
|
||||
|
||||
// 1. Write to localStorage (fast local cache)
|
||||
// 1. Write to safeStorage (fast local cache)
|
||||
for (const opt of SETTINGS_MAP) {
|
||||
const val = newSettings[opt.serverKey];
|
||||
if (opt.type === 'boolean' || opt.type === 'frontendLogsEnabled' || opt.type === 'backendLogsEnabled') {
|
||||
localStorage.setItem(opt.localKey, val ? 'true' : 'false');
|
||||
safeStorage.setItem(opt.localKey, val ? 'true' : 'false');
|
||||
} else if (opt.type === 'json') {
|
||||
localStorage.setItem(opt.localKey, JSON.stringify(val));
|
||||
safeStorage.setItem(opt.localKey, JSON.stringify(val));
|
||||
} else {
|
||||
localStorage.setItem(opt.localKey, String(val ?? ''));
|
||||
safeStorage.setItem(opt.localKey, String(val ?? ''));
|
||||
}
|
||||
}
|
||||
defaultRootSelectInputs.forEach((select) => {
|
||||
const storageKey = select.dataset.storageKey;
|
||||
if (storageKey) {
|
||||
localStorage.setItem(storageKey, select.value || '');
|
||||
safeStorage.setItem(storageKey, select.value || '');
|
||||
}
|
||||
});
|
||||
Object.entries(sourceEnabled).forEach(([key, val]) => {
|
||||
localStorage.setItem(key, val ? 'true' : 'false');
|
||||
safeStorage.setItem(key, val ? 'true' : 'false');
|
||||
});
|
||||
if (civitaiLimitInput) civitaiLimitInput.value = `${civitaiCandidateLimit}`;
|
||||
this.applyFrontendLoggingPreference(newSettings.frontend_logs_enabled, newSettings.frontend_log_level);
|
||||
@@ -3523,7 +3522,7 @@ export const optionsMethods = {
|
||||
getStoredTokens() {
|
||||
const tokens = {};
|
||||
for (const opt of SETTINGS_MAP) {
|
||||
const raw = localStorage.getItem(opt.localKey);
|
||||
const raw = safeStorage.getItem(opt.localKey);
|
||||
if (opt.type === 'boolean') {
|
||||
tokens[opt.serverKey] = raw === null ? opt.default : raw !== 'false';
|
||||
} else if (opt.type === 'json') {
|
||||
@@ -3578,29 +3577,29 @@ export const optionsMethods = {
|
||||
if (serverValue === undefined) continue;
|
||||
|
||||
if (opt.type === 'boolean' || opt.type === 'frontendLogsEnabled' || opt.type === 'backendLogsEnabled') {
|
||||
localStorage.setItem(opt.localKey, serverValue ? 'true' : 'false');
|
||||
safeStorage.setItem(opt.localKey, serverValue ? 'true' : 'false');
|
||||
} else if (opt.type === 'json') {
|
||||
localStorage.setItem(opt.localKey, JSON.stringify(serverValue || {}));
|
||||
safeStorage.setItem(opt.localKey, JSON.stringify(serverValue || {}));
|
||||
} else if (opt.type === 'backend') {
|
||||
localStorage.setItem(opt.localKey, this.normalizeDownloadBackend(serverValue));
|
||||
safeStorage.setItem(opt.localKey, this.normalizeDownloadBackend(serverValue));
|
||||
} else if (opt.type === 'pathMode') {
|
||||
localStorage.setItem(opt.localKey, this.normalizeDownloadPathMode(serverValue));
|
||||
safeStorage.setItem(opt.localKey, this.normalizeDownloadPathMode(serverValue));
|
||||
} else {
|
||||
if (serverValue !== null && serverValue !== '') {
|
||||
localStorage.setItem(opt.localKey, String(serverValue));
|
||||
safeStorage.setItem(opt.localKey, String(serverValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
this.getDefaultRootCategoryDefinitions().forEach((item) => {
|
||||
if (data[item.settingKey] !== undefined) {
|
||||
localStorage.setItem(item.storageKey, String(data[item.settingKey] || ''));
|
||||
safeStorage.setItem(item.storageKey, String(data[item.settingKey] || ''));
|
||||
}
|
||||
});
|
||||
|
||||
// Source-enabled flags stored as a nested object
|
||||
if (data.search_source_enabled && typeof data.search_source_enabled === 'object') {
|
||||
Object.entries(data.search_source_enabled).forEach(([key, val]) => {
|
||||
if (key) localStorage.setItem(key, val ? 'true' : 'false');
|
||||
if (key) safeStorage.setItem(key, val ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
const tokens = this.getStoredTokens();
|
||||
|
||||
Reference in New Issue
Block a user