From 03f2b0278d85b26400800c143f3bdce57ddf37d3 Mon Sep 17 00:00:00 2001 From: Devon22 Date: Sat, 15 Mar 2025 00:47:00 +0800 Subject: [PATCH] 1.8.12 --- main.ts | 11 +- manifest.json | 2 +- package-lock.json | 4 +- package.json | 2 +- src/galleryBlock.ts | 1705 ++++++++++++++++++----------------- src/galleryBlockGenerate.ts | 225 +++-- src/imageUpload.ts | 126 ++- src/translations.ts | 4 + styles.css | 66 +- versions.json | 2 +- 10 files changed, 1195 insertions(+), 952 deletions(-) diff --git a/main.ts b/main.ts index 683051a..ec83006 100644 --- a/main.ts +++ b/main.ts @@ -105,7 +105,16 @@ export default class MediaViewPlugin extends Plugin { if (activeView) { const editor = activeView.editor; const selectedText = editor.getSelection(); - new GalleryBlockGenerateModal(this.app, editor, selectedText).open(); + //new GalleryBlockGenerateModal(this.app, selectedText).open(); + // 創建並打開設定對話框 + const modal = new GalleryBlockGenerateModal(this.app, selectedText); + + // 設置確認後的回調函數,使用 vault.process 修改文件 + modal.onConfirm = (newGalleryBlock: string) => { + editor.replaceSelection(newGalleryBlock); + }; + + modal.open(); } else { new Notice(t('please_open_note')); } diff --git a/manifest.json b/manifest.json index 9047717..273f3b9 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "mediaviewer", "name": "Media Viewer", - "version": "1.8.11", + "version": "1.8.12", "minAppVersion": "1.1.0", "description": "View and manage media files within your notes.", "author": "Devon22", diff --git a/package-lock.json b/package-lock.json index 7508dc6..1dd06c4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediaviewer", - "version": "1.8.11", + "version": "1.8.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediaviewer", - "version": "1.8.11", + "version": "1.8.12", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.json b/package.json index 35c3e8f..40ceaa9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediaviewer", - "version": "1.8.11", + "version": "1.8.12", "description": "This is a sample plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/src/galleryBlock.ts b/src/galleryBlock.ts index 46b661c..3ef9055 100644 --- a/src/galleryBlock.ts +++ b/src/galleryBlock.ts @@ -1,816 +1,891 @@ -import { App, TFile } from 'obsidian'; -import MediaViewPlugin from '../main'; -import { FullScreenModal } from './fullscreen'; -import { MediaViewSettings } from './settings'; -import { ImageUploadModal } from './imageUpload'; -import { t } from './translations'; - -interface GalleryItem { - type: 'image' | 'video' | 'note' | string; - url?: string; - path?: string; - title?: { - type: 'internal' | 'external' | 'text' | string; - url?: string; - text: string; - } | string | null; - linkUrl?: string | null; - file?: TFile | null; - isInternalLink?: boolean; - isExternalLink?: boolean; - thumbnail?: string | null; -} - -interface ContainerInfo { - title: { - type: 'internal' | 'external' | 'text' | string; - url?: string; - text: string; - } | string | null; - addButtonEnabled: boolean; - gridSize: string; - paginationEnabled: number; -} - -interface MediaUrlsData { - items: GalleryItem[]; - containerInfo: ContainerInfo; - galleryId: string; -} - -export class GalleryBlock { - app: App; - plugin: MediaViewPlugin; - - constructor(app: App, plugin: MediaViewPlugin) { - this.app = app; - this.plugin = plugin; - } - - // 處理 gallery 區塊 - async processGalleryBlock(source: string, el: HTMLElement): Promise { - if (el.querySelector('.mvgb-media-gallery-grid')) { - return; - } - - try { - const mediaUrlsData = await this.parseGalleryContent(source); - const galleryDiv = this.createGalleryElement(mediaUrlsData); - el.appendChild(galleryDiv); - } catch (error) { - console.error('Error processing gallery block:', error); - } - } - - // 解析 gallery 內容 - async parseGalleryContent(content: string): Promise { - const items: GalleryItem[] = []; - const lines = content.split('\n'); - - let currentTitle = null; - let currentLinkUrl = null; - let currentThumbnail = null; - let containerTitle = null; - let addButtonEnabled = false; - let gridSize = this.plugin.settings.galleryGridSize || 'medium'; - let paginationEnabled = this.plugin.settings.itemsPerPage || 0; // 設定變數以決定是否開啟分頁功能 - - // 產生基於內容的唯一識別碼 - const galleryId = 'gallery-' + this.hashString(content.trim()); - - // 處理縮圖連結的輔助函數 - const processMediaLink = (linkText: string) => { - // 處理 Obsidian 內部連結 ![[file]] - const internalMatch = linkText.match(/!\[\[(.*?)\]\]/); - if (internalMatch) { - const file = this.app.metadataCache.getFirstLinkpathDest(internalMatch[1], ''); - if (file) { - return this.app.vault.getResourcePath(file); - } - } - - // 處理標準 Markdown 連結 ![alt](path) - const markdownMatch = linkText.match(/!\[(.*?)\]\((.*?)\)/); - if (markdownMatch) { - const url = markdownMatch[2].split(' "')[0]; - if (url.startsWith('http')) { - return url; - } else { - const file = this.app.metadataCache.getFirstLinkpathDest(url, ''); - if(!file) { - const fileByPath = this.app.vault.getAbstractFileByPath(url); - if (fileByPath && fileByPath instanceof TFile) { - return this.app.vault.getResourcePath(fileByPath); - } - } else { - return this.app.vault.getResourcePath(file); - } - } - } - - // 處理直接的 URL 或路徑 - if (linkText.startsWith('http')) { - return linkText; - } else { - const file = this.app.vault.getAbstractFileByPath(linkText); - if (file instanceof TFile) { - return this.app.vault.getResourcePath(file); - } - } - - return null; - }; - - // 新增:處理筆記內容中的第一張圖片 - const findFirstImageInNote = async (file: TFile) => { - try { - const content = await this.app.vault.cachedRead(file); - const internalMatch = content.match(/(?:!\[\[(.*?\.(?:jpg|jpeg|png|gif|webp))(?:\|.*?)?\]\]|!\[(.*?)\]\(\s*(\S+?(?:\.(?:jpg|jpeg|png|gif|webp)|format=(?:jpg|jpeg|png|gif|webp))[^\s)]*)\s*(?:\s+["'][^"']*["'])?\s*\))/i); - if (internalMatch) { - return internalMatch[0]; - } else { - return null; - } - } catch (error) { - console.error('Error reading note content:', error); - return null; - } - }; - - for (const line of lines) { - const trimmedLine = line.trim(); - if (!trimmedLine) continue; - - const paginationMatch = trimmedLine.match(/^pagination:\s*(\d+)$/i); - if (paginationMatch) { - const paginationValue = parseInt(paginationMatch[1]); - paginationEnabled = paginationValue || 0; - continue; - } - - // 新增 size 參數的處理 - const sizeMatch = trimmedLine.match(/^size:\s*(small|medium|large)$/i); - if (sizeMatch) { - gridSize = sizeMatch[1].toLowerCase(); - continue; - } - - // 檢查是否要禁用新增按鈕 - const addButtonMatch = trimmedLine.match(/^addbutton:\s*(true|false)$/i); - if (addButtonMatch) { - addButtonEnabled = addButtonMatch[1].toLowerCase() === 'true'; - continue; - } - - // 檢查是否為容器標題設定 - const containerTitleMatch = trimmedLine.match(/^title:\s*(.+)$/); - if (containerTitleMatch) { - const titleText = containerTitleMatch[1]; - - // 檢查是否為內部連結 [[note]] - const internalLinkMatch = titleText.match(/\[\[(.*?)(?:\|.*?)?\]\]/); - if (internalLinkMatch) { - const linktext = internalLinkMatch[1].split('|')[0]; - const file = this.app.metadataCache.getFirstLinkpathDest(linktext, ''); - containerTitle = { - text: linktext, - url: file ? file.path : linktext, - type: 'internal' - }; - } - // 檢查是否為外部連結 [text](url) - else { - const externalLinkMatch = titleText.match(/\[(.*?)\]\((.*?)\)/); - if (externalLinkMatch) { - containerTitle = { - text: externalLinkMatch[1], - url: externalLinkMatch[2], - type: 'external' - }; - } else { - // 純文字 - containerTitle = { - text: titleText, - type: 'text' - }; - } - } - continue; - } - - // 檢查是否為個別圖片的標題設定 - const titleMatch = trimmedLine.match(/^alt:\s*(.+)$/); - if (titleMatch) { - const titleText = titleMatch[1]; - - // 檢查是否為內部連結 - const internalLinkMatch = titleText.match(/\[\[(.*?)(?:\|.*?)?\]\]/); - if (internalLinkMatch) { - const linktext = internalLinkMatch[1].split('|')[0]; - const file = this.app.metadataCache.getFirstLinkpathDest(linktext, ''); - currentTitle = { - text: linktext, - url: file ? file.path : linktext, - type: 'internal' - }; - } - // 檢查是否為外部連結 - else { - const externalLinkMatch = titleText.match(/\[(.*?)\]\((.*?)\)/); - if (externalLinkMatch) { - currentTitle = { - text: externalLinkMatch[1], - url: externalLinkMatch[2], - type: 'external' - }; - } else { - // 純文字 - currentTitle = { - text: titleText, - type: 'text' - }; - } - } - continue; - } - - // 檢查是否為縮圖設定 - const thumbnailMatch = trimmedLine.match(/^img:\s*(.+)$/); - if (thumbnailMatch) { - const imageUrl = thumbnailMatch[1].trim(); - const isImageFile = /\.(jpg|jpeg|png|gif|webp|bmp|svg|tiff|tif)$/i.test(imageUrl) || /^https?:\/\/.*\.(jpg|jpeg|png|gif|webp|bmp|svg|tiff|tif)$/i.test(imageUrl); - if (isImageFile) { - currentThumbnail = processMediaLink(`![Image](${imageUrl})`); - } else { - currentThumbnail = processMediaLink(imageUrl); - } - continue; - } - - // 處理筆記連結 - const internalMatch = line.match(/(!?)\[\[(.*?)(?:\|.*?)?\]\]/); - if (internalMatch) { - const [_, isImage, linktext] = internalMatch; - const actualLinktext = linktext.split('|')[0].split('#')[0]; - const file = this.app.metadataCache.getFirstLinkpathDest(actualLinktext, ''); - - if (isImage && !currentThumbnail) { - // 處理一般的圖片/媒體連結 - if (file) { - const extension = file.extension.toLowerCase(); - if (extension.match(/^(jpg|jpeg|png|gif|webp|mp4|mkv|mov|webm)$/)) { - items.push({ - type: extension.match(/^(jpg|jpeg|png|gif|webp)$/) ? 'image' : 'video', - url: this.app.vault.getResourcePath(file), - path: file.path, - title: currentTitle, - linkUrl: currentLinkUrl - }); - } - } - } else { - // 處理筆記連結 - if (file && !currentThumbnail) { - // 尋找筆記中的第一張圖片 - const firstImage = await findFirstImageInNote(file); - if (firstImage) { - currentThumbnail = processMediaLink(firstImage); - } - } - - items.push({ - type: 'note', - title: currentTitle || actualLinktext, - path: file ? file.path : actualLinktext, - linkUrl: currentLinkUrl, - file: file, - isInternalLink: true, - thumbnail: currentThumbnail - }); - } - currentTitle = null; - currentLinkUrl = null; - currentThumbnail = null; - continue; - } - - // 處理標準 Markdown 連結 - const markdownMatch = line.match(/(!?)\[(.*?)(?:\|.*?)?\]\((.*?)\)/); - if (markdownMatch) { - const [_, isImage, text, url] = markdownMatch; - - if (isImage && !currentThumbnail) { - // 處理一般的圖片連結 - if (!url.startsWith('http://') && !url.startsWith('https://')) { - // 使用 getFirstLinkpathDest 來解析路徑 - const file = this.app.metadataCache.getFirstLinkpathDest(url, ''); - if (!file) { - // 如果找不到檔案,再嘗試直接用路徑查找 - const fileByPath = this.app.vault.getAbstractFileByPath(url); - if (fileByPath && fileByPath instanceof TFile) { - const extension = url.toLowerCase(); - items.push({ - type: extension.match(/\.(mp4|mkv|mov|webm)$/) ? 'video' : 'image', - url: this.app.vault.getResourcePath(fileByPath), - path: text || fileByPath.path, - title: currentTitle, - linkUrl: currentLinkUrl - }); - } - } else { - const extension = file.extension.toLowerCase(); - items.push({ - type: extension.match(/^(mp4|mkv|mov|webm)$/) ? 'video' : 'image', - url: this.app.vault.getResourcePath(file), - path: text || file.path, - title: currentTitle, - linkUrl: currentLinkUrl - }); - } - } else { - const urlForTypeCheck = url.split(' "')[0].split('?')[0].toLowerCase(); - const isImageFile = urlForTypeCheck.match(/\.(mp4|mkv|mov|webm)$/); - items.push({ - type: isImageFile ? 'video' : 'image', - url: url.split(' "')[0], - path: text, - title: currentTitle, - linkUrl: currentLinkUrl - }); - } - } else { - // 處理一般連結 - items.push({ - type: 'note', - title: currentTitle || text, - path: url, - linkUrl: url, - isExternalLink: url.startsWith('http://') || url.startsWith('https://'), - thumbnail: currentThumbnail - }); - } - currentTitle = null; - currentLinkUrl = null; - currentThumbnail = null; - continue; - } - } - - return { - items, - containerInfo: { - title: containerTitle, - addButtonEnabled: addButtonEnabled, - gridSize: gridSize, - paginationEnabled: paginationEnabled - }, - galleryId: galleryId - }; - } - - hashString(str: string) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - const char = str.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; // Convert to 32bit integer - } - return Math.abs(hash).toString(36); - } - - createGalleryElement(mediaUrlsData: MediaUrlsData) { - const { items, containerInfo, galleryId } = mediaUrlsData; - const titleDiv = document.createElement('div'); - const galleryDiv = document.createElement('div'); - galleryDiv.className = 'mvgb-media-gallery-grid'; - - // 使用從 mediaUrlsData 中取得的 galleryId - galleryDiv.setAttribute('data-gallery-id', galleryId); - - // 根據 size 參數添加對應的 class 並設定寬度 - galleryDiv.addClass(`size-${containerInfo.gridSize}`); - const propertyName = `galleryGridSize${containerInfo.gridSize.charAt(0).toUpperCase() + containerInfo.gridSize.slice(1)}` as keyof MediaViewSettings; - const width = this.plugin.settings[propertyName]; - galleryDiv.style.gridTemplateColumns = `repeat(auto-fill, minmax(${width}px, 1fr))`; - - // 處理容器標題 - if (typeof containerInfo.title === 'object' && containerInfo.title !== null) { - const containerLinkArea = titleDiv.createEl('div', { - cls: 'mvgb-container-link-area' - }); - - if (containerInfo.title.type === 'text') { - // 純文字 - const textSpan = containerLinkArea.createEl('span', { - text: containerInfo.title.text - }); - } else { - // 內部或外部連結 - const link = containerLinkArea.createEl('a', { - text: containerInfo.title.text - }); - if (containerInfo.title.url) { - if (containerInfo.title.type === 'internal') { - // 內部連結 - const file = this.app.vault.getAbstractFileByPath(containerInfo.title.url); - if (file instanceof TFile) { - link.onclick = (e) => { - e.preventDefault(); - const leaf = this.app.workspace.getLeaf('tab'); - leaf.openFile(file); - }; - } - } else { - // 外部連結 - link.href = encodeURI(containerInfo.title.url); - link.target = '_blank'; - link.rel = 'noopener noreferrer'; - link.onclick = (e) => { - e.stopPropagation(); - }; - } - } - } - - galleryDiv.style.borderRadius = '0 8px 8px 8px'; - } - - const container = document.createElement('div'); - container.className = 'mvgb-media-container'; - container.appendChild(titleDiv); - container.appendChild(galleryDiv); - - // 處理分頁和新增按鈕 - const itemsPerPage = containerInfo.paginationEnabled || this.plugin.settings.itemsPerPage || 0; - - // 當 itemsPerPage 為 0 或項目數量不足時,顯示所有項目 - if (itemsPerPage <= 0 || items.length <= itemsPerPage) { - // 顯示所有項目 - items.forEach((item: GalleryItem, index: number) => { - if (item.type === 'note') { - const noteContainer = this.createNoteContainer(item); - galleryDiv.appendChild(noteContainer); - } else { - const mediaContainer = this.createMediaContainer(item, index); - galleryDiv.appendChild(mediaContainer); - } - }); - - // 不使用分頁時,如果允許新增圖片,則顯示傳統的新增按鈕 - if (containerInfo.addButtonEnabled || items.length === 0) { - const addContainer = document.createElement('div'); - addContainer.className = 'mv-media-thumbnail-container mvgb-add-media-button'; - - const addIcon = addContainer.createDiv('mvgb-add-media-icon'); - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('viewBox', '0 0 24 24'); - svg.setAttribute('width', '24'); - svg.setAttribute('height', '24'); - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('fill', 'currentColor'); - path.setAttribute('d', 'M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z'); - svg.appendChild(path); - addIcon.appendChild(svg); - - const addIconText = document.createElement('div'); - addIconText.className = 'mvgb-add-media-text'; - addIconText.textContent = t('add_image'); - addIcon.appendChild(addIconText); - - addContainer.onclick = () => { - const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv); - modal.open(); - }; - - galleryDiv.appendChild(addContainer); - } - } else { - // 使用分頁顯示 - const totalPages = Math.ceil(items.length / itemsPerPage); - const currentPage = 1; - - const controlsDiv = document.createElement('div'); - controlsDiv.className = 'mvgb-gallery-controls'; - - const paginationDiv = document.createElement('div'); - paginationDiv.className = 'mvgb-pagination'; - - const prevPageButton = paginationDiv.createEl('button', { - cls: 'mvgb-gallery-control-button prev-page-button', - text: t('prev_page') - }); - - const pageInfoSpan = paginationDiv.createEl('span', { - cls: 'mvgb-page-info', - text: `${currentPage} / ${totalPages}` - }); - - const nextPageButton = paginationDiv.createEl('button', { - cls: 'mvgb-gallery-control-button next-page-button', - text: t('next_page') - }); - - // 儲存當前頁碼到按鈕上,以便更新時使用 - paginationDiv.dataset.currentPage = currentPage.toString(); - - prevPageButton.onclick = () => { - const currentPage = parseInt(paginationDiv.dataset.currentPage || '0'); - // 如果是第一頁,則跳到最後一頁,否則往前一頁 - if (currentPage !== 0) { - const newPage = currentPage === 1 ? totalPages : currentPage - 1; - paginationDiv.dataset.currentPage = newPage.toString(); - this.updateGalleryPage(galleryDiv, items, newPage, itemsPerPage); - } - }; - - nextPageButton.onclick = () => { - const currentPage = parseInt(paginationDiv.dataset.currentPage || '0'); - // 如果是最後一頁,則跳到第一頁,否則往後一頁 - if (currentPage !== 0) { - const newPage = currentPage === totalPages ? 1 : currentPage + 1; - paginationDiv.dataset.currentPage = newPage.toString(); - this.updateGalleryPage(galleryDiv, items, newPage, itemsPerPage); - } - }; - - // 新增圖片按鈕移到分頁控制項旁邊 - if (containerInfo.addButtonEnabled || items.length === 0) { - const addButton = paginationDiv.createEl('button', { - cls: 'mvgb-gallery-control-button mvgb-add-image-button', - text: t('add_image') - }); - addButton.onclick = () => { - const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv); - modal.open(); - }; - } - - paginationDiv.appendChild(prevPageButton); - paginationDiv.appendChild(pageInfoSpan); - paginationDiv.appendChild(nextPageButton); - controlsDiv.appendChild(paginationDiv); - container.appendChild(controlsDiv); - - // 初始化第一頁 - this.updateGalleryPage(galleryDiv, items, currentPage, itemsPerPage); - } - - // 加入拖曳事件處理 - galleryDiv.addEventListener('dragover', (e) => { - e.preventDefault(); - e.stopPropagation(); - galleryDiv.addClass('drag-over'); - }); - - galleryDiv.addEventListener('dragleave', (e) => { - e.preventDefault(); - e.stopPropagation(); - galleryDiv.removeClass('drag-over'); - }); - - galleryDiv.addEventListener('drop', async (e) => { - e.preventDefault(); - e.stopPropagation(); - galleryDiv.removeClass('drag-over'); - - const files = []; - - // 處理所有拖放的項目 - for (const item of (e.dataTransfer as any).items) { - if (item.kind === 'string') { - if (item.type === 'text/uri-list') { - // 處理 URI 列表 - const uriPromise = new Promise(resolve => { - item.getAsString((string: string) => { - resolve({ type: 'uri', getData: () => string }); - }); - }); - files.push(uriPromise); - } else if (item.type === 'text/plain') { - const textPromise = new Promise(resolve => { - item.getAsString((string: string) => { - resolve({ type: 'text', getData: () => string }); - }); - }); - files.push(textPromise); - } - } else if (item.kind === 'file') { - // 處理一般檔案 - files.push(item.getAsFile()); - } - } - - const resolvedFiles = await Promise.all(files); - const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv); - await modal.handleFiles(resolvedFiles); - }); - - return container; - } - - createNoteContainer(item: GalleryItem) { - const container = document.createElement('div'); - container.className = 'mv-media-thumbnail-container mvgb-note-thumbnail'; - - const notePreview = document.createElement('div'); - notePreview.className = 'mvgb-note-preview'; - - // 如果有縮圖,使用縮圖 - if (item.thumbnail) { - const img = document.createElement('img'); - img.src = item.thumbnail; - img.className = 'mvgb-note-thumbnail-image'; - notePreview.appendChild(img); - } else { - // 否則使用預設圖示 - const noteIcon = document.createElement('div'); - noteIcon.className = 'mvgb-note-icon'; - - if (item.isExternalLink) { - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('viewBox', '0 0 24 24'); - svg.setAttribute('width', '24'); - svg.setAttribute('height', '24'); - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('fill', 'currentColor'); - path.setAttribute('d', 'M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z'); - svg.appendChild(path); - noteIcon.appendChild(svg); - noteIcon.classList.add('external-link'); - } else { - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('viewBox', '0 0 24 24'); - svg.setAttribute('width', '24'); - svg.setAttribute('height', '24'); - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('fill', 'currentColor'); - path.setAttribute('d', 'M14,17H7V15H14V17M17,13H7V11H17V13M17,9H7V7H17V9M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z'); - svg.appendChild(path); - noteIcon.appendChild(svg); - noteIcon.classList.add('internal-link'); - } - notePreview.appendChild(noteIcon); - } - - const noteTitle = createEl('div', { cls: 'mvgb-note-title', text: item.title as string }); - notePreview.appendChild(noteTitle); - - container.appendChild(notePreview); - - container.onclick = (event) => { - event.stopPropagation(); - if (item.isExternalLink) { - // 外部連結:在新分頁開啟 - if (item.linkUrl) { - window.open(item.linkUrl, '_blank', 'noopener,noreferrer'); - } - } else if (item.isInternalLink && item.file) { - // 內部連結:在新分頁開啟筆記 - const leaf = this.app.workspace.getLeaf('tab'); - leaf.openFile(item.file); - } else { - // 其他連結:嘗試在新分頁開啟 - window.open(item.path, '_blank', 'noopener,noreferrer'); - } - }; - - return container; - } - - updateGalleryPage(galleryDiv: HTMLElement, items: GalleryItem[], page: number, itemsPerPage: number) { - const start = (page - 1) * itemsPerPage; - const end = start + itemsPerPage; - const currentPageItems = items.slice(start, end); - const totalPages = Math.ceil(items.length / itemsPerPage); - - if(!galleryDiv) return; - - // 移除舊的項目 - galleryDiv.replaceChildren(); - - // 新增新的項目 - currentPageItems.forEach((item, index) => { - if (item.type === 'note') { - const noteContainer = this.createNoteContainer(item); - galleryDiv.appendChild(noteContainer); - } else { - const mediaContainer = this.createMediaContainer(item, start + index); - galleryDiv.appendChild(mediaContainer); - } - }); - - // 如果是最後一頁且項目數量不足,添加空的佔位元素 - if (page === totalPages && currentPageItems.length < itemsPerPage) { - const placeholdersNeeded = itemsPerPage - currentPageItems.length; - for (let i = 0; i < placeholdersNeeded; i++) { - const placeholder = document.createElement('div'); - placeholder.className = 'mv-media-thumbnail-container placeholder'; - placeholder.style.visibility = 'hidden'; // 隱藏但保持佔位 - galleryDiv.appendChild(placeholder); - } - } - - // 更新分頁控制項 - if (galleryDiv.parentElement) { - const paginationDiv = galleryDiv.parentElement.querySelector('.mvgb-pagination'); - if (paginationDiv) { - const pageInfoSpan = paginationDiv.querySelector('.mvgb-page-info'); - if (pageInfoSpan) { - pageInfoSpan.textContent = `${page} / ${totalPages}`; - } - } - } - } - - createMediaContainer(media: GalleryItem, index: number) { - const container = document.createElement('div'); - container.className = 'mv-media-thumbnail-container'; - - if (media.type === 'image') { - const img = document.createElement('img') as HTMLImageElement; - if (media.url) { - img.src = media.url; - img.alt = media.path || ''; - img.style.pointerEvents = 'none'; - container.appendChild(img); - } - } else { - const video = document.createElement('video') as HTMLVideoElement; - if (media.url) { - video.src = media.url; - video.style.pointerEvents = 'none'; - container.appendChild(video); - } - - const videoIcon = document.createElement('div'); - videoIcon.className = 'mv-video-indicator'; - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('viewBox', '0 0 24 24'); - svg.setAttribute('width', '24'); - svg.setAttribute('height', '24'); - const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); - path.setAttribute('d', 'M8 5v14l11-7z'); - svg.appendChild(path); - videoIcon.appendChild(svg); - container.appendChild(videoIcon); - } - - if (typeof media.title === 'object' && media.title !== null) { - const linkArea = document.createElement('div'); - linkArea.className = 'mvgb-media-link-area'; - - if (media.title.type === 'text') { - // 純文字 - const textSpan = document.createElement('span'); - textSpan.textContent = media.title.text; - textSpan.style.color = 'white'; - linkArea.appendChild(textSpan); - } else { - // 內部或外部連結 - const link = document.createElement('a'); - link.textContent = media.title.text; - - if (media.title.url) { - if (media.title.type === 'internal') { - // 內部連結 - const file = this.app.vault.getAbstractFileByPath(media.title.url); - if (file instanceof TFile) { - link.onclick = (e) => { - e.preventDefault(); - e.stopPropagation(); - const leaf = this.app.workspace.getLeaf('tab'); - leaf.openFile(file); - }; - } - } else { - // 外部連結 - link.href = encodeURI(media.title.url); - link.target = '_blank'; - link.rel = 'noopener noreferrer'; - link.onclick = (e) => { - e.stopPropagation(); - }; - } - } - - linkArea.appendChild(link); - } - - container.appendChild(linkArea); - } - - container.onclick = () => { - const modal = new FullScreenModal(this.app, this.plugin, 'thumbnail'); - modal.open(); - setTimeout(() => { - const allUrls = modal.mediaUrls; - const targetIndex = allUrls.findIndex(m => m.url === media.url); - if (targetIndex !== -1) { - modal.showMedia(targetIndex); - } - }, 100); - }; - - return container; - } +import { App, TFile, Menu, Notice } from 'obsidian'; +import MediaViewPlugin from '../main'; +import { FullScreenModal } from './fullscreen'; +import { MediaViewSettings } from './settings'; +import { ImageUploadModal } from './imageUpload'; +import { GalleryBlockGenerateModal } from './galleryBlockGenerate'; +import { t } from './translations'; + +interface GalleryItem { + type: 'image' | 'video' | 'note' | string; + url?: string; + path?: string; + title?: { + type: 'internal' | 'external' | 'text' | string; + url?: string; + text: string; + } | string | null; + linkUrl?: string | null; + file?: TFile | null; + isInternalLink?: boolean; + isExternalLink?: boolean; + thumbnail?: string | null; +} + +interface ContainerInfo { + title: { + type: 'internal' | 'external' | 'text' | string; + url?: string; + text: string; + } | string | null; + addButtonEnabled: boolean; + gridSize: string; + paginationEnabled: number; +} + +interface MediaUrlsData { + items: GalleryItem[]; + containerInfo: ContainerInfo; + galleryId: string; +} + +export class GalleryBlock { + app: App; + plugin: MediaViewPlugin; + + constructor(app: App, plugin: MediaViewPlugin) { + this.app = app; + this.plugin = plugin; + } + + // 處理 gallery 區塊 + async processGalleryBlock(source: string, el: HTMLElement): Promise { + if (el.querySelector('.mvgb-media-gallery-grid')) { + return; + } + + try { + const mediaUrlsData = await this.parseGalleryContent(source); + const galleryDiv = this.createGalleryElement(mediaUrlsData); + el.appendChild(galleryDiv); + } catch (error) { + console.error('Error processing gallery block:', error); + } + } + + // 解析 gallery 內容 + async parseGalleryContent(content: string): Promise { + const items: GalleryItem[] = []; + const lines = content.split('\n'); + + let currentTitle = null; + let currentLinkUrl = null; + let currentThumbnail = null; + let containerTitle = null; + let addButtonEnabled = false; + let gridSize = this.plugin.settings.galleryGridSize || 'medium'; + let paginationEnabled = this.plugin.settings.itemsPerPage || 0; // 設定變數以決定是否開啟分頁功能 + + // 產生基於內容的唯一識別碼 + const galleryId = 'gallery-' + this.hashString(content.trim()); + + // 處理縮圖連結的輔助函數 + const processMediaLink = (linkText: string) => { + // 處理 Obsidian 內部連結 ![[file]] + const internalMatch = linkText.match(/!\[\[(.*?)\]\]/); + if (internalMatch) { + const file = this.app.metadataCache.getFirstLinkpathDest(internalMatch[1], ''); + if (file) { + return this.app.vault.getResourcePath(file); + } + } + + // 處理標準 Markdown 連結 ![alt](path) + const markdownMatch = linkText.match(/!\[(.*?)\]\((.*?)\)/); + if (markdownMatch) { + const url = markdownMatch[2].split(' "')[0]; + if (url.startsWith('http')) { + return url; + } else { + const file = this.app.metadataCache.getFirstLinkpathDest(url, ''); + if(!file) { + const fileByPath = this.app.vault.getAbstractFileByPath(url); + if (fileByPath && fileByPath instanceof TFile) { + return this.app.vault.getResourcePath(fileByPath); + } + } else { + return this.app.vault.getResourcePath(file); + } + } + } + + // 處理直接的 URL 或路徑 + if (linkText.startsWith('http')) { + return linkText; + } else { + const file = this.app.vault.getAbstractFileByPath(linkText); + if (file instanceof TFile) { + return this.app.vault.getResourcePath(file); + } + } + + return null; + }; + + // 新增:處理筆記內容中的第一張圖片 + const findFirstImageInNote = async (file: TFile) => { + try { + const content = await this.app.vault.cachedRead(file); + const internalMatch = content.match(/(?:!\[\[(.*?\.(?:jpg|jpeg|png|gif|webp))(?:\|.*?)?\]\]|!\[(.*?)\]\(\s*(\S+?(?:\.(?:jpg|jpeg|png|gif|webp)|format=(?:jpg|jpeg|png|gif|webp))[^\s)]*)\s*(?:\s+["'][^"']*["'])?\s*\))/i); + if (internalMatch) { + return internalMatch[0]; + } else { + return null; + } + } catch (error) { + console.error('Error reading note content:', error); + return null; + } + }; + + for (const line of lines) { + const trimmedLine = line.trim(); + if (!trimmedLine) continue; + + const paginationMatch = trimmedLine.match(/^pagination:\s*(\d+)$/i); + if (paginationMatch) { + const paginationValue = parseInt(paginationMatch[1]); + paginationEnabled = paginationValue || 0; + continue; + } + + // 新增 size 參數的處理 + const sizeMatch = trimmedLine.match(/^size:\s*(small|medium|large)$/i); + if (sizeMatch) { + gridSize = sizeMatch[1].toLowerCase(); + continue; + } + + // 檢查是否要禁用新增按鈕 + const addButtonMatch = trimmedLine.match(/^addbutton:\s*(true|false)$/i); + if (addButtonMatch) { + addButtonEnabled = addButtonMatch[1].toLowerCase() === 'true'; + continue; + } + + // 檢查是否為容器標題設定 + const containerTitleMatch = trimmedLine.match(/^title:\s*(.+)$/); + if (containerTitleMatch) { + const titleText = containerTitleMatch[1]; + + // 檢查是否為內部連結 [[note]] + const internalLinkMatch = titleText.match(/\[\[(.*?)(?:\|.*?)?\]\]/); + if (internalLinkMatch) { + const linktext = internalLinkMatch[1].split('|')[0]; + const file = this.app.metadataCache.getFirstLinkpathDest(linktext, ''); + containerTitle = { + text: linktext, + url: file ? file.path : linktext, + type: 'internal' + }; + } + // 檢查是否為外部連結 [text](url) + else { + const externalLinkMatch = titleText.match(/\[(.*?)\]\((.*?)\)/); + if (externalLinkMatch) { + containerTitle = { + text: externalLinkMatch[1], + url: externalLinkMatch[2], + type: 'external' + }; + } else { + // 純文字 + containerTitle = { + text: titleText, + type: 'text' + }; + } + } + continue; + } + + // 檢查是否為個別圖片的標題設定 + const titleMatch = trimmedLine.match(/^alt:\s*(.+)$/); + if (titleMatch) { + const titleText = titleMatch[1]; + + // 檢查是否為內部連結 + const internalLinkMatch = titleText.match(/\[\[(.*?)(?:\|.*?)?\]\]/); + if (internalLinkMatch) { + const linktext = internalLinkMatch[1].split('|')[0]; + const file = this.app.metadataCache.getFirstLinkpathDest(linktext, ''); + currentTitle = { + text: linktext, + url: file ? file.path : linktext, + type: 'internal' + }; + } + // 檢查是否為外部連結 + else { + const externalLinkMatch = titleText.match(/\[(.*?)\]\((.*?)\)/); + if (externalLinkMatch) { + currentTitle = { + text: externalLinkMatch[1], + url: externalLinkMatch[2], + type: 'external' + }; + } else { + // 純文字 + currentTitle = { + text: titleText, + type: 'text' + }; + } + } + continue; + } + + // 檢查是否為縮圖設定 + const thumbnailMatch = trimmedLine.match(/^img:\s*(.+)$/); + if (thumbnailMatch) { + const imageUrl = thumbnailMatch[1].trim(); + const isImageFile = /\.(jpg|jpeg|png|gif|webp|bmp|svg|tiff|tif)$/i.test(imageUrl) || /^https?:\/\/.*\.(jpg|jpeg|png|gif|webp|bmp|svg|tiff|tif)$/i.test(imageUrl); + if (isImageFile) { + currentThumbnail = processMediaLink(`![Image](${imageUrl})`); + } else { + currentThumbnail = processMediaLink(imageUrl); + } + continue; + } + + // 處理筆記連結 + const internalMatch = line.match(/(!?)\[\[(.*?)(?:\|.*?)?\]\]/); + if (internalMatch) { + const [_, isImage, linktext] = internalMatch; + const actualLinktext = linktext.split('|')[0].split('#')[0]; + const file = this.app.metadataCache.getFirstLinkpathDest(actualLinktext, ''); + + if (isImage && !currentThumbnail) { + // 處理一般的圖片/媒體連結 + if (file) { + const extension = file.extension.toLowerCase(); + if (extension.match(/^(jpg|jpeg|png|gif|webp|mp4|mkv|mov|webm)$/)) { + items.push({ + type: extension.match(/^(jpg|jpeg|png|gif|webp)$/) ? 'image' : 'video', + url: this.app.vault.getResourcePath(file), + path: file.path, + title: currentTitle, + linkUrl: currentLinkUrl + }); + } + } + } else { + // 處理筆記連結 + if (file && !currentThumbnail) { + // 尋找筆記中的第一張圖片 + const firstImage = await findFirstImageInNote(file); + if (firstImage) { + currentThumbnail = processMediaLink(firstImage); + } + } + + items.push({ + type: 'note', + title: currentTitle || actualLinktext, + path: file ? file.path : actualLinktext, + linkUrl: currentLinkUrl, + file: file, + isInternalLink: true, + thumbnail: currentThumbnail + }); + } + currentTitle = null; + currentLinkUrl = null; + currentThumbnail = null; + continue; + } + + // 處理標準 Markdown 連結 + const markdownMatch = line.match(/(!?)\[(.*?)(?:\|.*?)?\]\((.*?)\)/); + if (markdownMatch) { + const [_, isImage, text, url] = markdownMatch; + + if (isImage && !currentThumbnail) { + // 處理一般的圖片連結 + if (!url.startsWith('http://') && !url.startsWith('https://')) { + // 使用 getFirstLinkpathDest 來解析路徑 + const file = this.app.metadataCache.getFirstLinkpathDest(url, ''); + if (!file) { + // 如果找不到檔案,再嘗試直接用路徑查找 + const fileByPath = this.app.vault.getAbstractFileByPath(url); + if (fileByPath && fileByPath instanceof TFile) { + const extension = url.toLowerCase(); + items.push({ + type: extension.match(/\.(mp4|mkv|mov|webm)$/) ? 'video' : 'image', + url: this.app.vault.getResourcePath(fileByPath), + path: text || fileByPath.path, + title: currentTitle, + linkUrl: currentLinkUrl + }); + } + } else { + const extension = file.extension.toLowerCase(); + items.push({ + type: extension.match(/^(mp4|mkv|mov|webm)$/) ? 'video' : 'image', + url: this.app.vault.getResourcePath(file), + path: text || file.path, + title: currentTitle, + linkUrl: currentLinkUrl + }); + } + } else { + const urlForTypeCheck = url.split(' "')[0].split('?')[0].toLowerCase(); + const isImageFile = urlForTypeCheck.match(/\.(mp4|mkv|mov|webm)$/); + items.push({ + type: isImageFile ? 'video' : 'image', + url: url.split(' "')[0], + path: text, + title: currentTitle, + linkUrl: currentLinkUrl + }); + } + } else { + // 處理一般連結 + items.push({ + type: 'note', + title: currentTitle || text, + path: url, + linkUrl: url, + isExternalLink: url.startsWith('http://') || url.startsWith('https://'), + thumbnail: currentThumbnail + }); + } + currentTitle = null; + currentLinkUrl = null; + currentThumbnail = null; + continue; + } + } + + return { + items, + containerInfo: { + title: containerTitle, + addButtonEnabled: addButtonEnabled, + gridSize: gridSize, + paginationEnabled: paginationEnabled + }, + galleryId: galleryId + }; + } + + hashString(str: string) { + let hash = 0; + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return Math.abs(hash).toString(36); + } + + createGalleryElement(mediaUrlsData: MediaUrlsData) { + const { items, containerInfo, galleryId } = mediaUrlsData; + const titleDiv = document.createElement('div'); + const galleryDiv = document.createElement('div'); + galleryDiv.className = 'mvgb-media-gallery-grid'; + + // 使用從 mediaUrlsData 中取得的 galleryId + galleryDiv.setAttribute('data-gallery-id', galleryId); + + // 根據 size 參數添加對應的 class 並設定寬度 + galleryDiv.addClass(`size-${containerInfo.gridSize}`); + const propertyName = `galleryGridSize${containerInfo.gridSize.charAt(0).toUpperCase() + containerInfo.gridSize.slice(1)}` as keyof MediaViewSettings; + const width = this.plugin.settings[propertyName]; + galleryDiv.style.gridTemplateColumns = `repeat(auto-fill, minmax(${width}px, 1fr))`; + + // 處理容器標題 + if (typeof containerInfo.title === 'object' && containerInfo.title !== null) { + const containerLinkArea = titleDiv.createEl('div', { + cls: 'mvgb-container-link-area' + }); + + if (containerInfo.title.type === 'text') { + // 純文字 + const textSpan = containerLinkArea.createEl('span', { + text: containerInfo.title.text + }); + } else { + // 內部或外部連結 + const link = containerLinkArea.createEl('a', { + text: containerInfo.title.text + }); + if (containerInfo.title.url) { + if (containerInfo.title.type === 'internal') { + // 內部連結 + const file = this.app.vault.getAbstractFileByPath(containerInfo.title.url); + if (file instanceof TFile) { + link.onclick = (e) => { + e.preventDefault(); + const leaf = this.app.workspace.getLeaf('tab'); + leaf.openFile(file); + }; + } + } else { + // 外部連結 + link.href = encodeURI(containerInfo.title.url); + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.onclick = (e) => { + e.stopPropagation(); + }; + } + } + } + + galleryDiv.style.borderRadius = '0 8px 8px 8px'; + } + + const container = document.createElement('div'); + container.className = 'mvgb-media-container'; + container.appendChild(titleDiv); + container.appendChild(galleryDiv); + + // 處理分頁和新增按鈕 + const itemsPerPage = containerInfo.paginationEnabled || this.plugin.settings.itemsPerPage || 0; + + // 當 itemsPerPage 為 0 或項目數量不足時,顯示所有項目 + if (itemsPerPage <= 0 || items.length <= itemsPerPage) { + // 顯示所有項目 + items.forEach((item: GalleryItem, index: number) => { + if (item.type === 'note') { + const noteContainer = this.createNoteContainer(item); + galleryDiv.appendChild(noteContainer); + } else { + const mediaContainer = this.createMediaContainer(item, index); + galleryDiv.appendChild(mediaContainer); + } + }); + + // 不使用分頁時,如果允許新增圖片,則顯示傳統的新增按鈕 + if (containerInfo.addButtonEnabled || items.length === 0) { + const addContainer = document.createElement('div'); + addContainer.className = 'mv-media-thumbnail-container mvgb-add-media-button'; + + const addIcon = addContainer.createDiv('mvgb-add-media-icon'); + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('width', '24'); + svg.setAttribute('height', '24'); + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('fill', 'currentColor'); + path.setAttribute('d', 'M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z'); + svg.appendChild(path); + addIcon.appendChild(svg); + + const addIconText = document.createElement('div'); + addIconText.className = 'mvgb-add-media-text'; + addIconText.textContent = t('add_image'); + addIcon.appendChild(addIconText); + + addContainer.onclick = () => { + const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv); + modal.open(); + }; + + galleryDiv.appendChild(addContainer); + } + } else { + // 使用分頁顯示 + const totalPages = Math.ceil(items.length / itemsPerPage); + const currentPage = 1; + + const controlsDiv = document.createElement('div'); + controlsDiv.className = 'mvgb-gallery-controls'; + + const paginationDiv = document.createElement('div'); + paginationDiv.className = 'mvgb-pagination'; + + const prevPageButton = paginationDiv.createEl('button', { + cls: 'mvgb-gallery-control-button prev-page-button', + text: t('prev_page') + }); + + const pageInfoSpan = paginationDiv.createEl('span', { + cls: 'mvgb-page-info', + text: `${currentPage} / ${totalPages}` + }); + + const nextPageButton = paginationDiv.createEl('button', { + cls: 'mvgb-gallery-control-button next-page-button', + text: t('next_page') + }); + + // 儲存當前頁碼到按鈕上,以便更新時使用 + paginationDiv.dataset.currentPage = currentPage.toString(); + + prevPageButton.onclick = () => { + const currentPage = parseInt(paginationDiv.dataset.currentPage || '0'); + // 如果是第一頁,則跳到最後一頁,否則往前一頁 + if (currentPage !== 0) { + const newPage = currentPage === 1 ? totalPages : currentPage - 1; + paginationDiv.dataset.currentPage = newPage.toString(); + this.updateGalleryPage(galleryDiv, items, newPage, itemsPerPage); + } + }; + + nextPageButton.onclick = () => { + const currentPage = parseInt(paginationDiv.dataset.currentPage || '0'); + // 如果是最後一頁,則跳到第一頁,否則往後一頁 + if (currentPage !== 0) { + const newPage = currentPage === totalPages ? 1 : currentPage + 1; + paginationDiv.dataset.currentPage = newPage.toString(); + this.updateGalleryPage(galleryDiv, items, newPage, itemsPerPage); + } + }; + + // 新增圖片按鈕移到分頁控制項旁邊 + if (containerInfo.addButtonEnabled || items.length === 0) { + const addButton = paginationDiv.createEl('button', { + cls: 'mvgb-gallery-control-button mvgb-add-image-button', + text: t('add_image') + }); + addButton.onclick = () => { + const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv); + modal.open(); + }; + } + + paginationDiv.appendChild(prevPageButton); + paginationDiv.appendChild(pageInfoSpan); + paginationDiv.appendChild(nextPageButton); + controlsDiv.appendChild(paginationDiv); + container.appendChild(controlsDiv); + + // 初始化第一頁 + this.updateGalleryPage(galleryDiv, items, currentPage, itemsPerPage); + } + + // 加入拖曳事件處理 + galleryDiv.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + galleryDiv.addClass('drag-over'); + }); + + galleryDiv.addEventListener('dragleave', (e) => { + e.preventDefault(); + e.stopPropagation(); + galleryDiv.removeClass('drag-over'); + }); + + galleryDiv.addEventListener('drop', async (e) => { + e.preventDefault(); + e.stopPropagation(); + galleryDiv.removeClass('drag-over'); + + const files = []; + + // 處理所有拖放的項目 + for (const item of (e.dataTransfer as any).items) { + if (item.kind === 'string') { + if (item.type === 'text/uri-list') { + // 處理 URI 列表 + const uriPromise = new Promise(resolve => { + item.getAsString((string: string) => { + resolve({ type: 'uri', getData: () => string }); + }); + }); + files.push(uriPromise); + } else if (item.type === 'text/plain') { + const textPromise = new Promise(resolve => { + item.getAsString((string: string) => { + resolve({ type: 'text', getData: () => string }); + }); + }); + files.push(textPromise); + } + } else if (item.kind === 'file') { + // 處理一般檔案 + files.push(item.getAsFile()); + } + } + + const resolvedFiles = await Promise.all(files); + const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv); + await modal.handleFiles(resolvedFiles); + }); + + // 右鍵選單 + galleryDiv.addEventListener('contextmenu', (event) => { + event.preventDefault(); + const menu = new Menu(); + menu.addItem((item) => { + item + .setTitle(t('add_image')) + .setIcon("image") + .onClick(() => { + const modal = new ImageUploadModal(this.app, this.plugin, galleryDiv); + modal.open(); + }); + }); + menu.addItem((item) => { + item + .setTitle(t('setting_gallery')) + .setIcon("settings") + .onClick(() => { + // 獲取 gallery 區塊的 ID + const galleryId = galleryDiv.getAttribute('data-gallery-id'); + if (!galleryId) { + return; + } + + // 獲取當前筆記的文件 + const activeFile = this.app.workspace.getActiveFile(); + if (!activeFile) { + new Notice(t('please_open_note')); + return; + } + + // 讀取文件內容 + this.app.vault.read(activeFile).then((content) => { + // 尋找包含當前 gallery ID 的 gallery 區塊 + const galleryBlockRegex = /```gallery\n([\s\S]*?)```/g; + let match; + let galleryContent = ''; + let matchPosition = { start: 0, end: 0 }; + + while ((match = galleryBlockRegex.exec(content)) !== null) { + const blockContent = match[1]; + const blockId = 'gallery-' + this.hashString(blockContent.trim()); + if (blockId === galleryId) { + galleryContent = blockContent; + matchPosition.start = match.index; + matchPosition.end = match.index + match[0].length; + break; + } + } + + if (!galleryContent) { + new Notice(t('gallery_not_found')); + return; + } + + // 創建並打開設定對話框 + const modal = new GalleryBlockGenerateModal(this.app, galleryContent); + + // 設置確認後的回調函數,使用 vault.process 修改文件 + modal.onConfirm = (newGalleryBlock: string) => { + this.app.vault.process(activeFile, (fileContent) => { + return fileContent.substring(0, matchPosition.start) + + newGalleryBlock + + fileContent.substring(matchPosition.end); + }); + }; + + modal.open(); + }); + }); + }); + menu.showAtMouseEvent(event); + }); + + return container; + } + + createNoteContainer(item: GalleryItem) { + const container = document.createElement('div'); + container.className = 'mv-media-thumbnail-container mvgb-note-thumbnail'; + + const notePreview = document.createElement('div'); + notePreview.className = 'mvgb-note-preview'; + + // 如果有縮圖,使用縮圖 + if (item.thumbnail) { + const img = document.createElement('img'); + img.src = item.thumbnail; + img.className = 'mvgb-note-thumbnail-image'; + notePreview.appendChild(img); + } else { + // 否則使用預設圖示 + const noteIcon = document.createElement('div'); + noteIcon.className = 'mvgb-note-icon'; + + if (item.isExternalLink) { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('width', '24'); + svg.setAttribute('height', '24'); + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('fill', 'currentColor'); + path.setAttribute('d', 'M14,3V5H17.59L7.76,14.83L9.17,16.24L19,6.41V10H21V3M19,19H5V5H12V3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V12H19V19Z'); + svg.appendChild(path); + noteIcon.appendChild(svg); + noteIcon.classList.add('external-link'); + } else { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('width', '24'); + svg.setAttribute('height', '24'); + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('fill', 'currentColor'); + path.setAttribute('d', 'M14,17H7V15H14V17M17,13H7V11H17V13M17,9H7V7H17V9M19,3H5C3.89,3 3,3.89 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5C21,3.89 20.1,3 19,3Z'); + svg.appendChild(path); + noteIcon.appendChild(svg); + noteIcon.classList.add('internal-link'); + } + notePreview.appendChild(noteIcon); + } + + const noteTitle = createEl('div', { cls: 'mvgb-note-title', text: item.title as string }); + notePreview.appendChild(noteTitle); + + container.appendChild(notePreview); + + container.onclick = (event) => { + event.stopPropagation(); + if (item.isExternalLink) { + // 外部連結:在新分頁開啟 + if (item.linkUrl) { + window.open(item.linkUrl, '_blank', 'noopener,noreferrer'); + } + } else if (item.isInternalLink && item.file) { + // 內部連結:在新分頁開啟筆記 + const leaf = this.app.workspace.getLeaf('tab'); + leaf.openFile(item.file); + } else { + // 其他連結:嘗試在新分頁開啟 + window.open(item.path, '_blank', 'noopener,noreferrer'); + } + }; + + return container; + } + + updateGalleryPage(galleryDiv: HTMLElement, items: GalleryItem[], page: number, itemsPerPage: number) { + const start = (page - 1) * itemsPerPage; + const end = start + itemsPerPage; + const currentPageItems = items.slice(start, end); + const totalPages = Math.ceil(items.length / itemsPerPage); + + if(!galleryDiv) return; + + // 移除舊的項目 + galleryDiv.replaceChildren(); + + // 新增新的項目 + currentPageItems.forEach((item, index) => { + if (item.type === 'note') { + const noteContainer = this.createNoteContainer(item); + galleryDiv.appendChild(noteContainer); + } else { + const mediaContainer = this.createMediaContainer(item, start + index); + galleryDiv.appendChild(mediaContainer); + } + }); + + // 如果是最後一頁且項目數量不足,添加空的佔位元素 + if (page === totalPages && currentPageItems.length < itemsPerPage) { + const placeholdersNeeded = itemsPerPage - currentPageItems.length; + for (let i = 0; i < placeholdersNeeded; i++) { + const placeholder = document.createElement('div'); + placeholder.className = 'mv-media-thumbnail-container placeholder'; + placeholder.style.visibility = 'hidden'; // 隱藏但保持佔位 + galleryDiv.appendChild(placeholder); + } + } + + // 更新分頁控制項 + if (galleryDiv.parentElement) { + const paginationDiv = galleryDiv.parentElement.querySelector('.mvgb-pagination'); + if (paginationDiv) { + const pageInfoSpan = paginationDiv.querySelector('.mvgb-page-info'); + if (pageInfoSpan) { + pageInfoSpan.textContent = `${page} / ${totalPages}`; + } + } + } + } + + createMediaContainer(media: GalleryItem, index: number) { + const container = document.createElement('div'); + container.className = 'mv-media-thumbnail-container'; + + if (media.type === 'image') { + const img = document.createElement('img') as HTMLImageElement; + if (media.url) { + img.src = media.url; + img.alt = media.path || ''; + img.style.pointerEvents = 'none'; + container.appendChild(img); + } + } else { + const video = document.createElement('video') as HTMLVideoElement; + if (media.url) { + video.src = media.url; + video.style.pointerEvents = 'none'; + container.appendChild(video); + } + + const videoIcon = document.createElement('div'); + videoIcon.className = 'mv-video-indicator'; + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('viewBox', '0 0 24 24'); + svg.setAttribute('width', '24'); + svg.setAttribute('height', '24'); + const path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); + path.setAttribute('d', 'M8 5v14l11-7z'); + svg.appendChild(path); + videoIcon.appendChild(svg); + container.appendChild(videoIcon); + } + + if (typeof media.title === 'object' && media.title !== null) { + const linkArea = document.createElement('div'); + linkArea.className = 'mvgb-media-link-area'; + + if (media.title.type === 'text') { + // 純文字 + const textSpan = document.createElement('span'); + textSpan.textContent = media.title.text; + textSpan.style.color = 'white'; + linkArea.appendChild(textSpan); + } else { + // 內部或外部連結 + const link = document.createElement('a'); + link.textContent = media.title.text; + + if (media.title.url) { + if (media.title.type === 'internal') { + // 內部連結 + const file = this.app.vault.getAbstractFileByPath(media.title.url); + if (file instanceof TFile) { + link.onclick = (e) => { + e.preventDefault(); + e.stopPropagation(); + const leaf = this.app.workspace.getLeaf('tab'); + leaf.openFile(file); + }; + } + } else { + // 外部連結 + link.href = encodeURI(media.title.url); + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.onclick = (e) => { + e.stopPropagation(); + }; + } + } + + linkArea.appendChild(link); + } + + container.appendChild(linkArea); + } + + container.onclick = () => { + const modal = new FullScreenModal(this.app, this.plugin, 'thumbnail'); + modal.open(); + setTimeout(() => { + const allUrls = modal.mediaUrls; + const targetIndex = allUrls.findIndex(m => m.url === media.url); + if (targetIndex !== -1) { + modal.showMedia(targetIndex); + } + }, 100); + }; + + return container; + } } \ No newline at end of file diff --git a/src/galleryBlockGenerate.ts b/src/galleryBlockGenerate.ts index 831c420..4e73823 100644 --- a/src/galleryBlockGenerate.ts +++ b/src/galleryBlockGenerate.ts @@ -1,88 +1,139 @@ -import { App, Modal,Setting, Editor} from 'obsidian'; -import { t } from './translations'; - -// Gallery 生成對話框 -export class GalleryBlockGenerateModal extends Modal { - selectedText: string; - title: string; - size: string; - addButton: boolean; - pagination: number; - editor: Editor; - - constructor(app: App, editor: Editor, selectedText: string) { - super(app); - this.editor = editor; - this.selectedText = selectedText; - this.title = ''; - this.size = 'medium'; - this.addButton = true; - this.pagination = 0; - } - - onOpen() { - const { contentEl } = this; - - // 標題 - new Setting(contentEl) - .setName(t('gallery_title')) - .setDesc(t('gallery_title_desc')) - .addText(text => text - .setValue(this.title) - .onChange(value => this.title = value)); - - // 尺寸選擇 - new Setting(contentEl) - .setName(t('gallery_size')) - .setDesc(t('gallery_size_desc')) - .addDropdown(dropdown => dropdown - .addOption('small', t('grid_size_small')) - .addOption('medium', t('grid_size_medium')) - .addOption('large', t('grid_size_large')) - .setValue(this.size) - .onChange(value => this.size = value)); - - // 上傳按鈕選項 - new Setting(contentEl) - .setName(t('gallery_add_button')) - .setDesc(t('gallery_add_button_desc')) - .addToggle(toggle => toggle - .setValue(this.addButton) - .onChange(value => this.addButton = value)); - - // 分頁設定 - new Setting(contentEl) - .setName(t('gallery_pagination')) - .setDesc(t('gallery_pagination_desc')) - .addText(text => text - .setValue(this.pagination ? this.pagination.toString() : '') - .setPlaceholder('0') - .onChange(value => { - const numValue = parseInt(value); - this.pagination = !isNaN(numValue) && numValue > 0 ? numValue : 0; - })); - - // 確認按鈕 - new Setting(contentEl) - .addButton(button => button - .setButtonText(t('confirm')) - .setCta() - .onClick(() => { - const galleryBlock = ['```gallery']; - galleryBlock.push(`title: ${this.title}`); - galleryBlock.push(`size: ${this.size}`); - galleryBlock.push(`addButton: ${this.addButton}`); - galleryBlock.push(`pagination: ${this.pagination}`); - if (this.selectedText) galleryBlock.push(this.selectedText); - galleryBlock.push('```\n'); - - this.editor.replaceSelection(galleryBlock.join('\n')); - this.close(); - })); - } - - onClose() { - const { contentEl } = this; - contentEl.empty(); - } +import { App, Modal,Setting, EditorPosition} from 'obsidian'; +import { t } from './translations'; + +// Gallery 生成對話框 +export class GalleryBlockGenerateModal extends Modal { + selectedText: string; + title: string; + size: string; + addButton: boolean; + pagination: number; + replaceRange: { from: EditorPosition, to: EditorPosition } | null; + onConfirm: ((newGalleryBlock: string) => void) | null; + + constructor(app: App, selectedText: string) { + super(app); + this.selectedText = this.parseExistingContent(selectedText); + this.replaceRange = null; + this.onConfirm = null; + if (this.title === undefined) this.title = ''; + if (this.size === undefined) this.size = 'medium'; + if (this.addButton === undefined) this.addButton = false; + if (this.pagination === undefined) this.pagination = 0; + } + + // 解析現有的 gallery 區塊內容 + parseExistingContent(content: string): string { + const lines = content.split('\n'); + let mediaItems = ''; + + for (const line of lines) { + const trimmedLine = line.trim(); + + // 解析標題 + const titleMatch = trimmedLine.match(/^title:\s*(.+)$/); + if (titleMatch) { + this.title = titleMatch[1]; + continue; + } + + // 解析尺寸 + const sizeMatch = trimmedLine.match(/^size:\s*(small|medium|large)$/i); + if (sizeMatch) { + this.size = sizeMatch[1].toLowerCase(); + continue; + } + + // 解析新增按鈕設定 + const addButtonMatch = trimmedLine.match(/^addbutton:\s*(true|false)$/i); + if (addButtonMatch) { + this.addButton = addButtonMatch[1].toLowerCase() === 'true'; + continue; + } + + // 解析分頁設定 + const paginationMatch = trimmedLine.match(/^pagination:\s*(\d+)$/i); + if (paginationMatch) { + this.pagination = parseInt(paginationMatch[1]) || 0; + continue; + } + + // 將非設定行收集為媒體項目 + if (!trimmedLine.match(/^(title|size|addbutton|pagination):/i) && trimmedLine) { + mediaItems += line + '\n'; + } + } + + return mediaItems.trim(); + } + + onOpen() { + const { contentEl } = this; + + // 標題 + new Setting(contentEl) + .setName(t('gallery_title')) + .setDesc(t('gallery_title_desc')) + .addText(text => text + .setValue(this.title || '') + .onChange(value => this.title = value)); + + // 尺寸選擇 + new Setting(contentEl) + .setName(t('gallery_size')) + .setDesc(t('gallery_size_desc')) + .addDropdown(dropdown => dropdown + .addOption('small', t('grid_size_small')) + .addOption('medium', t('grid_size_medium')) + .addOption('large', t('grid_size_large')) + .setValue(this.size) + .onChange(value => this.size = value)); + + // 上傳按鈕選項 + new Setting(contentEl) + .setName(t('gallery_add_button')) + .setDesc(t('gallery_add_button_desc')) + .addToggle(toggle => toggle + .setValue(this.addButton) + .onChange(value => this.addButton = value)); + + // 分頁設定 + new Setting(contentEl) + .setName(t('gallery_pagination')) + .setDesc(t('gallery_pagination_desc')) + .addText(text => text + .setValue(this.pagination ? this.pagination.toString() : '') + .setPlaceholder('0') + .onChange(value => { + const numValue = parseInt(value); + this.pagination = !isNaN(numValue) && numValue > 0 ? numValue : 0; + })); + + // 確認按鈕 + new Setting(contentEl) + .addButton(button => button + .setButtonText(t('confirm')) + .setCta() + .onClick(() => { + const galleryBlock = ['```gallery']; + galleryBlock.push(`title: ${this.title}`); + galleryBlock.push(`size: ${this.size}`); + galleryBlock.push(`addButton: ${this.addButton}`); + galleryBlock.push(`pagination: ${this.pagination}`); + if (this.selectedText) galleryBlock.push(this.selectedText); + galleryBlock.push('```\n'); + + const newGalleryBlockContent = galleryBlock.join('\n'); + if (this.onConfirm) { + this.onConfirm(newGalleryBlockContent.trim()); + } + + this.close(); + })); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } } \ No newline at end of file diff --git a/src/imageUpload.ts b/src/imageUpload.ts index adfaf29..fd1b581 100644 --- a/src/imageUpload.ts +++ b/src/imageUpload.ts @@ -5,10 +5,13 @@ import { t } from './translations'; export class ImageUploadModal extends Modal { plugin: MediaViewPlugin; galleryElement: HTMLElement; + insertAtEnd: boolean; // 新增變數來儲存插入位置選項 + constructor(app: App, plugin: MediaViewPlugin, galleryElement: HTMLElement) { super(app); this.plugin = plugin; this.galleryElement = galleryElement; // 儲存觸發上傳的 gallery 元素 + this.insertAtEnd = this.plugin.settings.insertAtEnd; // 初始化為設定中的值 } onOpen() { @@ -32,44 +35,6 @@ export class ImageUploadModal extends Modal { const instructions = dropZone.createDiv('mvgb-upload-instructions'); instructions.setText(t('drag_and_drop')); - // 處理拖放事件 - dropZone.addEventListener('dragover', (e) => { - e.preventDefault(); - dropZone.addClass('drag-over'); - }); - - dropZone.addEventListener('dragleave', () => { - dropZone.removeClass('drag-over'); - }); - - dropZone.addEventListener('drop', async (e) => { - e.preventDefault(); - dropZone.removeClass('drag-over'); - - if (e.dataTransfer === null) return; - const files = (e.dataTransfer.files as any); - await this.handleFiles(files); - }); - - const fileInput = contentEl.createEl('input', { - type: 'file', - attr: { - accept: 'image/*,video/*', - multiple: true, - style: 'display: none;' // 隱藏但保持在DOM中 - } - }); - - fileInput.addEventListener('change', async () => { - if (fileInput.files && fileInput.files.length > 0) { - await this.handleFiles(Array.from(fileInput.files)); - } - }); - - dropZone.addEventListener('click', () => { - fileInput.click(); - }); - // 新增貼上剪貼簿按鈕 const pasteButton = contentEl.createEl('button', { text: t('paste_from_clipboard'), @@ -133,6 +98,81 @@ export class ImageUploadModal extends Modal { console.error('剪貼簿讀取錯誤:', err); } }); + + // 新增插入位置選項 + const insertPositionContainer = contentEl.createDiv('mvgb-insert-position-container'); + insertPositionContainer.addClass('mvgb-setting-item'); + + const insertPositionLabel = insertPositionContainer.createDiv('mvgb-setting-label'); + insertPositionLabel.setText(t('insert_position')); + + const insertPositionControl = insertPositionContainer.createDiv('mvgb-setting-control'); + + // 建立下拉選單 + const insertPositionDropdown = insertPositionControl.createEl('select', { + cls: 'mvgb-insert-position-dropdown' + }); + + // 添加選項 + const endOption = insertPositionDropdown.createEl('option', { + text: t('insert_at_end'), + value: 'true' + }); + + const startOption = insertPositionDropdown.createEl('option', { + text: t('insert_at_start'), + value: 'false' + }); + + // 設定初始選擇的選項 + if (this.insertAtEnd) { + endOption.selected = true; + } else { + startOption.selected = true; + } + + // 添加變更事件 + insertPositionDropdown.addEventListener('change', () => { + this.insertAtEnd = insertPositionDropdown.value === 'true'; + }); + + // 處理拖放事件 + dropZone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropZone.addClass('drag-over'); + }); + + dropZone.addEventListener('dragleave', () => { + dropZone.removeClass('drag-over'); + }); + + dropZone.addEventListener('drop', async (e) => { + e.preventDefault(); + dropZone.removeClass('drag-over'); + + if (e.dataTransfer === null) return; + const files = (e.dataTransfer.files as any); + await this.handleFiles(files); + }); + + const fileInput = contentEl.createEl('input', { + type: 'file', + attr: { + accept: 'image/*,video/*', + multiple: true, + style: 'display: none;' // 隱藏但保持在DOM中 + } + }); + + fileInput.addEventListener('change', async () => { + if (fileInput.files && fileInput.files.length > 0) { + await this.handleFiles(Array.from(fileInput.files)); + } + }); + + dropZone.addEventListener('click', () => { + fileInput.click(); + }); } async handleFiles(files: File[]): Promise { @@ -258,8 +298,8 @@ export class ImageUploadModal extends Modal { // 如果這個 gallery 區塊的 ID 與觸發上傳的元素相同 if (currentGalleryId === galleryId) { - // 在 gallery 區塊內容的最前或最後一行插入新連結 (根據設定) - const newBlockContent = this.plugin.settings.insertAtEnd + // 在 gallery 區塊內容的最前或最後一行插入新連結 (根據選擇的選項) + const newBlockContent = this.insertAtEnd ? blockContent.trimEnd() + `\n${newLinks.join('\n')}\n` : `${newLinks.join('\n')}\n` + blockContent + '\n'; @@ -314,8 +354,8 @@ export class ImageUploadModal extends Modal { // 如果這個 gallery 區塊的 ID 與觸發上傳的元素相同 if (currentGalleryId === galleryId) { - // 在 gallery 區塊內容的最前或最後一行插入新連結 (根據設定) - const newBlockContent = this.plugin.settings.insertAtEnd + // 在 gallery 區塊內容的最前或最後一行插入新連結 (根據選擇的選項) + const newBlockContent = this.insertAtEnd ? blockContent.trimEnd() + `\n${links.join('\n')}\n` : `${links.join('\n')}\n` + blockContent + '\n'; @@ -355,7 +395,7 @@ export class ImageUploadModal extends Modal { getSafeFileName(originalName: string) { // 移除不安全的字元,只保留字母、數字、底線和檔案副檔名 - const name = originalName.replace(/[<>:"/\\|?*]/g, '_'); + const name = originalName.replace(/[#<>:"/\\|?*]/g, '_'); // 確保檔案名稱唯一 return name; } diff --git a/src/translations.ts b/src/translations.ts index 0258b2e..5290232 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -73,6 +73,7 @@ const TRANSLATIONS: Translations = { // 命令 'open_media_viewer': '打開媒體瀏覽器', 'generate_gallery': '生成 Gallery 區塊', + 'setting_gallery': '設定 Gallery 區塊', 'gallery_title': 'Gallery 標題', 'gallery_title_desc': '設定 Gallery 的標題(選填)', 'gallery_size': 'Gallery 尺寸', @@ -133,6 +134,7 @@ const TRANSLATIONS: Translations = { // Commands 'open_media_viewer': 'Open media viewer', 'generate_gallery': 'Generate gallery block', + 'setting_gallery': 'Setting gallery block', 'gallery_title': 'Gallery title', 'gallery_title_desc': 'Set the title of the gallery (optional)', 'gallery_size': 'Gallery size', @@ -193,6 +195,7 @@ const TRANSLATIONS: Translations = { // 命令 'open_media_viewer': '打开媒体浏览器', 'generate_gallery': '生成 Gallery 区块', + 'setting_gallery': '设置 Gallery 区块', 'gallery_title': '相册标题', 'gallery_title_desc': '设置相册的标题(选填)', 'gallery_size': '相册尺寸', @@ -253,6 +256,7 @@ const TRANSLATIONS: Translations = { // コマンド 'open_media_viewer': 'メディアビューワーを開く', 'generate_gallery': 'ギャラリーブロックを生成', + 'setting_gallery': 'ギャラリーブロックを設定', 'gallery_title': 'ギャラリータイトル', 'gallery_title_desc': 'ギャラリーのタイトルを設定(オプション)', 'gallery_size': 'ギャラリーサイズ', diff --git a/styles.css b/styles.css index 8c9348d..d966fa1 100644 --- a/styles.css +++ b/styles.css @@ -598,4 +598,68 @@ height: 48px; color: var(--text-muted); margin-bottom: 10px; -} \ No newline at end of file +} + +.mvgb-paste-button { + display: block; + width: 100%; + padding: 4px; + margin-top: 15px; + background-color: var(--interactive-accent); + color: var(--text-on-accent); + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s ease; +} + +.mvgb-paste-button:hover { + background-color: var(--interactive-accent-hover); +} + +/* 上傳檔案的位置選擇 */ +.mvgb-insert-position-container { + margin-top: 15px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.mvgb-setting-item { + display: flex; + align-items: center; + padding: 8px 0; +} + +.mvgb-setting-label { + flex: 0 0 100px; + font-weight: 500; + color: var(--text-normal); +} + +.mvgb-setting-control { + flex: 1; +} + +.mvgb-insert-position-dropdown { + width: 50%; + padding: 4px 8px; + border-radius: 4px; + border: 1px solid var(--background-modifier-border); + background-color: var(--background-primary); + color: var(--text-normal); + font-size: 14px; + transition: border-color 0.2s ease; +} + +.mvgb-insert-position-dropdown:focus { + border-color: var(--interactive-accent); + outline: none; +} + +.mvgb-insert-position-dropdown option { + background-color: var(--background-primary); + color: var(--text-normal); +} + diff --git a/versions.json b/versions.json index 68cdb22..4ef5503 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "1.8.11": "1.1.0" + "1.8.12": "1.1.0" } \ No newline at end of file