mirror of
https://github.com/lemon695/obsidian-note-image-gallery.git
synced 2026-07-22 06:41:53 +00:00
fix: 修复异常
This commit is contained in:
parent
ef92618025
commit
632fce6133
9 changed files with 389 additions and 424 deletions
|
|
@ -217,7 +217,7 @@ Contributions are welcome! Here's how you can help:
|
|||
|
||||
## 📝 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ obsidian-note-image-gallery/
|
|||
|
||||
## 📝 许可证
|
||||
|
||||
本项目采用 MIT 许可证 - 详见 [LICENSE](LICENSE) 文件。
|
||||
本项目采用 GNU 通用公共许可证 v3.0 - 详见 [LICENSE](LICENSE) 文件。
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export const translations = {
|
|||
saveCacheIndexFailed: 'Failed to save cache index:',
|
||||
|
||||
// Settings
|
||||
imageGallerySettings: 'Image Gallery Settings',
|
||||
imageGallerySettings: 'Image Gallery settings',
|
||||
enableCache: 'Enable image cache',
|
||||
enableCacheDesc: 'Cache remote images to speed up loading',
|
||||
cacheValidPeriod: 'Cache valid period',
|
||||
|
|
@ -146,8 +146,10 @@ export const translations = {
|
|||
type Locale = keyof typeof translations;
|
||||
|
||||
export function getLocale(): Locale {
|
||||
const lang = window.localStorage.getItem('language') || 'en-GB';
|
||||
return (lang in translations ? lang : 'en-GB') as Locale;
|
||||
// Use Obsidian's official i18n API instead of reading from localStorage directly
|
||||
const lang: string = (window as unknown as { i18next?: { language?: string } }).i18next?.language ?? 'en-GB';
|
||||
if (lang && lang.startsWith('zh')) return 'zh';
|
||||
return 'en-GB';
|
||||
}
|
||||
|
||||
export function t(key: keyof typeof translations["en-GB"], params?: Record<string, string>): string {
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export default class NoteImageGalleryPlugin extends Plugin {
|
|||
log.setDebugMode(this.settings.debugMode);
|
||||
|
||||
this.imageExtractorService = new ImageExtractorService();
|
||||
this.imageCacheService = new ImageCacheService(this.app);
|
||||
this.imageCacheService = new ImageCacheService(this.app, this);
|
||||
|
||||
this.imageLoader = new ObsidianImageLoader(this.app, this);
|
||||
await this.imageCacheService.initCache();
|
||||
|
|
@ -101,7 +101,7 @@ export default class NoteImageGalleryPlugin extends Plugin {
|
|||
if (!(file instanceof TFile) || file.extension !== 'md') {
|
||||
return null;
|
||||
}
|
||||
return await this.app.vault.read(file);
|
||||
return await this.app.vault.cachedRead(file);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,4 +1,4 @@
|
|||
import {App} from 'obsidian';
|
||||
import {App, Plugin} from 'obsidian';
|
||||
import {log} from "../utils/log-utils";
|
||||
|
||||
interface CachedImage {
|
||||
|
|
@ -21,6 +21,7 @@ interface CacheIndex {
|
|||
|
||||
export class ImageCacheService {
|
||||
private app: App;
|
||||
private pluginDir: string;
|
||||
private maxCacheAge = 7 * 24 * 60 * 60 * 1000; // 7天缓存过期时间
|
||||
private maxCacheSize = 100 * 1024 * 1024; // 100MB最大缓存大小
|
||||
private cacheIndex: CacheIndex = {};
|
||||
|
|
@ -30,15 +31,16 @@ export class ImageCacheService {
|
|||
private debounceSaveTimeout: number | null = null;
|
||||
|
||||
private get cacheDir(): string {
|
||||
return `${this.app.vault.configDir}/plugins/note-image-gallery/cache`;
|
||||
return `${this.pluginDir}/cache`;
|
||||
}
|
||||
|
||||
private get indexFile(): string {
|
||||
return `${this.app.vault.configDir}/plugins/note-image-gallery/cache/index.json`;
|
||||
return `${this.pluginDir}/cache/index.json`;
|
||||
}
|
||||
|
||||
constructor(app: App) {
|
||||
constructor(app: App, plugin: Plugin) {
|
||||
this.app = app;
|
||||
this.pluginDir = plugin.manifest.dir!;
|
||||
void this.loadCacheIndex();
|
||||
}
|
||||
|
||||
|
|
@ -49,28 +51,21 @@ export class ImageCacheService {
|
|||
try {
|
||||
const adapter = this.app.vault.adapter;
|
||||
|
||||
// 检查并创建插件目录
|
||||
const pluginDir = `${this.app.vault.configDir}/plugins/note-image-gallery`;
|
||||
if (!(await adapter.exists(pluginDir))) {
|
||||
log.debug(() => `创建插件目录: ${pluginDir}`);
|
||||
await adapter.mkdir(pluginDir);
|
||||
}
|
||||
|
||||
// 检查并创建缓存目录
|
||||
if (!(await adapter.exists(this.cacheDir))) {
|
||||
log.debug(() => `创建缓存目录: ${this.cacheDir}`);
|
||||
log.debug(() => `Creating cache directory: ${this.cacheDir}`);
|
||||
await adapter.mkdir(this.cacheDir);
|
||||
}
|
||||
|
||||
// 验证目录是否已创建成功
|
||||
if (!(await adapter.exists(this.cacheDir))) {
|
||||
throw new Error(`无法验证缓存目录是否创建成功: ${this.cacheDir}`);
|
||||
throw new Error(`Failed to verify cache directory was created: ${this.cacheDir}`);
|
||||
}
|
||||
|
||||
log.debug(() => `缓存目录已确认: ${this.cacheDir}`);
|
||||
log.debug(() => `Cache directory confirmed: ${this.cacheDir}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(() => `确保缓存目录存在时出错:`, error instanceof Error ? error : undefined);
|
||||
log.error(() => `Error ensuring cache directory exists:`, error instanceof Error ? error : undefined);
|
||||
throw error; // 重新抛出错误以便调用者知道操作失败
|
||||
}
|
||||
}
|
||||
|
|
@ -80,9 +75,9 @@ export class ImageCacheService {
|
|||
await this.ensureCacheDir();
|
||||
await this.loadCacheIndex();
|
||||
await this.cleanupOrphanedFiles();
|
||||
log.debug(() => `缓存服务初始化完成,总大小: ${Math.round(this.totalCacheSize / 1024 / 1024)}MB`);
|
||||
log.debug(() => `Cache service initialized, total size: ${Math.round(this.totalCacheSize / 1024 / 1024)}MB`);
|
||||
} catch (error) {
|
||||
log.error(() => `缓存服务初始化失败:`, error instanceof Error ? error : undefined);
|
||||
log.error(() => `Cache service initialization failed:`, error instanceof Error ? error : undefined);
|
||||
// 确保基本的缓存功能可用
|
||||
this.cacheIndex = {};
|
||||
this.totalCacheSize = 0;
|
||||
|
|
@ -109,7 +104,7 @@ export class ImageCacheService {
|
|||
this.totalCacheSize += entry.size;
|
||||
});
|
||||
|
||||
log.debug(() => `已加载缓存索引,共 ${Object.keys(this.cacheIndex).length} 项,总大小 ${Math.round(this.totalCacheSize / 1024 / 1024)}MB`);
|
||||
log.debug(() => `Cache index loaded, ${Object.keys(this.cacheIndex).length} entries, total size ${Math.round(this.totalCacheSize / 1024 / 1024)}MB`);
|
||||
|
||||
// 检查缓存文件是否实际存在
|
||||
await this.validateCacheFiles();
|
||||
|
|
@ -117,7 +112,7 @@ export class ImageCacheService {
|
|||
// 加载后清理过期缓存
|
||||
await this.cleanCache();
|
||||
} catch (e) {
|
||||
log.error(() => `加载缓存索引失败:`, e instanceof Error ? e : undefined);
|
||||
log.error(() => `Failed to load cache index:`, e instanceof Error ? e : undefined);
|
||||
this.cacheIndex = {};
|
||||
this.totalCacheSize = 0;
|
||||
}
|
||||
|
|
@ -143,7 +138,7 @@ export class ImageCacheService {
|
|||
if (!exists) {
|
||||
invalidUrls.push(url);
|
||||
this.totalCacheSize -= entry.size;
|
||||
log.debug(() => `缓存文件不存在,从索引中移除: ${url}`);
|
||||
log.debug(() => `Cache file missing, removing from index: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -153,7 +148,7 @@ export class ImageCacheService {
|
|||
});
|
||||
|
||||
if (invalidUrls.length > 0) {
|
||||
log.debug(() => `移除了 ${invalidUrls.length} 个无效的缓存项`);
|
||||
log.debug(() => `Removed ${invalidUrls.length} invalid cache entries`);
|
||||
await this.saveCacheIndex();
|
||||
}
|
||||
}
|
||||
|
|
@ -163,7 +158,7 @@ export class ImageCacheService {
|
|||
*/
|
||||
public async saveCacheIndex() {
|
||||
if (this._isSaving) {
|
||||
log.debug(() => `缓存索引正在保存中,跳过重复调用`);
|
||||
log.debug(() => `Cache index save already in progress, skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -171,7 +166,7 @@ export class ImageCacheService {
|
|||
let retries = 0;
|
||||
const maxRetries = 3;
|
||||
|
||||
log.debug(() => `开始保存缓存索引,条目数: ${Object.keys(this.cacheIndex).length}`);
|
||||
log.debug(() => `Saving cache index, entries: ${Object.keys(this.cacheIndex).length}`);
|
||||
|
||||
while (retries < maxRetries) {
|
||||
try {
|
||||
|
|
@ -179,27 +174,27 @@ export class ImageCacheService {
|
|||
await this.ensureCacheDir();
|
||||
|
||||
// 准备JSON数据
|
||||
const jsonData = JSON.stringify(this.cacheIndex, null, 2); // 使用格式化的JSON便于检查
|
||||
const jsonData = JSON.stringify(this.cacheIndex);
|
||||
|
||||
// 写入文件
|
||||
const adapter = this.app.vault.adapter;
|
||||
await adapter.write(this.indexFile, jsonData);
|
||||
|
||||
log.debug(() => `缓存索引保存成功,大小: ${Math.round(jsonData.length / 1024)}KB`);
|
||||
log.debug(() => `Cache index saved, size: ${Math.round(jsonData.length / 1024)}KB`);
|
||||
break;
|
||||
} catch (e) {
|
||||
retries++;
|
||||
log.error(() => `保存缓存索引失败 (尝试 ${retries}/${maxRetries}):`, e instanceof Error ? e : undefined);
|
||||
log.error(() => `Failed to save cache index (attempt ${retries}/${maxRetries}):`, e instanceof Error ? e : undefined);
|
||||
|
||||
if (retries >= maxRetries) {
|
||||
log.error(() => `达到最大重试次数,无法保存缓存索引`);
|
||||
log.error(() => `Max retries reached, unable to save cache index`);
|
||||
break;
|
||||
}
|
||||
|
||||
// 等待短暂时间后重试
|
||||
const delay = 500 * retries;
|
||||
log.debug(() => `${delay}ms后重试保存缓存索引`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
log.debug(() => `Retrying cache index save in ${delay}ms`);
|
||||
await new Promise(resolve => window.setTimeout(resolve, delay));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,23 +205,23 @@ export class ImageCacheService {
|
|||
* 将图片添加到缓存
|
||||
*/
|
||||
async cacheImage(url: string, data: ArrayBuffer, etag?: string, mimeType?: string): Promise<string> {
|
||||
log.debug(() => `请求缓存图片: ${url}, 大小: ${Math.round(data.byteLength / 1024)}KB, 类型: ${mimeType || '未知'}`);
|
||||
log.debug(() => `Caching image: ${url}, size: ${Math.round(data.byteLength / 1024)}KB, type: ${mimeType || 'unknown'}`);
|
||||
|
||||
if (!data || data.byteLength === 0) {
|
||||
log.error(() => `跳过缓存图片 ${url}: 数据无效或为空`);
|
||||
log.error(() => `Skipping cache for ${url}: invalid or empty data`);
|
||||
return this.arrayBufferToBase64(data);
|
||||
}
|
||||
|
||||
// 检查是否应该缓存此图片
|
||||
if (!this.shouldCacheImage(url, mimeType)) {
|
||||
log.debug(() => `跳过缓存图片 ${url}: 缓存条件不满足`);
|
||||
log.debug(() => `Skipping cache for ${url}: cache conditions not met`);
|
||||
return await this.arrayBufferToBase64(data);
|
||||
}
|
||||
|
||||
// 检查数据大小,过大的图片不缓存
|
||||
const MAX_SINGLE_IMAGE_SIZE = 20 * 1024 * 1024; // 20MB单图片上限
|
||||
if (data.byteLength > MAX_SINGLE_IMAGE_SIZE) {
|
||||
log.debug(() => `跳过缓存图片 ${url}: 图片过大 (${Math.round(data.byteLength / 1024)}KB)`);
|
||||
log.debug(() => `Skipping cache for ${url}: image too large (${Math.round(data.byteLength / 1024)}KB)`);
|
||||
return await this.arrayBufferToBase64(data);
|
||||
}
|
||||
|
||||
|
|
@ -237,14 +232,14 @@ export class ImageCacheService {
|
|||
// 确保缓存目录存在
|
||||
const dirExists = await this.ensureCacheDir();
|
||||
if (!dirExists) {
|
||||
throw new Error("无法确保缓存目录存在");
|
||||
throw new Error("Failed to ensure cache directory exists");
|
||||
}
|
||||
|
||||
// 生成文件名
|
||||
const filename = this.generateFilename(url);
|
||||
const filePath = `${this.cacheDir}/${filename}`;
|
||||
|
||||
log.debug(() => `写入缓存文件: ${filePath}, 大小: ${Math.round(data.byteLength / 1024)}KB`);
|
||||
log.debug(() => `Writing cache file: ${filePath}, size: ${Math.round(data.byteLength / 1024)}KB`);
|
||||
|
||||
const dataArray = new Uint8Array(data);
|
||||
|
||||
|
|
@ -253,7 +248,7 @@ export class ImageCacheService {
|
|||
|
||||
const fileExists = await this.app.vault.adapter.exists(filePath);
|
||||
if (!fileExists) {
|
||||
throw new Error(`缓存文件写入后验证失败: ${filePath}`);
|
||||
throw new Error(`Cache file verification failed after write: ${filePath}`);
|
||||
}
|
||||
|
||||
// 文件写入成功后再更新索引
|
||||
|
|
@ -274,9 +269,9 @@ export class ImageCacheService {
|
|||
// 保存索引
|
||||
this.debouncedSaveCacheIndex(5000); // 使用已有的防抖方法,5秒后保存
|
||||
|
||||
log.debug(() => `成功缓存图片: ${url}, 总缓存大小: ${Math.round(this.totalCacheSize / 1024 / 1024)}MB`);
|
||||
log.debug(() => `Image cached: ${url}, total cache size: ${Math.round(this.totalCacheSize / 1024 / 1024)}MB`);
|
||||
} catch (error) {
|
||||
log.error(() => `缓存图片 ${url} 失败:`, error instanceof Error ? error : undefined);
|
||||
log.error(() => `Failed to cache image ${url}:`, error instanceof Error ? error : undefined);
|
||||
}
|
||||
|
||||
return base64Data;
|
||||
|
|
@ -286,43 +281,43 @@ export class ImageCacheService {
|
|||
* 从缓存中获取图片
|
||||
*/
|
||||
async getCachedImage(url: string): Promise<CachedImage | null> {
|
||||
log.debug(() => `检查缓存: ${url}`);
|
||||
log.debug(() => `Checking cache: ${url}`);
|
||||
|
||||
// 首先检查缓存是否启用
|
||||
if (!this.shouldUseCache()) {
|
||||
log.debug(() => `缓存未启用,跳过检查: ${url}`);
|
||||
log.debug(() => `Cache disabled, skipping check: ${url}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheEntry = this.cacheIndex[url];
|
||||
|
||||
if (!cacheEntry) {
|
||||
log.debug(() => `缓存未命中: ${url}`);
|
||||
log.debug(() => `Cache miss: ${url}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 检查缓存是否过期
|
||||
if (Date.now() - cacheEntry.timestamp > this.maxCacheAge) {
|
||||
log.debug(() => `缓存已过期: ${url}, 时间: ${new Date(cacheEntry.timestamp).toLocaleString()}`);
|
||||
log.debug(() => `Cache expired: ${url}, time: ${new Date(cacheEntry.timestamp).toLocaleString()}`);
|
||||
await this.removeCacheEntry(url);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const filePath = `${this.cacheDir}/${cacheEntry.filename}`;
|
||||
log.debug(() => `读取缓存文件: ${filePath}`);
|
||||
log.debug(() => `Reading cache file: ${filePath}`);
|
||||
|
||||
// 检查文件是否存在
|
||||
const exists = await this.app.vault.adapter.exists(filePath);
|
||||
if (!exists) {
|
||||
log.warn(() => `缓存索引存在但文件不存在: ${filePath}`);
|
||||
log.warn(() => `Cache index entry exists but file missing: ${filePath}`);
|
||||
await this.removeCacheEntry(url);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 读取缓存文件
|
||||
const arrayBuffer = await this.app.vault.adapter.readBinary(filePath);
|
||||
log.debug(() => `读取缓存成功: ${url}, 大小: ${Math.round(arrayBuffer.byteLength / 1024)}KB`);
|
||||
log.debug(() => `Cache read success: ${url}, size: ${Math.round(arrayBuffer.byteLength / 1024)}KB`);
|
||||
|
||||
const base64Data = await this.arrayBufferToBase64(arrayBuffer);
|
||||
|
||||
|
|
@ -339,7 +334,7 @@ export class ImageCacheService {
|
|||
etag: cacheEntry.etag
|
||||
};
|
||||
} catch (e) {
|
||||
log.error(() => `读取缓存图片 ${url} 失败:`, e instanceof Error ? e : undefined);
|
||||
log.error(() => `Failed to read cached image ${url}:`, e instanceof Error ? e : undefined);
|
||||
await this.removeCacheEntry(url);
|
||||
return null;
|
||||
}
|
||||
|
|
@ -348,12 +343,12 @@ export class ImageCacheService {
|
|||
// 添加防抖保存索引方法
|
||||
private debouncedSaveCacheIndex(delay = 2000): void {
|
||||
if (this.debounceSaveTimeout) {
|
||||
clearTimeout(this.debounceSaveTimeout);
|
||||
window.clearTimeout(this.debounceSaveTimeout);
|
||||
}
|
||||
|
||||
this.debounceSaveTimeout = window.setTimeout(() => {
|
||||
this.saveCacheIndex().catch((e: unknown) => {
|
||||
log.error(() => `延迟保存缓存索引失败:`, e instanceof Error ? e : undefined);
|
||||
log.error(() => `Deferred cache index save failed:`, e instanceof Error ? e : undefined);
|
||||
});
|
||||
this.debounceSaveTimeout = null;
|
||||
}, delay);
|
||||
|
|
@ -364,7 +359,7 @@ export class ImageCacheService {
|
|||
*/
|
||||
cleanup(): void {
|
||||
if (this.debounceSaveTimeout) {
|
||||
clearTimeout(this.debounceSaveTimeout);
|
||||
window.clearTimeout(this.debounceSaveTimeout);
|
||||
this.debounceSaveTimeout = null;
|
||||
}
|
||||
}
|
||||
|
|
@ -427,13 +422,13 @@ export class ImageCacheService {
|
|||
|
||||
// 计算需要释放的空间
|
||||
let spaceToFree = this.totalCacheSize - (this.maxCacheSize * 0.8); // 释放到80%
|
||||
log.debug(() => `缓存超过限制,需要释放 ${Math.round(spaceToFree / 1024 / 1024)}MB 空间, 根据优先级`);
|
||||
log.debug(() => `Cache over limit, need to free ${Math.round(spaceToFree / 1024 / 1024)}MB based on priority`);
|
||||
|
||||
// 从低分开始删除,直到释放足够空间
|
||||
for (const entry of validEntries) {
|
||||
if (spaceToFree <= 0) break;
|
||||
|
||||
log.debug(() => `删除低优先级缓存: ${entry.url}, 分数: ${entry['score']}, 大小: ${Math.round(entry.size / 1024)}KB`);
|
||||
log.debug(() => `Removing low-priority cache: ${entry.url}, score: ${entry['score']}, size: ${Math.round(entry.size / 1024)}KB`);
|
||||
await this.removeCacheEntry(entry.url);
|
||||
|
||||
freedSpace += entry.size;
|
||||
|
|
@ -443,7 +438,7 @@ export class ImageCacheService {
|
|||
}
|
||||
|
||||
if (removedCount > 0) {
|
||||
log.debug(() => `缓存清理完成: 删除了 ${removedCount} 项,释放了 ${Math.round(freedSpace / 1024 / 1024)}MB 空间`);
|
||||
log.debug(() => `Cache cleanup done: removed ${removedCount} entries, freed ${Math.round(freedSpace / 1024 / 1024)}MB`);
|
||||
await this.saveCacheIndex();
|
||||
}
|
||||
}
|
||||
|
|
@ -470,7 +465,7 @@ export class ImageCacheService {
|
|||
// 从索引中删除
|
||||
delete this.cacheIndex[url];
|
||||
} catch (e) {
|
||||
log.error(() => `移除缓存项 ${url} 失败:`, e instanceof Error ? e : undefined);
|
||||
log.error(() => `Failed to remove cache entry ${url}:`, e instanceof Error ? e : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -492,13 +487,13 @@ export class ImageCacheService {
|
|||
* 计算字符串的哈希值
|
||||
*/
|
||||
private hashString(str: string): string {
|
||||
let hash = 0;
|
||||
// FNV-1a 32-bit: better distribution than djb2, fewer collisions for URL-like strings
|
||||
let hash = 2166136261;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
hash = ((hash << 5) - hash) + char;
|
||||
hash = hash & hash; // 转换为32位整数
|
||||
hash ^= str.charCodeAt(i);
|
||||
hash = (hash * 16777619) >>> 0; // keep 32-bit unsigned
|
||||
}
|
||||
return Math.abs(hash).toString(16);
|
||||
return hash.toString(16);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -606,11 +601,11 @@ export class ImageCacheService {
|
|||
if (this.shouldUseCacheCallback) {
|
||||
const enabled = this.shouldUseCacheCallback();
|
||||
if (!enabled) {
|
||||
log.debug(() => `缓存已禁用(通过回调函数)`);
|
||||
log.debug(() => `Cache disabled (via callback)`);
|
||||
}
|
||||
return enabled;
|
||||
}
|
||||
log.debug(() => `缓存已启用(默认值)`);
|
||||
log.debug(() => `Cache enabled (default)`);
|
||||
return true; // 默认启用缓存
|
||||
}
|
||||
|
||||
|
|
@ -663,9 +658,9 @@ export class ImageCacheService {
|
|||
// 保存空索引
|
||||
await this.saveCacheIndex();
|
||||
|
||||
log.debug(() => `所有缓存已清除`);
|
||||
log.debug(() => `All cache cleared`);
|
||||
} catch (e) {
|
||||
log.error(() => `清除所有缓存失败:`, e instanceof Error ? e : undefined);
|
||||
log.error(() => `Failed to clear all cache:`, e instanceof Error ? e : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -702,17 +697,17 @@ export class ImageCacheService {
|
|||
const filePath = `${this.cacheDir}/${filename}`;
|
||||
try {
|
||||
await adapter.remove(filePath);
|
||||
log.debug(() => `已删除孤立的缓存文件: ${filename}`);
|
||||
log.debug(() => `Deleted orphaned cache file: ${filename}`);
|
||||
} catch (e) {
|
||||
log.error(() => `删除孤立的缓存文件失败: ${filename}`, e instanceof Error ? e : undefined);
|
||||
log.error(() => `Failed to delete orphaned cache file: ${filename}`, e instanceof Error ? e : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
if (orphanedFiles.length > 0) {
|
||||
log.debug(() => `共删除了 ${orphanedFiles.length} 个孤立的缓存文件`);
|
||||
log.debug(() => `Deleted ${orphanedFiles.length} orphaned cache files`);
|
||||
}
|
||||
} catch (e) {
|
||||
log.error(() => `清理孤立文件失败:`, e instanceof Error ? e : undefined);
|
||||
log.error(() => `Failed to clean up orphaned files:`, e instanceof Error ? e : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -740,7 +735,7 @@ export class ImageCacheService {
|
|||
|
||||
return cacheFiles.map(file => file.name);
|
||||
} catch (e) {
|
||||
log.error(() => `列出缓存目录文件失败:`, e instanceof Error ? e : undefined);
|
||||
log.error(() => `Failed to list cache directory files:`, e instanceof Error ? e : undefined);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export class ObsidianImageLoader {
|
|||
|
||||
return false;
|
||||
} catch (error) {
|
||||
log.error(() => `通过Obsidian API加载本地图片失败: ${imagePath}`, error instanceof Error ? error : undefined);
|
||||
log.error(() => `Failed to load local image via Obsidian API: ${imagePath}`, error instanceof Error ? error : undefined);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -103,8 +103,8 @@ export class ObsidianImageLoader {
|
|||
|
||||
return [...new Set(result)];
|
||||
} catch (error) {
|
||||
log.error(() => `生成替代路径时出错:`, error instanceof Error ? error : undefined);
|
||||
return [originalPath]; // 出错时返回原始路径
|
||||
log.error(() => `Error generating alternative paths:`, error instanceof Error ? error : undefined);
|
||||
return [originalPath]; // return original path on error
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,7 +158,7 @@ export class ObsidianImageLoader {
|
|||
img.src = objectUrl;
|
||||
|
||||
try {
|
||||
log.debug(() => `尝试缓存网络图片: ${imageUrl}, 内容类型: ${contentType}`);
|
||||
log.debug(() => `Caching network image: ${imageUrl}, content-type: ${contentType}`);
|
||||
|
||||
await this.plugin.imageCacheService.cacheImage(
|
||||
imageUrl,
|
||||
|
|
@ -167,12 +167,12 @@ export class ObsidianImageLoader {
|
|||
contentType
|
||||
);
|
||||
} catch (error) {
|
||||
log.error(() => `缓存图片失败: ${imageUrl}`, error instanceof Error ? error : undefined);
|
||||
log.error(() => `Failed to cache image: ${imageUrl}`, error instanceof Error ? error : undefined);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
log.error(() => `通过Obsidian API加载网络图片失败: ${imageUrl}`, error instanceof Error ? error : undefined);
|
||||
log.error(() => `Failed to load network image via Obsidian API: ${imageUrl}`, error instanceof Error ? error : undefined);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import {log} from "./log-utils";
|
||||
|
||||
/**
|
||||
* 重试处理器
|
||||
* Retry handler
|
||||
*/
|
||||
export class RetryHandler {
|
||||
constructor(
|
||||
|
|
@ -24,12 +24,12 @@ export class RetryHandler {
|
|||
lastError = error as Error;
|
||||
|
||||
if (attempt < this.maxRetries) {
|
||||
log.debug(() => `${context || '操作'} 失败,重试 ${attempt + 1}/${this.maxRetries}`);
|
||||
log.debug(() => `${context || 'Operation'} failed, retrying ${attempt + 1}/${this.maxRetries}`);
|
||||
this.onRetry?.(attempt + 1);
|
||||
// 添加指数退避延迟,避免立即重试
|
||||
await new Promise(resolve => setTimeout(resolve, Math.min(1000 * Math.pow(2, attempt), 5000)));
|
||||
// exponential backoff to avoid immediate retry
|
||||
await new Promise(resolve => window.setTimeout(resolve, Math.min(1000 * Math.pow(2, attempt), 5000)));
|
||||
} else {
|
||||
log.error(() => `${context || '操作'} 达到最大重试次数`, error as Error);
|
||||
log.error(() => `${context || 'Operation'} reached max retries`, error as Error);
|
||||
this.onFinalFailure?.(error as Error);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
193
styles.css
193
styles.css
|
|
@ -1,37 +1,44 @@
|
|||
.modal:has(.current-note-image-gallery) {
|
||||
width: 90vw !important;
|
||||
height: 90vh !important;
|
||||
max-width: 90vw !important;
|
||||
max-height: 90vh !important;
|
||||
/* Plugin: note-image-gallery — all classes prefixed with nig- */
|
||||
|
||||
.modal:has(.nig-gallery) {
|
||||
--dialog-width: 90vw;
|
||||
--dialog-max-width: 90vw;
|
||||
--dialog-max-height: 90vh;
|
||||
width: 90vw;
|
||||
height: 90vh;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal:has(.current-note-image-gallery) > .modal-close-button {
|
||||
top: 12px !important;
|
||||
right: 12px !important;
|
||||
.modal:has(.nig-gallery) > .modal-close-button {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.modal-content.current-note-image-gallery {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
padding: 0 !important;
|
||||
.modal-content.nig-gallery {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.current-note-image-gallery {
|
||||
width: 90vw !important;
|
||||
height: 90vh !important;
|
||||
max-width: 90vw !important;
|
||||
max-height: 90vh !important;
|
||||
.nig-gallery {
|
||||
width: 90vw;
|
||||
height: 90vh;
|
||||
max-width: 90vw;
|
||||
max-height: 90vh;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
display: flex !important;
|
||||
flex-direction: column !important;
|
||||
padding: 0 !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.modal-toolbar {
|
||||
.nig-toolbar {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
|
|
@ -41,13 +48,13 @@
|
|||
position: relative;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
.nig-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.image-wall-container {
|
||||
.nig-image-wall-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
width: 100%;
|
||||
|
|
@ -55,16 +62,16 @@
|
|||
}
|
||||
|
||||
/* 优化瀑布流列数 */
|
||||
.image-wall.waterfall {
|
||||
.nig-image-wall.nig-waterfall {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)) !important;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
grid-auto-rows: 20px;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
/* 图片项样式 */
|
||||
.image-item {
|
||||
.nig-image-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
|
|
@ -72,21 +79,21 @@
|
|||
background: var(--background-secondary);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex; /* 改用 flex 布局使内容居中 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.image-item:hover {
|
||||
.nig-image-item:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.image-item img {
|
||||
.nig-image-item img {
|
||||
width: 100%;
|
||||
height: auto; /* 让高度自适应,不要强制 100% */
|
||||
height: auto;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
display: block;
|
||||
|
|
@ -95,7 +102,7 @@
|
|||
}
|
||||
|
||||
/* 大图查看遮罩 */
|
||||
.lightbox-overlay {
|
||||
.nig-lightbox-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
@ -108,13 +115,13 @@
|
|||
z-index: 1000;
|
||||
}
|
||||
|
||||
.lightbox-overlay img {
|
||||
.nig-lightbox-overlay img {
|
||||
max-width: 90%;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.lightbox-close {
|
||||
.nig-lightbox-close {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
|
|
@ -132,7 +139,7 @@
|
|||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.loading-text {
|
||||
.nig-loading-text {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
|
|
@ -144,7 +151,7 @@
|
|||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.image-item.error {
|
||||
.nig-image-item.nig-error {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: var(--text-error);
|
||||
|
|
@ -157,45 +164,45 @@
|
|||
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 768px) {
|
||||
.image-wall.waterfall {
|
||||
.nig-image-wall.nig-waterfall {
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
grid-auto-rows: 15px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.current-note-image-gallery {
|
||||
width: 95vw !important;
|
||||
height: 95vh !important;
|
||||
.nig-gallery {
|
||||
width: 95vw;
|
||||
height: 95vh;
|
||||
}
|
||||
|
||||
.filter-toolbar {
|
||||
.nig-filter-toolbar {
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
.filter-container, .sort-container {
|
||||
.nig-filter-container, .nig-sort-container {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.filter-btn, .sort-select {
|
||||
.nig-filter-btn, .nig-sort-select {
|
||||
padding: 2px 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.modal-toolbar {
|
||||
.nig-toolbar {
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.filter-toolbar {
|
||||
.nig-filter-toolbar {
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.image-context-menu {
|
||||
.nig-context-menu {
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
|
|
@ -204,18 +211,18 @@
|
|||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.image-context-menu .menu-item {
|
||||
.nig-context-menu .nig-menu-item {
|
||||
padding: 6px 12px;
|
||||
cursor: pointer;
|
||||
color: var(--text-normal);
|
||||
}
|
||||
|
||||
.image-context-menu .menu-item:hover {
|
||||
.nig-context-menu .nig-menu-item:hover {
|
||||
background-color: var(--background-modifier-hover);
|
||||
}
|
||||
|
||||
/* 图片容器样式 */
|
||||
.lightbox-image-container {
|
||||
.nig-lightbox-image-container {
|
||||
position: relative;
|
||||
max-width: 90%;
|
||||
max-height: 90vh;
|
||||
|
|
@ -224,14 +231,14 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.lightbox-image-container img {
|
||||
.nig-lightbox-image-container img {
|
||||
max-width: 100%;
|
||||
max-height: 90vh;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
/* 导航按钮样式 */
|
||||
.lightbox-nav {
|
||||
.nig-lightbox-nav {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
|
|
@ -248,20 +255,20 @@
|
|||
z-index: 1001;
|
||||
}
|
||||
|
||||
.lightbox-nav:hover {
|
||||
.nig-lightbox-nav:hover {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.lightbox-nav.prev {
|
||||
.nig-lightbox-nav.prev {
|
||||
left: 20px;
|
||||
}
|
||||
|
||||
.lightbox-nav.next {
|
||||
.nig-lightbox-nav.next {
|
||||
right: 20px;
|
||||
}
|
||||
|
||||
/* 图片计数器样式 */
|
||||
.lightbox-counter {
|
||||
.nig-lightbox-counter {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
|
|
@ -275,60 +282,60 @@
|
|||
}
|
||||
|
||||
/* 进度条样式 */
|
||||
.progress-container {
|
||||
.nig-progress-container {
|
||||
flex: 0 1 300px;
|
||||
margin: 0 20px 0 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden; /* 防止过渡期间内容溢出 */
|
||||
overflow: hidden;
|
||||
transition: opacity 0.5s ease, height 0.4s ease, margin 0.4s ease;
|
||||
height: 20px; /* 固定一致的高度 */
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.progress-container.complete {
|
||||
.nig-progress-container.nig-complete {
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
margin: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.progress-container progress {
|
||||
.nig-progress-container progress {
|
||||
width: 100%;
|
||||
height: 4px; /* 更细的进度条 */
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
appearance: none; /* 去除默认样式 */
|
||||
appearance: none;
|
||||
border: none;
|
||||
background-color: rgba(0, 0, 0, 0.1); /* 浅色背景 */
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 进度条填充样式 */
|
||||
.progress-container progress::-webkit-progress-value {
|
||||
background-color: var(--interactive-accent, #1a73e8); /* 谷歌蓝色 */
|
||||
transition: width 0.3s ease; /* 平滑过渡 */
|
||||
.nig-progress-container progress::-webkit-progress-value {
|
||||
background-color: var(--interactive-accent, #1a73e8);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-container progress::-moz-progress-bar {
|
||||
.nig-progress-container progress::-moz-progress-bar {
|
||||
background-color: var(--interactive-accent, #1a73e8);
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* 进度指示器文本 */
|
||||
.progress-text {
|
||||
.nig-progress-text {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
margin-left: 8px;
|
||||
min-width: 50px; /* 固定宽度避免跳动 */
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* 在暗色主题中适应 */
|
||||
.theme-dark .progress-container progress {
|
||||
.theme-dark .nig-progress-container progress {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* 图片占位符样式 */
|
||||
.image-placeholder {
|
||||
.nig-image-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
|
@ -338,17 +345,18 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--background-secondary);
|
||||
z-index: 1; /* 确保在图片加载前显示在上层 */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 确保加载完成的图片显示在最上层 */
|
||||
.image-item img[style*="opacity: 1"] {
|
||||
.nig-image-item img.nig-loaded {
|
||||
opacity: 1;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 缩放控制样式 */
|
||||
.lightbox-controls {
|
||||
.nig-lightbox-controls {
|
||||
position: fixed;
|
||||
bottom: 60px;
|
||||
left: 50%;
|
||||
|
|
@ -360,7 +368,7 @@
|
|||
gap: 12px;
|
||||
}
|
||||
|
||||
.zoom-button {
|
||||
.nig-zoom-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
|
|
@ -373,26 +381,26 @@
|
|||
font-size: 18px;
|
||||
}
|
||||
|
||||
.zoom-button:hover {
|
||||
.nig-zoom-button:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 筛选和排序工具栏 */
|
||||
.filter-toolbar {
|
||||
.nig-filter-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-left: auto; /* 推到右侧 */
|
||||
padding-right: 50px; /* 为默认关闭按钮留出空间 */
|
||||
margin-left: auto;
|
||||
padding-right: 50px;
|
||||
}
|
||||
|
||||
.filter-container, .sort-container {
|
||||
.nig-filter-container, .nig-sort-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filter-btn {
|
||||
.nig-filter-btn {
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
|
|
@ -402,17 +410,17 @@
|
|||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.filter-btn:hover {
|
||||
.nig-filter-btn:hover {
|
||||
background: var(--background-secondary);
|
||||
}
|
||||
|
||||
.filter-btn.active {
|
||||
.nig-filter-btn.nig-active {
|
||||
background: var(--interactive-accent);
|
||||
color: var(--text-on-accent);
|
||||
border-color: var(--interactive-accent);
|
||||
}
|
||||
|
||||
.sort-select {
|
||||
.nig-sort-select {
|
||||
background: var(--background-primary);
|
||||
border: 1px solid var(--background-modifier-border);
|
||||
border-radius: 4px;
|
||||
|
|
@ -420,18 +428,3 @@
|
|||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image-item img[style*="opacity: 1"] {
|
||||
opacity: 1 !important;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 确保已加载图片保持可见状态 */
|
||||
.image-item img[style*="opacity: 1"],
|
||||
.image-item img.loaded,
|
||||
.image-item img[complete="true"] {
|
||||
opacity: 1 !important;
|
||||
z-index: 2 !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue