This commit is contained in:
Devon22 2025-04-01 01:30:58 +08:00
parent 03f2b0278d
commit ef8658e723
11 changed files with 227 additions and 77 deletions

View file

@ -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();

View file

@ -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",

4
package-lock.json generated
View file

@ -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",

View file

@ -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": {

View file

@ -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(/\.[^/.]+$/, '');
}
}
}
});

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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'))

View file

@ -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': 'グリッドサイズ',

View file

@ -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);
}

View file

@ -1,3 +1,3 @@
{
"1.8.12": "1.1.0"
"1.9.0": "1.1.0"
}