From fe9dda88983d7e3601a289e0b80c9b29320b3aa0 Mon Sep 17 00:00:00 2001 From: Lemon695 Date: Mon, 8 Jun 2026 16:21:05 +0800 Subject: [PATCH] Harden the release branch for Obsidian market submission Constraint: Keep the plugin release-safe without introducing new dependencies Rejected: Commit the branch with mismatched version metadata and missing release verifier | that would leave the release flow broken Confidence: high Scope-risk: moderate Directive: Keep package, manifest, and versions metadata aligned before cutting future release tags Tested: npm run verify:release; npm run lint; npm run build Not-tested: Real Obsidian desktop and mobile smoke tests on this branch --- .gitignore | 1 + README.md | 19 +- README_zh.md | 17 +- manifest.json | 2 +- package.json | 5 +- scripts/verify-release.mjs | 111 ++++++++ src/i18n/locale.ts | 26 +- .../current-note-image-gallery-service.ts | 249 ------------------ src/service/image-cache-service.ts | 1 - src/service/image-extractor-service.ts | 51 +--- src/service/obsidian-image-loader.ts | 11 - versions.json | 4 +- 12 files changed, 166 insertions(+), 331 deletions(-) create mode 100644 scripts/verify-release.mjs diff --git a/.gitignore b/.gitignore index 783c449..8965fde 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ data.json /build.sh /lint-result.txt /docs/ +/.omx/ diff --git a/README.md b/README.md index 99e0480..439baaa 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ There are three ways to open the image gallery: -1. **Command Palette**: Press `Ctrl/Cmd + P` and search for "Open Current Note Image Gallery" +1. **Command Palette**: Press `Ctrl/Cmd + P` and search for "Open gallery for current note" 2. **Ribbon Icon**: Click the image gallery icon in the left sidebar 3. **Hotkey**: Assign a custom hotkey in Settings → Hotkeys @@ -136,6 +136,16 @@ Access plugin settings via Settings → Note Image Gallery --- +## 🔒 Privacy and disclosures + +- **No account required**: The plugin works without signing in to any service. +- **No telemetry or ads**: The plugin doesn't collect analytics or show advertisements. +- **Network use**: The plugin may send network requests only when your current note contains remote image URLs and you choose to open the gallery. +- **External file access**: The plugin reads images referenced by the current note and stores remote-image cache files inside the plugin data directory. +- **Open source**: The source code is available in this repository. + +--- + ## 🎯 Performance Tips 1. **Enable Caching**: Keep image caching enabled for faster loading of remote images @@ -156,6 +166,9 @@ npm install # Build for development (with watch mode) npm run dev +# Run lint +npm run lint + # Build for production npm run build @@ -169,7 +182,9 @@ npm run build obsidian-note-image-gallery/ ├── src/ │ ├── main.ts # Plugin entry point -│ ├── settings.ts # Settings tab and configuration +│ ├── settings.ts # Settings tab and configuration +│ ├── i18n/ +│ │ └── locale.ts # Internationalization strings │ ├── service/ │ │ ├── current-note-image-gallery-service.ts # Main gallery service │ │ ├── image-cache-service.ts # Cache management diff --git a/README_zh.md b/README_zh.md index 162a097..1e3cb98 100644 --- a/README_zh.md +++ b/README_zh.md @@ -84,7 +84,7 @@ 有三种方式打开图片墙: -1. **命令面板**:按 `Ctrl/Cmd + P` 并搜索"当前文件"或"Current file" +1. **命令面板**:按 `Ctrl/Cmd + P` 并搜索“打开当前笔记图片墙”或 “Open gallery for current note” 2. **侧边栏图标**:点击左侧边栏中的图片墙图标 3. **快捷键**:在设置 → 快捷键中自定义快捷键 @@ -136,6 +136,16 @@ --- +## 🔒 隐私与披露 + +- **无需账号**:插件不需要登录任何服务即可使用。 +- **无遥测、无广告**:插件不会收集分析数据,也不会展示广告。 +- **网络访问**:仅当当前笔记包含远程图片 URL 且你主动打开图片墙时,插件才会请求这些远程图片。 +- **外部文件访问**:插件会读取当前笔记引用的图片,并把远程图片缓存写入插件数据目录。 +- **开源**:源码已在本仓库公开。 + +--- + ## 🎯 性能优化建议 1. **启用缓存**:保持图片缓存开启以加快远程图片加载速度 @@ -156,6 +166,9 @@ npm install # 开发构建(带监听模式) npm run dev +# 运行 Lint +npm run lint + # 生产构建 npm run build @@ -169,7 +182,7 @@ npm run build obsidian-note-image-gallery/ ├── src/ │ ├── main.ts # 插件入口 -│ ├── settings.ts # 设置选项卡和配置 +│ ├── settings.ts # 设置选项卡和配置 │ ├── i18n/ │ │ └── locale.ts # 国际化翻译 │ ├── service/ diff --git a/manifest.json b/manifest.json index 90d0e72..58b7aef 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "note-image-gallery", "name": "Note Image Gallery", - "version": "1.0.11", + "version": "1.1.0", "minAppVersion": "1.9.14", "description": "Display all images from your note in a gallery view.", "author": "Lemon695", diff --git a/package.json b/package.json index 420801a..3d79b1e 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "obsidian-note-image-gallery", - "version": "1.0.10", + "version": "1.1.0", "description": "An Obsidian plugin to display note's images in a gallery view", "main": "main.js", "scripts": { "dev": "node esbuild.config.mjs", "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", "version": "node version-bump.mjs && git add manifest.json versions.json", + "verify:release": "node scripts/verify-release.mjs", "lint": "eslint .", "lint:fix": "eslint . --fix" }, @@ -19,7 +20,7 @@ "note" ], "author": "", - "license": "MIT", + "license": "GPL-3.0-only", "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@eslint/js": "^9.39.1", diff --git a/scripts/verify-release.mjs b/scripts/verify-release.mjs new file mode 100644 index 0000000..fe7e0e2 --- /dev/null +++ b/scripts/verify-release.mjs @@ -0,0 +1,111 @@ +/* eslint-env node */ +/* global console, process */ +import { existsSync, readFileSync } from "fs"; + +function readJson(path) { + return JSON.parse(readFileSync(path, "utf8")); +} + +function readText(path) { + return readFileSync(path, "utf8"); +} + +const packageJson = readJson("package.json"); +const manifest = readJson("manifest.json"); +const versions = readJson("versions.json"); +const readme = readText("README.md"); +const readmeZh = readText("README_zh.md"); +const license = readText("LICENSE"); + +const checks = []; + +function check(ok, label, detail = "") { + checks.push({ ok, label, detail }); +} + +function includesAll(text, snippets) { + return snippets.every((snippet) => text.includes(snippet)); +} + +check(existsSync("README.md"), "README.md exists"); +check(existsSync("README_zh.md"), "README_zh.md exists"); +check(existsSync("LICENSE"), "LICENSE exists"); +check(existsSync("manifest.json"), "manifest.json exists"); +check(existsSync("package-lock.json"), "package-lock.json exists"); + +check( + packageJson.version === manifest.version, + "package.json version matches manifest.json", + `${packageJson.version} vs ${manifest.version}` +); + +check( + versions[manifest.version] === manifest.minAppVersion, + "versions.json contains current release mapping", + `${manifest.version} -> ${versions[manifest.version] ?? "missing"}` +); + +check( + /^\d+\.\d+\.\d+$/.test(manifest.version), + "manifest version uses x.y.z format", + manifest.version +); + +check( + /^[a-z-]+$/.test(manifest.id) && !manifest.id.endsWith("plugin") && !manifest.id.includes("obsidian"), + "manifest id satisfies Obsidian rules", + manifest.id +); + +check( + packageJson.license === "GPL-3.0-only", + "package.json license is aligned", + packageJson.license +); + +check( + license.includes("GNU GENERAL PUBLIC LICENSE"), + "LICENSE content looks like GPL" +); + +check( + includesAll(readme, [ + "## 🔒 Privacy and disclosures", + "No account required", + "No telemetry or ads", + "Network use", + "External file access" + ]), + "README.md contains disclosures" +); + +check( + includesAll(readmeZh, [ + "## 🔒 隐私与披露", + "无需账号", + "无遥测、无广告", + "网络访问", + "外部文件访问" + ]), + "README_zh.md contains disclosures" +); + +check( + !existsSync("main.js"), + "main.js is not committed in repo root" +); + +const failed = checks.filter((item) => !item.ok); + +for (const item of checks) { + const icon = item.ok ? "✅" : "❌"; + const suffix = item.detail ? ` — ${item.detail}` : ""; + console.log(`${icon} ${item.label}${suffix}`); +} + +if (failed.length > 0) { + console.error(`\nRelease verification failed: ${failed.length} issue(s) found.`); + process.exit(1); +} + +console.log("\nRelease verification passed."); diff --git a/src/i18n/locale.ts b/src/i18n/locale.ts index bd034aa..c10363c 100644 --- a/src/i18n/locale.ts +++ b/src/i18n/locale.ts @@ -3,12 +3,12 @@ import { getLanguage } from 'obsidian'; export const translations = { 'en-GB': { // Command names - currentFile: 'Current file', + currentFile: 'Open gallery for current note', clearImageCache: 'Clear image cache', // Main plugin loadingPlugin: 'Loading plugin v', - openGallery: 'Open gallery', + openGallery: 'Open image gallery', imageCacheCleared: 'Image cache cleared', noImagesFound: 'No images found in current note', errorOpeningGallery: 'Error opening image gallery', @@ -25,7 +25,7 @@ export const translations = { cacheSizeZeroOrInvalid: 'Cache size is 0 or invalid', getCacheSizeFailed: 'Failed to get cache size:', unableToGetCacheSize: 'Unable to get cache size, please try to reinitialize cache', - cacheStatus: 'Cache Status', + cacheStatus: 'Cache status', currentCacheSize: 'Current cache size: {size} MB / {maxSize} MB', maxCacheSize: 'Maximum cache size', maxCacheSizeDesc: 'Maximum size for image cache: {size} MB', @@ -35,7 +35,7 @@ export const translations = { clearCache: 'Clear cache', clearCacheDesc: 'Delete all cached images', clearAllCache: 'Clear all cache', - developer: 'Developer', + developer: 'Developer tools', debugMode: 'Debug mode', debugModeDesc: 'Enable debug mode to log detailed information to the console.', debugModeStatus: 'Debug mode {status}', @@ -73,12 +73,12 @@ export const translations = { }, 'zh': { // Command names - currentFile: '当前文件', + currentFile: '打开当前笔记图片墙', clearImageCache: '清除图片缓存', // Main plugin loadingPlugin: '加载插件 v', - openGallery: '打开图片墙', + openGallery: '打开图片画廊', imageCacheCleared: '图片缓存已清除', noImagesFound: '当前笔记中未找到图片', errorOpeningGallery: '打开图片墙时出错', @@ -105,7 +105,7 @@ export const translations = { clearCache: '清除缓存', clearCacheDesc: '删除所有缓存的图片', clearAllCache: '清除全部缓存', - developer: '开发者', + developer: '开发者工具', debugMode: '调试模式', debugModeDesc: '启用调试模式以在控制台中记录详细信息。', debugModeStatus: '调试模式已{status}', @@ -162,15 +162,3 @@ export function t(key: keyof typeof translations["en-GB"], params?: Record).moment); - console.debug('========================'); -} diff --git a/src/service/current-note-image-gallery-service.ts b/src/service/current-note-image-gallery-service.ts index 9d638c0..ce22044 100644 --- a/src/service/current-note-image-gallery-service.ts +++ b/src/service/current-note-image-gallery-service.ts @@ -5,17 +5,6 @@ import {RetryHandler} from "../utils/retry-handler"; import {ResourceManager} from "../utils/resource-manager"; import {t} from "../i18n/locale"; -// 定义Electron请求接口(用于向后兼容) -interface ElectronRequest { - abort(): void; -} - -interface ImageRequest { - controller?: AbortController; - electronRequest?: ElectronRequest; - timestamp: number; -} - interface ImageData { path: string; element: HTMLElement; @@ -40,7 +29,6 @@ export class CurrentNoteImageGalleryService extends Modal { private images: string[] = []; private loadedImages = 0; private totalImages = 0; - private currentRequests: Map = new Map(); private imageDataMap: Map = new Map(); private queueImageLoad: (imagePath: string, isVisible?: boolean) => void = () => { }; @@ -69,7 +57,6 @@ export class CurrentNoteImageGalleryService extends Modal { onOpen() { this.loadedImages = 0; - this.currentRequests.clear(); this.imageDataMap.clear(); const {contentEl} = this; @@ -521,40 +508,6 @@ export class CurrentNoteImageGalleryService extends Modal { } } - private createImageElement(imagePath: string, imageWall: HTMLElement) { - const imageDiv = imageWall.createDiv('nig-image-item'); - imageDiv.setAttribute('data-path', imagePath); - - // 存储图片元素引用 - this.imageDataMap.set(imagePath, { - path: imagePath, - element: imageDiv, - isLoading: false, - hasError: false - }); - - // 监听此元素以实现懒加载 - this.intersectionObserver?.observe(imageDiv); - - // 添加点击事件用于查看大图 - imageDiv.addEventListener('click', (e) => { - // 阻止事件冒泡,避免与其他插件(如 Image Toolkit)冲突 - e.stopPropagation(); - e.preventDefault(); - - const currentIndex = this.images.indexOf(imagePath); - this.createLightboxWithNavigation(currentIndex); - }); - - imageDiv.addEventListener('contextmenu', (e) => { - e.preventDefault(); - const img = imageDiv.querySelector('img'); - if (img) { - this.createContextMenu(e, img); - } - }); - } - /** * 分批渲染图片元素,避免阻塞主线程 */ @@ -1323,185 +1276,6 @@ export class CurrentNoteImageGalleryService extends Modal { } } - private async loadWeiboImage( - imagePath: string, - img: HTMLImageElement, - imageDiv: HTMLElement, - loadingText: HTMLElement, - retryCount = 0 - ): Promise { - try { - // 异步获取缓存 - const cachedImage = await this.plugin.imageCacheService.getCachedImage(imagePath); - - if (cachedImage) { - log.debug(() => `Loading Weibo image from cache: ${imagePath}`); - await new Promise((resolve, reject) => { - img.onload = () => { - this.handleImageLoadSuccess(img, imageDiv, loadingText, imagePath); - resolve(); - }; - - img.onerror = (e) => { - const error = e instanceof Error ? e : new Error('Cached Weibo image load error'); - log.error(() => `Cached Weibo image load error`, error); - reject(error); - }; - - // 设置图片源为缓存的base64数据 - img.src = cachedImage.data; - }).catch(async (e) => { - await this.loadWeiboImageWithoutCache(imagePath, img, imageDiv, loadingText, retryCount, () => {}, () => {}); - }); - return; - } - - await this.loadWeiboImageWithoutCache(imagePath, img, imageDiv, loadingText, retryCount, () => {}, () => {}); - } catch (error) { - const currentRequestId = imageDiv.getAttribute('data-request-id'); - this.handleError(error instanceof Error ? error : new Error(String(error)), imageDiv, currentRequestId || undefined, retryCount); - throw error; - } - } - - private async loadWeiboImageWithoutCache( - imagePath: string, - img: HTMLImageElement, - imageDiv: HTMLElement, - loadingText: HTMLElement, - retryCount: number, - resolve: () => void, - reject: (error: unknown) => void - ): Promise { - // 使用 Obsidian 的 requestUrl API 代替 electron.remote - const MAX_RETRIES = 3; - - try { - log.debug(() => `Loading Weibo image via requestUrl (no cache): ${imagePath}`); - - const response = await requestUrl({ - url: imagePath, - method: 'GET', - headers: { - 'Referer': 'https://weibo.com/', - 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' - } - }); - - if (response.status !== 200) { - throw new Error(`HTTP Error: ${response.status}`); - } - - const arrayBuffer = response.arrayBuffer; - const contentType = response.headers['content-type'] || 'image/jpeg'; - const blob = new Blob([arrayBuffer], {type: contentType}); - - // 使用 ResourceManager 管理 objectURL - const objectUrl = this.resourceManager.createObjectURL(imagePath, blob); - - img.onload = () => { - if (loadingText && loadingText.parentNode) { - loadingText.remove(); - } - - // 移除占位符 - const placeholder = imageDiv.querySelector('.nig-image-placeholder'); - if (placeholder) { - placeholder.remove(); - } - - const ratio = img.naturalHeight / img.naturalWidth; - const baseHeight = 10; - const heightSpan = Math.min(Math.ceil(ratio * baseHeight), 30); - - imageDiv.setCssStyles({ gridRowEnd: `span ${heightSpan}` }); - img.setCssStyles({ opacity: '1' }); - this.loadedImages++; - this.updateProgressBar(); - - const imageData = this.imageDataMap.get(imagePath); - if (imageData) { - imageData.isLoading = false; - } - - resolve(); - }; - - img.onerror = async () => { - if (retryCount < MAX_RETRIES) { - log.debug(() => `Retrying Weibo image load (${retryCount + 1}/${MAX_RETRIES}): ${imagePath}`); - await this.loadWeiboImage(imagePath, img, imageDiv, loadingText, retryCount + 1); - resolve(); - } else { - this.handleImageLoadFailure(imageDiv, t('loadingFailed')); - reject(new Error('Max retries reached')); - } - }; - - img.src = objectUrl; - - const imageData = this.imageDataMap.get(imagePath); - if (imageData) { - imageData.objectUrl = objectUrl; - } - - // 尝试缓存图片 - try { - await this.plugin.imageCacheService.cacheImage( - imagePath, - arrayBuffer, - undefined, - contentType - ); - log.debug(() => `Weibo image cached: ${imagePath}`); - } catch (cacheError) { - log.error(() => `Failed to cache Weibo image: ${imagePath}`, cacheError instanceof Error ? cacheError : undefined); - } - } catch (error) { - log.error(() => `requestUrl failed to load Weibo image: ${imagePath}`, error instanceof Error ? error : undefined); - - const imageData = this.imageDataMap.get(imagePath); - if (imageData) { - imageData.isLoading = false; - } - - if (retryCount < MAX_RETRIES) { - log.debug(() => `Retrying after request failure (${retryCount + 1}/${MAX_RETRIES}): ${imagePath}`); - window.setTimeout(() => { - this.loadWeiboImage(imagePath, img, imageDiv, loadingText, retryCount + 1) - .then(resolve) - .catch(reject); - }, 1000 * (retryCount + 1)); - } else { - this.handleImageLoadFailure(imageDiv, t('loadingFailed')); - reject(error); - } - } - } - - private handleError(error: Error, imageDiv: HTMLElement, requestId: string | undefined, retryCount: number) { - const MAX_RETRIES = 3; - - log.error(() => 'Error loading Weibo image:', error instanceof Error ? error : undefined); - if (requestId) { - this.currentRequests.delete(requestId); - } - - if (retryCount < MAX_RETRIES) { - log.debug(() => `Retrying after error (${retryCount + 1}/${MAX_RETRIES})`); - window.setTimeout(() => { - const img = imageDiv.querySelector('img'); - const loadingText = imageDiv.querySelector('.nig-loading-text'); - if (img && loadingText) { - const imgSrc = img.src; - void this.loadWeiboImage(imgSrc, img, imageDiv, loadingText as HTMLElement, retryCount + 1); - } - }, 1000 * (retryCount + 1)); - } else { - this.handleImageLoadFailure(imageDiv, t('loadingFailed')); - } - } - private updateProgressBar() { const progressEl = this.contentEl.querySelector('progress'); const progressText = this.contentEl.querySelector('.nig-progress-text'); @@ -1569,16 +1343,6 @@ export class CurrentNoteImageGalleryService extends Modal { return dest.path; } - // 如果是文件名(没有路径),尝试在库中查找 - if (!cleanLink.includes('/')) { - const allFiles = this.app.vault.getFiles(); - const matchedFiles = allFiles.filter(f => f.name === cleanLink); - if (matchedFiles.length > 0) { - log.debug(() => `Found by filename: ${matchedFiles[0].path}`); - return matchedFiles[0].path; - } - } - // 如果都失败了,尝试添加_resources前缀 if (!cleanLink.startsWith('_resources/')) { const resourcePath = `_resources/${cleanLink}`; @@ -2048,19 +1812,6 @@ export class CurrentNoteImageGalleryService extends Modal { onClose() { this.isClosed = true; - this.currentRequests.forEach((request) => { - try { - if (request.controller) { - request.controller.abort(); - } else if (request.electronRequest) { - request.electronRequest.abort(); - } - } catch (e) { - log.error(() => 'Error aborting request:', e instanceof Error ? e : undefined); - } - }); - this.currentRequests.clear(); - this.resourceManager.revokeAll(); this.imageDataMap.clear(); diff --git a/src/service/image-cache-service.ts b/src/service/image-cache-service.ts index 539f5ac..752ef40 100644 --- a/src/service/image-cache-service.ts +++ b/src/service/image-cache-service.ts @@ -41,7 +41,6 @@ export class ImageCacheService { constructor(app: App, plugin: Plugin) { this.app = app; this.pluginDir = plugin.manifest.dir!; - void this.loadCacheIndex(); } /** diff --git a/src/service/image-extractor-service.ts b/src/service/image-extractor-service.ts index 6cbffad..356d3de 100644 --- a/src/service/image-extractor-service.ts +++ b/src/service/image-extractor-service.ts @@ -42,14 +42,18 @@ class HybridImageExtractor implements ImageExtractor { * 处理格式:![[图片文件名]] */ class WikiImageExtractor implements ImageExtractor { - // 修改正则:不匹配后面紧跟 ]( 的情况(混合格式) - private readonly regex = /!\[\[(.*?)]](?!\()/g; + private readonly regex = /!\[\[(.*?)]]/g; extract(content: string): string[] { const images: string[] = []; let match; while ((match = this.regex.exec(content)) !== null) { + const nextChar = content[match.index + match[0].length]; + if (nextChar === '(') { + continue; + } + if (match[1]) { const imagePath = this.cleanImagePath(match[1]); if (imagePath) { @@ -79,8 +83,7 @@ class WikiImageExtractor implements ImageExtractor { * 处理格式:![alt](image.jpg) */ class MarkdownImageExtractor implements ImageExtractor { - // 修改正则:不匹配前面是 ]] 的情况(混合格式) - private readonly regex = /(? f.name === filename); - for (const file of matchingFiles) { - result.push(file.path); - } - } - - const allFiles = this.app.vault.getFiles(); - const matchingPathFiles = allFiles.filter(f => f.path.includes(originalPath)); - for (const file of matchingPathFiles) { - result.push(file.path); } return [...new Set(result)]; diff --git a/versions.json b/versions.json index eed9f20..7337b88 100644 --- a/versions.json +++ b/versions.json @@ -2,5 +2,7 @@ "1.0.0": "0.15.0", "1.0.1": "1.8.4", "1.0.2": "1.8.4", - "1.0.3": "1.8.4" + "1.0.3": "1.8.4", + "1.0.11": "1.9.14", + "1.1.0": "1.9.14" }