diff --git a/main.ts b/main.ts index ec83006..3a63076 100644 --- a/main.ts +++ b/main.ts @@ -65,7 +65,7 @@ export default class MediaViewPlugin extends Plugin { // 確認點擊的是圖片,且不在 code block 內,也不在 modal 內 if (target.tagName === 'IMG' && !target.closest('pre') && - !target.closest('.media-viewer-modal') && + !target.closest('.mv-media-viewer-modal') && !target.closest('.mvgb-media-gallery-grid')) { // 阻止預設行為 evt.preventDefault(); diff --git a/manifest.json b/manifest.json index 273f3b9..35c1f8c 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "mediaviewer", "name": "Media Viewer", - "version": "1.8.12", + "version": "1.9.0", "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 1dd06c4..5c685e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mediaviewer", - "version": "1.8.12", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mediaviewer", - "version": "1.8.12", + "version": "1.9.0", "license": "MIT", "devDependencies": { "@types/node": "^16.11.6", diff --git a/package.json b/package.json index 40ceaa9..5be7f34 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mediaviewer", - "version": "1.8.12", + "version": "1.9.0", "description": "This is a sample plugin for Obsidian (https://obsidian.md)", "main": "main.js", "scripts": { diff --git a/src/fullscreen.ts b/src/fullscreen.ts index 8a2b7f7..91d4032 100644 --- a/src/fullscreen.ts +++ b/src/fullscreen.ts @@ -93,7 +93,7 @@ export class FullScreenModal extends Modal { url = this.app.vault.getResourcePath(fileByPath); if (url) { const extension = markdownLink.toLowerCase(); - if (!extension.match(/\.(jpg|jpeg|png|gif|webp|mp4|mkv|mov|webm)$/)) { + if (!extension.match(/\.(jpg|jpeg|png|gif|webp|mp4|mkv|mov|webm|flac|m4a|mp3|ogg|wav|3gp)$/)) { continue; } } @@ -108,7 +108,7 @@ export class FullScreenModal extends Modal { let type = 'image'; if (file) { const extension = file.extension.toLowerCase(); - if (!extension.match(/^(jpg|jpeg|png|gif|webp|mp4|mkv|mov|webm)$/)) { + if (!extension.match(/^(jpg|jpeg|png|gif|webp|mp4|mkv|mov|webm|flac|m4a|mp3|ogg|wav|3gp)$/)) { continue; } type = extension.match(/^(jpg|jpeg|png|gif|webp)$/) ? 'image' : 'video'; @@ -187,19 +187,44 @@ export class FullScreenModal extends Modal { img.src = media.url; img.onclick = () => this.showMedia(index); } else { - const video = container.createEl('video'); - video.src = media.url; - video.onclick = () => this.showMedia(index); - const videoIcon = container.createDiv('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('fill', 'currentColor'); - path.setAttribute('d', 'M8 5v14l11-7z'); - svg.appendChild(path); - videoIcon.appendChild(svg); + if (media.url.match(/\.(mp4|mkv|mov|webm)/i)) { + const video = container.createEl('video'); + video.src = media.url; + video.onclick = () => this.showMedia(index); + const videoIcon = container.createDiv('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('fill', 'currentColor'); + path.setAttribute('d', 'M8 5v14l11-7z'); + svg.appendChild(path); + videoIcon.appendChild(svg); + } else { + const audio = container.createEl('audio'); + audio.src = media.url; + container.onclick = () => this.showMedia(index); + const audioIcon = container.createDiv('mv-audio-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('fill', 'currentColor'); + path.setAttribute('d', 'M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z'); + svg.appendChild(path); + audioIcon.appendChild(svg); + + // 添加檔案名稱顯示 + const fileName = media.path ? media.path.split('/').pop() : media.url.split('/').pop(); + if (fileName) { + const cleanFileName = fileName.split('?')[0]; // 移除檔案名稱中 ? 後面的部分 + const decodedFileName = decodeURIComponent(cleanFileName) + const filenameDiv = container.createDiv('mv-audio-filename'); + filenameDiv.textContent = decodedFileName.replace(/\.[^/.]+$/, ''); + } + } } }); @@ -290,7 +315,9 @@ export class FullScreenModal extends Modal { }; } else { this.fullVideo.src = media.url; - this.fullVideo.muted = this.plugin.settings.muteVideoOnOpen; + if (media.url.match(/\.(mp4|mkv|mov|webm)/i)) { + this.fullVideo.muted = this.plugin.settings.muteVideoOnOpen; + } this.fullVideo.style.display = 'block'; this.fullImage.style.display = 'none'; this.fullVideo.loop = true; @@ -330,13 +357,13 @@ export class FullScreenModal extends Modal { const decodedFileName = decodeURIComponent(cleanFileName); // 添加檔案編號 - const numSpan = infoPanel.createEl('span', { + infoPanel.createEl('span', { text: `${this.currentIndex + 1}/${this.mediaUrls.length}`, cls: 'mv-info-item' }); // 添加檔案名稱 - const labelSpan = infoPanel.createEl('span', { + infoPanel.createEl('span', { text: decodedFileName, cls: ['mv-info-item', 'mv-info-filename'], attr: { contentEditable: 'true' } @@ -344,7 +371,7 @@ export class FullScreenModal extends Modal { // 添加圖片尺寸 if (this.isImage) { - const sizeSpan = infoPanel.createEl('span', { + infoPanel.createEl('span', { text: `(${this.fullImage.naturalWidth} × ${this.fullImage.naturalHeight})`, cls: ['mv-info-item', 'mv-info-dimensions'] }); @@ -583,18 +610,43 @@ export class FullScreenModal extends Modal { img.src = media.url; img.onclick = () => this.showMedia(idx); } else { - const video = container.createEl('video'); - video.src = media.url; - video.onclick = () => this.showMedia(idx); - const videoIcon = container.createDiv('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); + if (media.url.match(/\.(mp4|mkv|mov|webm)/i)) { + const video = container.createEl('video'); + video.src = media.url; + video.onclick = () => this.showMedia(idx); + const videoIcon = container.createDiv('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); + } else { + const audio = container.createEl('audio'); + audio.src = media.url; + container.onclick = () => this.showMedia(idx); + const audioIcon = container.createDiv('mv-audio-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('fill', 'currentColor'); + path.setAttribute('d', 'M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z'); + svg.appendChild(path); + audioIcon.appendChild(svg); + + // 添加檔案名稱顯示 + const fileName = media.path ? media.path.split('/').pop() : media.url.split('/').pop(); + if (fileName) { + const cleanFileName = fileName.split('?')[0]; // 移除檔案名稱中 ? 後面的部分 + const decodedFileName = decodeURIComponent(cleanFileName) + const filenameDiv = container.createDiv('mv-audio-filename'); + filenameDiv.textContent = decodedFileName.replace(/\.[^/.]+$/, ''); + } + } } }); diff --git a/src/galleryBlock.ts b/src/galleryBlock.ts index 3ef9055..c2156c6 100644 --- a/src/galleryBlock.ts +++ b/src/galleryBlock.ts @@ -1,4 +1,4 @@ -import { App, TFile, Menu, Notice } from 'obsidian'; +import { App, TFile, Menu, Notice, Platform } from 'obsidian'; import MediaViewPlugin from '../main'; import { FullScreenModal } from './fullscreen'; import { MediaViewSettings } from './settings'; @@ -259,7 +259,7 @@ export class GalleryBlock { // 處理一般的圖片/媒體連結 if (file) { const extension = file.extension.toLowerCase(); - if (extension.match(/^(jpg|jpeg|png|gif|webp|mp4|mkv|mov|webm)$/)) { + if (extension.match(/^(jpg|jpeg|png|gif|webp|mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/)) { items.push({ type: extension.match(/^(jpg|jpeg|png|gif|webp)$/) ? 'image' : 'video', url: this.app.vault.getResourcePath(file), @@ -311,7 +311,7 @@ export class GalleryBlock { if (fileByPath && fileByPath instanceof TFile) { const extension = url.toLowerCase(); items.push({ - type: extension.match(/\.(mp4|mkv|mov|webm)$/) ? 'video' : 'image', + type: extension.match(/\.(mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/) ? 'video' : 'image', url: this.app.vault.getResourcePath(fileByPath), path: text || fileByPath.path, title: currentTitle, @@ -321,7 +321,7 @@ export class GalleryBlock { } else { const extension = file.extension.toLowerCase(); items.push({ - type: extension.match(/^(mp4|mkv|mov|webm)$/) ? 'video' : 'image', + type: extension.match(/^(mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/) ? 'video' : 'image', url: this.app.vault.getResourcePath(file), path: text || file.path, title: currentTitle, @@ -330,7 +330,7 @@ export class GalleryBlock { } } else { const urlForTypeCheck = url.split(' "')[0].split('?')[0].toLowerCase(); - const isImageFile = urlForTypeCheck.match(/\.(mp4|mkv|mov|webm)$/); + const isImageFile = urlForTypeCheck.match(/\.(mp4|mkv|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/); items.push({ type: isImageFile ? 'video' : 'image', url: url.split(' "')[0], @@ -608,6 +608,9 @@ export class GalleryBlock { // 右鍵選單 galleryDiv.addEventListener('contextmenu', (event) => { + if (event.target !== galleryDiv) { + return; + } event.preventDefault(); const menu = new Menu(); menu.addItem((item) => { @@ -764,7 +767,7 @@ export class GalleryBlock { galleryDiv.replaceChildren(); // 新增新的項目 - currentPageItems.forEach((item, index) => { + currentPageItems.forEach((item: GalleryItem, index: number) => { if (item.type === 'note') { const noteContainer = this.createNoteContainer(item); galleryDiv.appendChild(noteContainer); @@ -806,28 +809,64 @@ export class GalleryBlock { if (media.url) { img.src = media.url; img.alt = media.path || ''; - img.style.pointerEvents = 'none'; + if(Platform.isMobile) 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); + if (media.path && media.url) { + // 處理影片檔案 + if (media.path.match(/\.(mp4|mkv|mov|webm)$/)) { + const video = document.createElement('video') as HTMLVideoElement; + 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); + } else { + // 處理音樂檔案 + const audio = document.createElement('audio') as HTMLAudioElement; + audio.src = media.url; + audio.style.pointerEvents = 'none'; + container.appendChild(audio); + + const audioIcon = document.createElement('div'); + audioIcon.className = 'mv-audio-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('fill', 'currentColor'); + path.setAttribute('d', 'M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z'); + svg.appendChild(path); + audioIcon.appendChild(svg); + container.appendChild(audioIcon); + + // 添加檔案名稱顯示 + const filenameDiv = document.createElement('div'); + filenameDiv.className = 'mv-audio-filename'; + let filename = ''; + if (media.path) { + // 從路徑中提取檔案名稱 + const pathParts = media.path.split('/'); + filename = pathParts[pathParts.length - 1]; + // 移除副檔名 + filename = filename.replace(/\.[^/.]+$/, ''); + } + filenameDiv.textContent = filename; + container.appendChild(filenameDiv); + } } - - 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) { @@ -873,19 +912,21 @@ export class GalleryBlock { 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); - }; - + + if ((!this.plugin.settings.disableClickToOpenMediaOnGallery && media.type === 'image') || media.type === 'video') { + 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/imageUpload.ts b/src/imageUpload.ts index fd1b581..14291d9 100644 --- a/src/imageUpload.ts +++ b/src/imageUpload.ts @@ -200,7 +200,7 @@ export class ImageUploadModal extends Modal { // 解碼 URL 並移除檔案副檔名 const fileName = decodeURIComponent(obsidianUrlMatch[1]).replace(/\.md$/, ''); // 判斷 filename 是否有圖片檔的副檔名 - if (/\.(jpg|jpeg|png|gif|webp|mp4|mov|webm)$/i.test(fileName)) { + if (/\.(jpg|jpeg|png|gif|webp|mp4|mov|webm|mp3|m4a|flac|ogg|wav|3gp)$/i.test(fileName)) { newLinks.push(`![[${fileName}]]`); } else { newLinks.push(`[[${fileName}]]`); @@ -226,7 +226,7 @@ export class ImageUploadModal extends Modal { // 處理一般檔案 if (file instanceof File) { // 檢查是否為支援的媒體類型 - if (file.type.startsWith('image/') || file.type.startsWith('video/')) { + if (file.type.startsWith('image/') || file.type.startsWith('video/') || file.type.startsWith('audio/')) { // 取得附件資料夾路徑 const attachmentFolderPath = this.getAttachmentFolderPath(activeFile); diff --git a/src/settings.ts b/src/settings.ts index 8e534a0..39652aa 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -7,6 +7,7 @@ export interface MediaViewSettings { allowMediaDeletion: boolean; autoOpenFirstImage: boolean; openMediaBrowserOnClick: boolean; + disableClickToOpenMediaOnGallery: boolean; muteVideoOnOpen: boolean; galleryGridSize: string; galleryGridSizeSmall: number; @@ -21,6 +22,7 @@ export const DEFAULT_SETTINGS: MediaViewSettings = { allowMediaDeletion: false, autoOpenFirstImage: false, openMediaBrowserOnClick: true, + disableClickToOpenMediaOnGallery: false, muteVideoOnOpen: false, galleryGridSize: 'medium', galleryGridSizeSmall: 100, @@ -82,6 +84,17 @@ export class MediaViewSettingTab extends PluginSettingTab { await this.plugin.saveSettings(); })); + // 禁用 Gallery 內圖片的點擊事件 (以便相容其他圖片類插件) + new Setting(containerEl) + .setName(t('disable_click_to_open_media_on_gallery')) + .setDesc(t('disable_click_to_open_media_on_gallery_desc')) + .addToggle(toggle => toggle + .setValue(this.plugin.settings.disableClickToOpenMediaOnGallery) + .onChange(async (value) => { + this.plugin.settings.disableClickToOpenMediaOnGallery = value; + await this.plugin.saveSettings(); + })); + new Setting(containerEl) .setName(t('mute_video_on_open')) .setDesc(t('mute_video_on_open_desc')) diff --git a/src/translations.ts b/src/translations.ts index 5290232..dff27f0 100644 --- a/src/translations.ts +++ b/src/translations.ts @@ -45,6 +45,8 @@ const TRANSLATIONS: Translations = { 'auto_open_first_image_desc': '啟用此選項後,媒體瀏覽器將自動打開第一張圖片', 'click_to_open_media': '點擊圖片打開媒體瀏覽器', 'click_to_open_media_desc': '啟用此選項後,點擊一般的圖片將以媒體瀏覽器打開 (需重啟)', + 'disable_click_to_open_media_on_gallery': '禁用 Gallery 內圖片點擊打開媒體瀏覽器', + 'disable_click_to_open_media_on_gallery_desc': '點擊 Gallery 內的圖片將不會打開媒體瀏覽器 (以便相容其他圖片類外掛)', 'mute_video_on_open': '打開影片時靜音', 'mute_video_on_open_desc': '啟用此選項後,媒體瀏覽器打開影片時會自動靜音', 'grid_size': '網格大小', @@ -106,6 +108,8 @@ const TRANSLATIONS: Translations = { 'auto_open_first_image_desc': 'Automatically open the first image when opening the media browser', 'click_to_open_media': 'Click image to open Media Browser', 'click_to_open_media_desc': 'Open media browser when clicking on images (requires restart)', + 'disable_click_to_open_media_on_gallery': 'Disable click to open media on gallery', + 'disable_click_to_open_media_on_gallery_desc': 'Disable click to open media on gallery (for compatibility with other image plugins)', 'mute_video_on_open': 'Mute video on open', 'mute_video_on_open_desc': 'Automatically mute videos when opening in the media browser', 'grid_size': 'Grid size', @@ -167,6 +171,8 @@ const TRANSLATIONS: Translations = { 'auto_open_first_image_desc': '启用此选项后,媒体浏览器将自动打开第一张图片', 'click_to_open_media': '点击图片打开媒体浏览器', 'click_to_open_media_desc': '启用此选项后,点击一般的图片将以媒体浏览器打开 (需重启)', + 'disable_click_to_open_media_on_gallery': '禁用 Gallery 内的图片点击打开媒体浏览器', + 'disable_click_to_open_media_on_gallery_desc': '点击 Gallery 内的图片将不会打开媒体浏览器 (以便兼容其他图片插件)', 'mute_video_on_open': '打开影片时静音', 'mute_video_on_open_desc': '启用此选项后,媒体浏览器打开影片时会自动静音', 'grid_size': '网格大小', @@ -228,6 +234,9 @@ const TRANSLATIONS: Translations = { 'auto_open_first_image_desc': 'メディアブラウザを開くときに最初の画像を自動的に開きます', 'click_to_open_media': '画像をクリックしてメディアブラウザを開く', 'click_to_open_media_desc': '画像をクリックするとメディアブラウザが開きます(再起動が必要)', + 'disable_click_to_open_media_on_gallery': 'ギャラリー内の画像をクリックしてメディアブラウザを開くを無効にする', + 'disable_click_to_open_media_on_gallery_desc': 'ギャラリー内の画像をクリックするとメディアブラウザが開かない(他の画像プラグインとの互換性のために)', + 'mute_video_on_open': '開くときにビデオをミュート', 'mute_video_on_open_desc': 'メディアブラウザで開くときにビデオを自動的にミュートします', 'grid_size': 'グリッドサイズ', diff --git a/styles.css b/styles.css index d966fa1..fa25d2d 100644 --- a/styles.css +++ b/styles.css @@ -118,6 +118,42 @@ fill: currentColor; } +.mv-audio-indicator { + position: absolute; + top: 8px; + left: 8px; + width: 32px; + height: 32px; + background: rgba(0, 100, 200, 0.6); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + z-index: 1; + backdrop-filter: blur(2px); +} + +.mv-audio-indicator svg { + width: 20px; + height: 20px; + fill: currentColor; +} + +.mv-audio-filename { + position: absolute; + bottom: 0; + left: 0; + right: 0; + padding: 15px; + color: var(--text-normal); + font-size: 14px; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + backdrop-filter: blur(2px); +} + .mv-full-media-view { position: fixed; top: 0; @@ -662,4 +698,3 @@ background-color: var(--background-primary); color: var(--text-normal); } - diff --git a/versions.json b/versions.json index 4ef5503..06df83d 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,3 @@ { - "1.8.12": "1.1.0" + "1.9.0": "1.1.0" } \ No newline at end of file