diff --git a/README.md b/README.md index 09ee14b..889666b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,10 @@ A powerful and fast native CBZ (Comic Book Archive) reader for Obsidian. Read your favorite mangas, comics, and graphic novels directly inside your Obsidian vault with a beautifully crafted, high-performance interface. -![](images/) + +![](images/Obsidian_TMmY6ZAHJQ.png) +![](images/Obsidian_OXUlwf8IZ1.gif) + ## ✨ Features diff --git a/esbuild.config.mjs b/esbuild.config.mjs deleted file mode 100644 index 5cdd616..0000000 --- a/esbuild.config.mjs +++ /dev/null @@ -1,48 +0,0 @@ -import esbuild from "esbuild"; -import process from "process"; -import builtins from "builtin-modules"; -import fs from "fs"; - -const prod = (process.argv[2] === "production"); - -const context = await esbuild.context({ - banner: { - js: '/* eslint-disable */', - }, - entryPoints: ["src/main.ts"], - bundle: true, - external: [ - "obsidian", - "electron", - "@codemirror/autocomplete", - "@codemirror/collab", - "@codemirror/commands", - "@codemirror/language", - "@codemirror/lint", - "@codemirror/search", - "@codemirror/state", - "@codemirror/view", - "@lezer/common", - "@lezer/highlight", - "@lezer/lr", - ...builtins], - format: "cjs", - target: "es2018", - logLevel: "info", - sourcemap: prod ? false : "inline", - treeShaking: true, - outfile: "main.js", -}); - -if (prod) { - await context.rebuild(); - - // Patch main.js to remove script tags created by JSZip setImmediate polyfill - let mainJs = fs.readFileSync("main.js", "utf8"); - mainJs = mainJs.replace(/createElement\(['"]script['"]\)/gi, 'createElement("div")'); - fs.writeFileSync("main.js", mainJs); - - process.exit(0); -} else { - await context.watch(); -} diff --git a/images/Obsidian_OXUlwf8IZ1.gif b/images/Obsidian_OXUlwf8IZ1.gif new file mode 100644 index 0000000..ce4c84a Binary files /dev/null and b/images/Obsidian_OXUlwf8IZ1.gif differ diff --git a/manifest.json b/manifest.json index 0b5b07c..536ecba 100644 --- a/manifest.json +++ b/manifest.json @@ -3,7 +3,7 @@ "name": "CBZ Reader", "version": "1.0.3", "minAppVersion": "1.0.0", - "description": "A powerful and fast native CBZ - Comic Book Archive reader", + "description": "A powerful and fast native CBZ Comic Book Archive reader", "author": "usero2-endofday", "authorUrl": "https://github.com/usero2", "fundingUrl": { diff --git a/package.json b/package.json deleted file mode 100644 index cab89b1..0000000 --- a/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "cbz-reader", - "version": "1.0.3", - "description": "Read CBZ files in Obsidian.", - "main": "main.js", - "scripts": { - "dev": "node esbuild.config.mjs", - "build": "node esbuild.config.mjs production" - }, - "keywords": [], - "author": "Author", - "license": "MIT", - "devDependencies": { - "@types/node": "^16.11.29", - "@typescript-eslint/eslint-plugin": "5.29.0", - "@typescript-eslint/parser": "5.29.0", - "builtin-modules": "3.3.0", - "esbuild": "0.17.3", - "obsidian": "latest", - "tslib": "2.4.0", - "typescript": "4.7.4" - }, - "dependencies": { - "jszip": "^3.10.1" - } -} - - diff --git a/plugin.js b/plugin.js deleted file mode 100644 index 1bfefcb..0000000 --- a/plugin.js +++ /dev/null @@ -1,479 +0,0 @@ -const { Plugin, ItemView } = require('obsidian'); - -const CBZ_VIEW_TYPE = "cbz-view"; - -class CbzView extends ItemView { - constructor(leaf) { - super(leaf); - this.file = null; - this.zip = null; - this.imageFiles = []; - - this.observer = null; - this.loadedImages = new Map(); - this.imgElements = new Map(); - - this.minimapObserver = null; - this.canvasElements = new Map(); - this.minimapItems = new Map(); // Store to toggle 'is-active' class - } - - getViewType() { return CBZ_VIEW_TYPE; } - getDisplayText() { return this.file ? this.file.name : "CBZ Reader"; } - - async setState(state, result) { - await super.setState(state, result); - if (state.file) { - const file = this.app.vault.getAbstractFileByPath(state.file); - if (file) { - await this.onLoadFile(file); - } - } - } - - getState() { - return { - file: this.file ? this.file.path : null - }; - } - - clear() { - this.file = null; - this.contentEl.empty(); - } - - async onLoadFile(file) { - this.file = file; - this.contentEl.empty(); - - const layout = this.contentEl.createDiv({ cls: "cbz-reader-layout" }); - const mainView = layout.createDiv({ cls: "cbz-main-view" }); - const minimapView = layout.createDiv({ cls: "cbz-minimap" }); - - // --- Vertical Scrollbar Minimap Logic --- - const minimapThumb = minimapView.createDiv({ cls: "cbz-minimap-thumb" }); - - let isDown = false; - let startY; - let startYScreen; // Add startYScreen - let isDraggingThumb = false; - - const updateThumbHeight = () => { - let currentItemHeight = 145; - const firstItem = this.minimapItems.get(0); - if (firstItem) currentItemHeight = firstItem.offsetHeight + 5; - - // Average height of a comic page is roughly 1.5x its width. - // We use a constant reference height to prevent slider from jittering - let h = (mainView.clientHeight / 1000) * currentItemHeight; - return Math.max(20, Math.min(h, minimapView.clientHeight / 2)); - }; - - const onMouseMove = (e) => { - if (!isDown) return; - e.preventDefault(); - const y = e.pageY; - const walk = y - startY; - if (Math.abs(walk) > 3) isDraggingThumb = true; - - const tHeight = updateThumbHeight(); - const trackViewHeight = minimapView.clientHeight; - - // Calculate new physical screen position of thumb relative to minimap view - let yScreen = startYScreen + walk; - yScreen = Math.max(0, Math.min(trackViewHeight - tHeight, yScreen)); - - // Calculate percentage p of track traveled - let p = 0; - if (trackViewHeight > tHeight) { - p = yScreen / (trackViewHeight - tHeight); - } - - let currentItemHeight = 145; - const firstItem = this.minimapItems.get(0); - if (firstItem) currentItemHeight = firstItem.offsetHeight + 5; - - const trackContentHeight = Math.max(minimapView.scrollHeight, (this.imageFiles ? this.imageFiles.length : 0) * currentItemHeight); - const maxThumbTop = trackContentHeight - tHeight; - - // Calculate exact position on the scrollable track - const exactPosition = p * maxThumbTop; - - // Update UI instantly - minimapThumb.setCssStyles({ - top: exactPosition + 'px', - height: tHeight + 'px' - }); - minimapView.scrollTop = exactPosition - yScreen; - - // Map exactPosition back to targetIndex and percentage for mainView - const targetIndex = Math.floor(exactPosition / currentItemHeight); - let itemPercentage = (exactPosition - (targetIndex * currentItemHeight)) / currentItemHeight; - - if (!this.imageFiles || this.imageFiles.length === 0) return; - const clampedIndex = Math.max(0, Math.min(targetIndex, this.imageFiles.length - 1)); - - if (targetIndex !== clampedIndex) { - itemPercentage = targetIndex < 0 ? 0 : 1; - } - itemPercentage = Math.max(0, Math.min(1, itemPercentage)); - - const pageContainer = mainView.querySelector(`.cbz-page-container[data-index="${clampedIndex}"]`); - if (pageContainer) { - const parentRect = mainView.getBoundingClientRect(); - const rect = pageContainer.getBoundingClientRect(); - const absoluteOffsetTop = mainView.scrollTop + (rect.top - parentRect.top); - - mainView.scrollTop = absoluteOffsetTop + (itemPercentage * rect.height); - } - }; - - const onMouseUp = () => { - if (isDown) { - isDown = false; - minimapThumb.setCssStyles({ cursor: '' }); - setTimeout(() => { isDraggingThumb = false; }, 50); - } - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - }; - - minimapThumb.addEventListener('mousedown', (e) => { - if (e.button !== 0) return; // only left click - e.preventDefault(); - e.stopPropagation(); // prevent clicking the track below it - isDown = true; - isDraggingThumb = false; - startY = e.pageY; - - const rect = minimapThumb.getBoundingClientRect(); - const parentRect = minimapView.getBoundingClientRect(); - startYScreen = rect.top - parentRect.top; - - minimapThumb.setCssStyles({ cursor: 'grabbing' }); - - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - }); - - // Track Click Logic - minimapView.addEventListener('mousedown', (e) => { - if (e.button !== 0) return; - if (e.target === minimapThumb) return; // handled by thumb - - e.preventDefault(); - - const rect = minimapView.getBoundingClientRect(); - const clickY = e.clientY - rect.top + minimapView.scrollTop; - - let itemHeight = 145; - const firstItem = this.minimapItems.get(0); - if (firstItem) itemHeight = firstItem.offsetHeight + 5; - - const targetIndex = Math.floor(clickY / itemHeight); - - // Can't scroll immediately if imageFiles is empty, but this only fires after load - if (this.imageFiles && this.imageFiles.length > 0) { - const clampedIndex = Math.max(0, Math.min(targetIndex, this.imageFiles.length - 1)); - const pageContainer = mainView.querySelector(`.cbz-page-container[data-index="${clampedIndex}"]`); - if (pageContainer) { - pageContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - } - }); - - // --- Smooth Wheel Logic --- - minimapView.addEventListener('wheel', (e) => { - e.preventDefault(); - - // Multiply deltaY so the minimap acts as a "fast scroll" area - const multiplier = 4; - mainView.scrollBy({ top: e.deltaY * multiplier, behavior: 'auto' }); - }, { passive: false }); - - mainView.createEl("h3", { text: `Loading ${file.name}...` }); - - try { - const arrayBuffer = await this.app.vault.readBinary(file); - this.zip = await JSZip.loadAsync(arrayBuffer); - - this.imageFiles = Object.values(this.zip.files) - .filter(zipEntry => !zipEntry.dir && this.isImage(zipEntry.name)) - .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' })); - - mainView.empty(); - - if (this.imageFiles.length === 0) { - mainView.createEl("h3", { text: "No images found in this CBZ file." }); - return; - } - - // Observer for main reading view - this.observer = new IntersectionObserver(this.handleIntersection.bind(this), { - root: mainView, - rootMargin: "2000px 0px 2000px 0px", - threshold: 0 - }); - - // Observer for active page tracking removed - now handled synchronously in scroll event - - - // Observer for minimap canvas loading - this.minimapObserver = new IntersectionObserver(this.handleMinimapIntersection.bind(this), { - root: minimapView, - rootMargin: "500px 0px 500px 0px", - threshold: 0 - }); - - this.imageFiles.forEach((zipEntry, index) => { - // Main view items - const pageContainer = mainView.createDiv({ cls: "cbz-page-container" }); - pageContainer.dataset.index = index.toString(); - const img = pageContainer.createEl("img"); - this.imgElements.set(index, img); - - this.observer.observe(pageContainer); - - // Minimap items - const minimapItem = minimapView.createDiv({ cls: "cbz-minimap-item" }); - minimapItem.dataset.index = index.toString(); - const canvas = minimapItem.createEl("canvas"); - this.canvasElements.set(index, canvas); - this.minimapItems.set(index, minimapItem); - - this.minimapObserver.observe(minimapItem); - - // The track click logic handles minimapItem clicks now through event bubbling - - }); - - // --- Continuous Smooth Minimap Sync --- - mainView.addEventListener('scroll', () => { - if (isDraggingThumb) return; - - // Synchronously find the active page (first page with bottom > viewport top) - const parentRect = mainView.getBoundingClientRect(); - const containers = mainView.querySelectorAll('.cbz-page-container'); - let activeContainer = null; - - for (let i = 0; i < containers.length; i++) { - const rect = containers[i].getBoundingClientRect(); - if (rect.bottom > parentRect.top) { - activeContainer = containers[i]; - break; - } - } - - if (!activeContainer) return; - const indexStr = activeContainer.dataset.index; - - // Also update the 'is-active' class on minimap items synchronously! - this.minimapItems.forEach((item, idx) => { - if (idx.toString() === indexStr) item.classList.add('is-active'); - else item.classList.remove('is-active'); - }); - - const rect = activeContainer.getBoundingClientRect(); - const offset = parentRect.top - rect.top; - let percentage = offset / rect.height; - percentage = Math.max(0, Math.min(1, percentage)); - - let currentItemHeight = 145; - const firstItem = this.minimapItems.get(0); - if (firstItem) currentItemHeight = firstItem.offsetHeight + 5; - - const exactPosition = parseInt(indexStr) * currentItemHeight + (percentage * currentItemHeight); - const tHeight = updateThumbHeight(); - minimapThumb.setCssStyles({ - height: tHeight + 'px', - top: exactPosition + 'px' - }); - - const trackContentHeight = Math.max(minimapView.scrollHeight, (this.imageFiles ? this.imageFiles.length : 0) * currentItemHeight); - const trackViewHeight = minimapView.clientHeight; - - let p = 0; - const maxThumbTop = trackContentHeight - tHeight; - if (maxThumbTop > 0) { - p = exactPosition / maxThumbTop; - p = Math.max(0, Math.min(1, p)); - } - - const yScreen = p * (trackViewHeight - tHeight); - const targetScrollTop = exactPosition - yScreen; - - minimapView.scrollTop = targetScrollTop; - }); - - } catch (err) { - console.error("Failed to load CBZ", err); - mainView.empty(); - mainView.createEl("h3", { text: `Error loading CBZ: ${err.message}` }); - } - } - - async handleIntersection(entries) { - if (!this.zip) return; - for (const entry of entries) { - const indexStr = entry.target.dataset.index; - if (!indexStr) continue; - - const index = parseInt(indexStr); - const img = this.imgElements.get(index); - if (!img) continue; - - if (entry.isIntersecting) { - if (!this.loadedImages.has(index)) { - this.loadImage(index, img); - } - } else { - if (this.loadedImages.has(index)) { - this.unloadImage(index, img); - } - } - } - } - - async handleMinimapIntersection(entries) { - if (!this.zip) return; - for (const entry of entries) { - const indexStr = entry.target.dataset.index; - if (!indexStr) continue; - - const index = parseInt(indexStr); - const canvas = this.canvasElements.get(index); - if (!canvas) continue; - - if (entry.isIntersecting) { - if (!canvas.dataset.loaded) { - this.loadMinimapCanvas(index, canvas); - } - } - // Note: We don't unload canvas to save memory because drawn canvas uses very little memory - // compared to an Object URL, and keeping them prevents re-extraction. - } - } - - async loadMinimapCanvas(index, canvas) { - canvas.dataset.loaded = "loading"; - try { - const zipEntry = this.imageFiles[index]; - const blob = await zipEntry.async("blob"); - - const ext = zipEntry.name.split('.').pop(); - let mimeType = "image/jpeg"; - if (ext && ext.toLowerCase() === 'png') mimeType = "image/png"; - else if (ext && ext.toLowerCase() === 'webp') mimeType = "image/webp"; - else if (ext && ext.toLowerCase() === 'gif') mimeType = "image/gif"; - - const typedBlob = new Blob([blob], { type: mimeType }); - const url = URL.createObjectURL(typedBlob); - - const img = new Image(); - img.onload = () => { - // Downscale to save memory. Width = 100px. - const maxWidth = 100; - const ratio = maxWidth / img.width; - const width = maxWidth; - const height = img.height * ratio; - - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, width, height); - - // Immediately revoke object url so memory is freed! - URL.revokeObjectURL(url); - canvas.dataset.loaded = "true"; - }; - img.onerror = () => { - URL.revokeObjectURL(url); - canvas.dataset.loaded = "error"; - }; - img.src = url; - - } catch (err) { - console.error(`Failed to load minimap image at index ${index}`, err); - canvas.dataset.loaded = "error"; - } - } - - async loadImage(index, img) { - this.loadedImages.set(index, "loading"); - try { - const zipEntry = this.imageFiles[index]; - const blob = await zipEntry.async("blob"); - - const ext = zipEntry.name.split('.').pop(); - let mimeType = "image/jpeg"; - if (ext && ext.toLowerCase() === 'png') mimeType = "image/png"; - else if (ext && ext.toLowerCase() === 'webp') mimeType = "image/webp"; - else if (ext && ext.toLowerCase() === 'gif') mimeType = "image/gif"; - - const typedBlob = new Blob([blob], { type: mimeType }); - const url = URL.createObjectURL(typedBlob); - - this.loadedImages.set(index, url); - img.src = url; - - if (img.parentElement) { - img.parentElement.setCssStyles({ minHeight: 'auto' }); - } - } catch (err) { - console.error(`Failed to load image at index ${index}`, err); - this.loadedImages.delete(index); - } - } - - unloadImage(index, img) { - const url = this.loadedImages.get(index); - if (url && url !== "loading") { - URL.revokeObjectURL(url); - img.removeAttribute("src"); - if (img.parentElement) { - img.parentElement.setCssStyles({ minHeight: `${img.height || 800}px` }); - } - } - this.loadedImages.delete(index); - } - - isImage(filename) { - const ext = filename.split('.').pop(); - return ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp'].includes(ext ? ext.toLowerCase() : ''); - } - - async onClose() { - if (this.observer) { - this.observer.disconnect(); - this.observer = null; - } - if (this.minimapObserver) { - this.minimapObserver.disconnect(); - this.minimapObserver = null; - } - for (const url of Array.from(this.loadedImages.values())) { - if (url !== "loading") { - URL.revokeObjectURL(url); - } - } - this.loadedImages.clear(); - this.imgElements.clear(); - this.canvasElements.clear(); - this.minimapItems.clear(); - this.zip = null; - this.imageFiles = []; - } -} - -class CBZReaderPlugin extends Plugin { - async onload() { - console.log('loading cbz-reader plugin'); - this.registerView(CBZ_VIEW_TYPE, (leaf) => new CbzView(leaf)); - this.registerExtensions(["cbz"], CBZ_VIEW_TYPE); - } - onunload() { - console.log('unloading cbz-reader plugin'); - } -} - -module.exports = CBZReaderPlugin; diff --git a/src/CbzView.ts b/src/CbzView.ts deleted file mode 100644 index b04337f..0000000 --- a/src/CbzView.ts +++ /dev/null @@ -1,480 +0,0 @@ -import { ItemView, WorkspaceLeaf, TFile } from "obsidian"; -import * as JSZip from "jszip"; - -export const CBZ_VIEW_TYPE = "cbz-view"; - -export class CbzView extends ItemView { - file: any = null; - zip: any = null; - imageFiles: any[] = []; - observer: any = null; - minimapObserver: any = null; - - loadedImages: Map = new Map(); - imgElements: Map = new Map(); - canvasElements: Map = new Map(); - minimapItems: Map = new Map(); - - constructor(leaf: WorkspaceLeaf) { - super(leaf); - this.file = null; - this.zip = null; - this.imageFiles = []; - - this.observer = null; - this.loadedImages = new Map(); - this.imgElements = new Map(); - - this.minimapObserver = null; - this.canvasElements = new Map(); - this.minimapItems = new Map(); // Store to toggle 'is-active' class - } - - getViewType() { return CBZ_VIEW_TYPE; } - getDisplayText() { return this.file ? this.file.name : "CBZ Reader"; } - - async setState(state, result) { - await super.setState(state, result); - if (state.file) { - const file = this.app.vault.getAbstractFileByPath(state.file); - if (file) { - await this.onLoadFile(file); - } - } - } - - getState() { - return { - file: this.file ? this.file.path : null - }; - } - - clear() { - this.file = null; - this.contentEl.empty(); - } - - async onLoadFile(file: TFile) { - this.file = file; - this.contentEl.empty(); - - const layout = this.contentEl.createDiv({ cls: "cbz-reader-layout" }); - const mainView = layout.createDiv({ cls: "cbz-main-view" }); - const minimapView = layout.createDiv({ cls: "cbz-minimap" }); - - // --- Vertical Scrollbar Minimap Logic --- - const minimapThumb = minimapView.createDiv({ cls: "cbz-minimap-thumb" }); - - let isDown = false; - let startY; - let startYScreen; // Add startYScreen - let isDraggingThumb = false; - - const updateThumbHeight = () => { - let currentItemHeight = 145; - const firstItem = this.minimapItems.get(0); - if (firstItem) currentItemHeight = firstItem.offsetHeight + 5; - - // Average height of a comic page is roughly 1.5x its width. - // We use a constant reference height to prevent slider from jittering - let h = (mainView.clientHeight / 1000) * currentItemHeight; - return Math.max(20, Math.min(h, minimapView.clientHeight / 2)); - }; - - const onMouseMove = (e) => { - if (!isDown) return; - e.preventDefault(); - const y = e.pageY; - const walk = y - startY; - if (Math.abs(walk) > 3) isDraggingThumb = true; - - const tHeight = updateThumbHeight(); - const trackViewHeight = minimapView.clientHeight; - - // Calculate new physical screen position of thumb relative to minimap view - let yScreen = startYScreen + walk; - yScreen = Math.max(0, Math.min(trackViewHeight - tHeight, yScreen)); - - // Calculate percentage p of track traveled - let p = 0; - if (trackViewHeight > tHeight) { - p = yScreen / (trackViewHeight - tHeight); - } - - let currentItemHeight = 145; - const firstItem = this.minimapItems.get(0); - if (firstItem) currentItemHeight = firstItem.offsetHeight + 5; - - const trackContentHeight = Math.max(minimapView.scrollHeight, (this.imageFiles ? this.imageFiles.length : 0) * currentItemHeight); - const maxThumbTop = trackContentHeight - tHeight; - - // Calculate exact position on the scrollable track - const exactPosition = p * maxThumbTop; - - // Update UI instantly - minimapThumb.setCssStyles({ - top: exactPosition + 'px', - height: tHeight + 'px' - }); - minimapView.scrollTop = exactPosition - yScreen; - - // Map exactPosition back to targetIndex and percentage for mainView - const targetIndex = Math.floor(exactPosition / currentItemHeight); - let itemPercentage = (exactPosition - (targetIndex * currentItemHeight)) / currentItemHeight; - - if (!this.imageFiles || this.imageFiles.length === 0) return; - const clampedIndex = Math.max(0, Math.min(targetIndex, this.imageFiles.length - 1)); - - if (targetIndex !== clampedIndex) { - itemPercentage = targetIndex < 0 ? 0 : 1; - } - itemPercentage = Math.max(0, Math.min(1, itemPercentage)); - - const pageContainer = mainView.querySelector(`.cbz-page-container[data-index="${clampedIndex}"]`); - if (pageContainer) { - const parentRect = mainView.getBoundingClientRect(); - const rect = pageContainer.getBoundingClientRect(); - const absoluteOffsetTop = mainView.scrollTop + (rect.top - parentRect.top); - - mainView.scrollTop = absoluteOffsetTop + (itemPercentage * rect.height); - } - }; - - const onMouseUp = () => { - if (isDown) { - isDown = false; - minimapThumb.setCssStyles({ cursor: '' }); - setTimeout(() => { isDraggingThumb = false; }, 50); - } - document.removeEventListener('mousemove', onMouseMove); - document.removeEventListener('mouseup', onMouseUp); - }; - - minimapThumb.addEventListener('mousedown', (e) => { - if (e.button !== 0) return; // only left click - e.preventDefault(); - e.stopPropagation(); // prevent clicking the track below it - isDown = true; - isDraggingThumb = false; - startY = e.pageY; - - const rect = minimapThumb.getBoundingClientRect(); - const parentRect = minimapView.getBoundingClientRect(); - startYScreen = rect.top - parentRect.top; - - minimapThumb.setCssStyles({ cursor: 'grabbing' }); - - document.addEventListener('mousemove', onMouseMove); - document.addEventListener('mouseup', onMouseUp); - }); - - // Track Click Logic - minimapView.addEventListener('mousedown', (e) => { - if (e.button !== 0) return; - if (e.target === minimapThumb) return; // handled by thumb - - e.preventDefault(); - - const rect = minimapView.getBoundingClientRect(); - const clickY = e.clientY - rect.top + minimapView.scrollTop; - - let itemHeight = 145; - const firstItem = this.minimapItems.get(0); - if (firstItem) itemHeight = firstItem.offsetHeight + 5; - - const targetIndex = Math.floor(clickY / itemHeight); - - // Can't scroll immediately if imageFiles is empty, but this only fires after load - if (this.imageFiles && this.imageFiles.length > 0) { - const clampedIndex = Math.max(0, Math.min(targetIndex, this.imageFiles.length - 1)); - const pageContainer = mainView.querySelector(`.cbz-page-container[data-index="${clampedIndex}"]`); - if (pageContainer) { - pageContainer.scrollIntoView({ behavior: 'smooth', block: 'start' }); - } - } - }); - - // --- Smooth Wheel Logic --- - minimapView.addEventListener('wheel', (e) => { - e.preventDefault(); - - // Multiply deltaY so the minimap acts as a "fast scroll" area - const multiplier = 4; - mainView.scrollBy({ top: e.deltaY * multiplier, behavior: 'auto' }); - }, { passive: false }); - - mainView.createEl("h3", { text: `Loading ${file.name}...` }); - - try { - const arrayBuffer = await this.app.vault.readBinary(file); - this.zip = await JSZip.loadAsync(arrayBuffer); - - this.imageFiles = Object.values(this.zip.files) - .filter(zipEntry => !zipEntry.dir && this.isImage(zipEntry.name)) - .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' })); - - mainView.empty(); - - if (this.imageFiles.length === 0) { - mainView.createEl("h3", { text: "No images found in this CBZ file." }); - return; - } - - // Observer for main reading view - this.observer = new IntersectionObserver(this.handleIntersection.bind(this), { - root: mainView, - rootMargin: "2000px 0px 2000px 0px", - threshold: 0 - }); - - // Observer for active page tracking removed - now handled synchronously in scroll event - - - // Observer for minimap canvas loading - this.minimapObserver = new IntersectionObserver(this.handleMinimapIntersection.bind(this), { - root: minimapView, - rootMargin: "500px 0px 500px 0px", - threshold: 0 - }); - - this.imageFiles.forEach((zipEntry, index) => { - // Main view items - const pageContainer = mainView.createDiv({ cls: "cbz-page-container" }); - pageContainer.dataset.index = index.toString(); - const img = pageContainer.createEl("img"); - this.imgElements.set(index, img); - - this.observer.observe(pageContainer); - - // Minimap items - const minimapItem = minimapView.createDiv({ cls: "cbz-minimap-item" }); - minimapItem.dataset.index = index.toString(); - const canvas = minimapItem.createEl("canvas"); - this.canvasElements.set(index, canvas); - this.minimapItems.set(index, minimapItem); - - this.minimapObserver.observe(minimapItem); - - // The track click logic handles minimapItem clicks now through event bubbling - - }); - - // --- Continuous Smooth Minimap Sync --- - mainView.addEventListener('scroll', () => { - if (isDraggingThumb) return; - - // Synchronously find the active page (first page with bottom > viewport top) - const parentRect = mainView.getBoundingClientRect(); - const containers = mainView.querySelectorAll('.cbz-page-container'); - let activeContainer = null; - - for (let i = 0; i < containers.length; i++) { - const rect = containers[i].getBoundingClientRect(); - if (rect.bottom > parentRect.top) { - activeContainer = containers[i]; - break; - } - } - - if (!activeContainer) return; - const indexStr = activeContainer.dataset.index; - - // Also update the 'is-active' class on minimap items synchronously! - this.minimapItems.forEach((item, idx) => { - if (idx.toString() === indexStr) item.classList.add('is-active'); - else item.classList.remove('is-active'); - }); - - const rect = activeContainer.getBoundingClientRect(); - const offset = parentRect.top - rect.top; - let percentage = offset / rect.height; - percentage = Math.max(0, Math.min(1, percentage)); - - let currentItemHeight = 145; - const firstItem = this.minimapItems.get(0); - if (firstItem) currentItemHeight = firstItem.offsetHeight + 5; - - const exactPosition = parseInt(indexStr) * currentItemHeight + (percentage * currentItemHeight); - const tHeight = updateThumbHeight(); - minimapThumb.setCssStyles({ - height: tHeight + 'px', - top: exactPosition + 'px' - }); - - const trackContentHeight = Math.max(minimapView.scrollHeight, (this.imageFiles ? this.imageFiles.length : 0) * currentItemHeight); - const trackViewHeight = minimapView.clientHeight; - - let p = 0; - const maxThumbTop = trackContentHeight - tHeight; - if (maxThumbTop > 0) { - p = exactPosition / maxThumbTop; - p = Math.max(0, Math.min(1, p)); - } - - const yScreen = p * (trackViewHeight - tHeight); - const targetScrollTop = exactPosition - yScreen; - - minimapView.scrollTop = targetScrollTop; - }); - - } catch (err) { - console.error("Failed to load CBZ", err); - mainView.empty(); - mainView.createEl("h3", { text: `Error loading CBZ: ${err.message}` }); - } - } - - async handleIntersection(entries: any[]) { - if (!this.zip) return; - for (const entry of entries) { - const indexStr = entry.target.dataset.index; - if (!indexStr) continue; - - const index = parseInt(indexStr); - const img = this.imgElements.get(index); - if (!img) continue; - - if (entry.isIntersecting) { - if (!this.loadedImages.has(index)) { - this.loadImage(index, img); - } - } else { - if (this.loadedImages.has(index)) { - this.unloadImage(index, img); - } - } - } - } - - async handleMinimapIntersection(entries: any[]) { - if (!this.zip) return; - for (const entry of entries) { - const indexStr = entry.target.dataset.index; - if (!indexStr) continue; - - const index = parseInt(indexStr); - const canvas = this.canvasElements.get(index); - if (!canvas) continue; - - if (entry.isIntersecting) { - if (!canvas.dataset.loaded) { - this.loadMinimapCanvas(index, canvas); - } - } - // Note: We don't unload canvas to save memory because drawn canvas uses very little memory - // compared to an Object URL, and keeping them prevents re-extraction. - } - } - - async loadMinimapCanvas(index: number, canvas: any) { - canvas.dataset.loaded = "loading"; - try { - const zipEntry = this.imageFiles[index]; - const blob = await zipEntry.async("blob"); - - const ext = zipEntry.name.split('.').pop(); - let mimeType = "image/jpeg"; - if (ext && ext.toLowerCase() === 'png') mimeType = "image/png"; - else if (ext && ext.toLowerCase() === 'webp') mimeType = "image/webp"; - else if (ext && ext.toLowerCase() === 'gif') mimeType = "image/gif"; - - const typedBlob = new Blob([blob], { type: mimeType }); - const url = URL.createObjectURL(typedBlob); - - const img = new Image(); - img.onload = () => { - // Downscale to save memory. Width = 100px. - const maxWidth = 100; - const ratio = maxWidth / img.width; - const width = maxWidth; - const height = img.height * ratio; - - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, width, height); - - // Immediately revoke object url so memory is freed! - URL.revokeObjectURL(url); - canvas.dataset.loaded = "true"; - }; - img.onerror = () => { - URL.revokeObjectURL(url); - canvas.dataset.loaded = "error"; - }; - img.src = url; - - } catch (err) { - console.error(`Failed to load minimap image at index ${index}`, err); - canvas.dataset.loaded = "error"; - } - } - - async loadImage(index: number, img: any) { - this.loadedImages.set(index, "loading"); - try { - const zipEntry = this.imageFiles[index]; - const blob = await zipEntry.async("blob"); - - const ext = zipEntry.name.split('.').pop(); - let mimeType = "image/jpeg"; - if (ext && ext.toLowerCase() === 'png') mimeType = "image/png"; - else if (ext && ext.toLowerCase() === 'webp') mimeType = "image/webp"; - else if (ext && ext.toLowerCase() === 'gif') mimeType = "image/gif"; - - const typedBlob = new Blob([blob], { type: mimeType }); - const url = URL.createObjectURL(typedBlob); - - this.loadedImages.set(index, url); - img.src = url; - - if (img.parentElement) { - img.parentElement.setCssStyles({ minHeight: 'auto' }); - } - } catch (err) { - console.error(`Failed to load image at index ${index}`, err); - this.loadedImages.delete(index); - } - } - - unloadImage(index: number, img: any) { - const url = this.loadedImages.get(index); - if (url && url !== "loading") { - URL.revokeObjectURL(url); - img.removeAttribute("src"); - if (img.parentElement) { - img.parentElement.setCssStyles({ minHeight: `${img.height || 800}px` }); - } - } - this.loadedImages.delete(index); - } - - isImage(filename: string) { - const ext = filename.split('.').pop(); - return ['jpg', 'jpeg', 'png', 'webp', 'gif', 'bmp'].includes(ext ? ext.toLowerCase() : ''); - } - - async onClose() { - if (this.observer) { - this.observer.disconnect(); - this.observer = null; - } - if (this.minimapObserver) { - this.minimapObserver.disconnect(); - this.minimapObserver = null; - } - for (const url of Array.from(this.loadedImages.values())) { - if (url !== "loading") { - URL.revokeObjectURL(url); - } - } - this.loadedImages.clear(); - this.imgElements.clear(); - this.canvasElements.clear(); - this.minimapItems.clear(); - this.zip = null; - this.imageFiles = []; - } - -} - diff --git a/src/main.ts b/src/main.ts deleted file mode 100644 index 108e5ed..0000000 --- a/src/main.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Plugin } from 'obsidian'; -import { CBZ_VIEW_TYPE, CbzView } from './CbzView'; - -export default class CBZReaderPlugin extends Plugin { - async onload() { - console.log('loading cbz-reader plugin'); - - this.registerView( - CBZ_VIEW_TYPE, - (leaf) => new CbzView(leaf) - ); - - this.registerExtensions(["cbz"], CBZ_VIEW_TYPE); - } - - onunload() { - console.log('unloading cbz-reader plugin'); - } -} diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index c44b729..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "inlineSourceMap": true, - "inlineSources": true, - "module": "ESNext", - "target": "ES6", - "allowJs": true, - "noImplicitAny": true, - "moduleResolution": "node", - "importHelpers": true, - "isolatedModules": true, - "strictNullChecks": true, - "lib": [ - "DOM", - "ES5", - "ES6", - "ES7" - ] - }, - "include": [ - "**/*.ts" - ] -}