Compare commits

...

7 commits
v0.0.4 ... main

Author SHA1 Message Date
Zhou Hua
ff1f62c274 0.0.7 2025-03-20 18:18:54 +08:00
Zhou Hua
df6dca652f 支持排他播放,支持波型图配置,修改播放器 bug 2025-03-20 18:18:44 +08:00
Zhou Hua
e8a1b4e0af 0.0.6 2025-03-10 16:44:29 +08:00
Zhou Hua
36a90e695f 删除无用的代码 2025-03-10 16:44:20 +08:00
Zhou Hua
ee5b62d009 0.0.5 2025-03-10 14:14:06 +08:00
Zhou Hua
3e7c5bb415 fix cr issues 2025-03-10 14:13:45 +08:00
Zhou Hua
958b0e3e44 删除 html 操作 2025-02-12 11:56:13 +08:00
16 changed files with 1266 additions and 716 deletions

5
.typesafe-i18n.json Normal file
View file

@ -0,0 +1,5 @@
{
"outputPath": "./i18n/",
"adapters": [],
"$schema": "https://unpkg.com/typesafe-i18n@5.26.2/schema/typesafe-i18n.json"
}

View file

@ -1,4 +1,4 @@
# Obsidian Waveform Audio Player
# Waveform Audio Player Plugin
A plugin that enhances the display of audio files in Obsidian, supporting both audio attachments and recorded audio files.

35
i18n/en/index.ts Normal file
View file

@ -0,0 +1,35 @@
import type { BaseTranslation } from '../i18n-types';
const en: BaseTranslation = {
settings: {
stopOtherPlayers: {
title: 'Stop other players when playing',
description: 'Stop other players when playing a new audio file',
},
waveformType: {
title: 'Waveform Type',
description: 'Choose the display style of the audio waveform',
options: {
bars: 'Bars',
envelope: 'Envelope',
line: 'Line',
mirror: 'Mirror',
wave: 'Wave'
}
},
samplePoints: {
title: 'Sample points',
description: 'Set the number of sample points for the waveform. The higher the number, the more detailed the waveform, but the performance consumption is also higher.',
options: {
'50': '50 (Lowest)',
'100': '100 (Low)',
'200': '200 (Default)',
'500': '500 (Medium)',
'1000': '1000 (High)',
'2000': '2000 (Very High)',
'5000': '5000 (Extreme)'
}
},
},
};
export default en;

10
i18n/formatters.ts Normal file
View file

@ -0,0 +1,10 @@
import type { FormattersInitializer } from 'typesafe-i18n';
import type { Locales, Formatters } from './i18n-types';
export const initFormatters: FormattersInitializer<Locales, Formatters> = (_locale: Locales) => {
const formatters: Formatters = {
// add your formatter functions here
};
return formatters;
};

190
i18n/i18n-types.ts Normal file
View file

@ -0,0 +1,190 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import type { BaseTranslation as BaseTranslationType, LocalizedString } from 'typesafe-i18n'
export type BaseTranslation = BaseTranslationType
export type BaseLocale = 'en'
export type Locales =
| 'en'
| 'zh'
export type Translation = RootTranslation
export type Translations = RootTranslation
type RootTranslation = {
settings: {
stopOtherPlayers: {
/**
* Stop other players when playing
*/
title: string
/**
* Stop other players when playing a new audio file
*/
description: string
}
waveformType: {
/**
* Waveform Type
*/
title: string
/**
* Choose the display style of the audio waveform
*/
description: string
options: {
/**
* Bars
*/
bars: string
/**
* Envelope
*/
envelope: string
/**
* Line
*/
line: string
/**
* Mirror
*/
mirror: string
/**
* Wave
*/
wave: string
}
}
samplePoints: {
/**
* Sample points
*/
title: string
/**
* Set the number of sample points for the waveform. The higher the number, the more detailed the waveform, but the performance consumption is also higher.
*/
description: string
options: {
/**
* 50 (Lowest)
*/
'50': string
/**
* 100 (Low)
*/
'100': string
/**
* 200 (Default)
*/
'200': string
/**
* 500 (Medium)
*/
'500': string
/**
* 1000 (High)
*/
'1000': string
/**
* 2000 (Very High)
*/
'2000': string
/**
* 5000 (Extreme)
*/
'5000': string
}
}
}
}
export type TranslationFunctions = {
settings: {
stopOtherPlayers: {
/**
* Stop other players when playing
*/
title: () => LocalizedString
/**
* Stop other players when playing a new audio file
*/
description: () => LocalizedString
}
waveformType: {
/**
* Waveform Type
*/
title: () => LocalizedString
/**
* Choose the display style of the audio waveform
*/
description: () => LocalizedString
options: {
/**
* Bars
*/
bars: () => LocalizedString
/**
* Envelope
*/
envelope: () => LocalizedString
/**
* Line
*/
line: () => LocalizedString
/**
* Mirror
*/
mirror: () => LocalizedString
/**
* Wave
*/
wave: () => LocalizedString
}
}
samplePoints: {
/**
* Sample points
*/
title: () => LocalizedString
/**
* Set the number of sample points for the waveform. The higher the number, the more detailed the waveform, but the performance consumption is also higher.
*/
description: () => LocalizedString
options: {
/**
* 50 (Lowest)
*/
'50': () => LocalizedString
/**
* 100 (Low)
*/
'100': () => LocalizedString
/**
* 200 (Default)
*/
'200': () => LocalizedString
/**
* 500 (Medium)
*/
'500': () => LocalizedString
/**
* 1000 (High)
*/
'1000': () => LocalizedString
/**
* 2000 (Very High)
*/
'2000': () => LocalizedString
/**
* 5000 (Extreme)
*/
'5000': () => LocalizedString
}
}
}
}
export type Formatters = {}

27
i18n/i18n-util.async.ts Normal file
View file

@ -0,0 +1,27 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { initFormatters } from './formatters'
import type { Locales, Translations } from './i18n-types'
import { loadedFormatters, loadedLocales, locales } from './i18n-util'
const localeTranslationLoaders = {
en: () => import('./en'),
zh: () => import('./zh'),
}
const updateDictionary = (locale: Locales, dictionary: Partial<Translations>): Translations =>
loadedLocales[locale] = { ...loadedLocales[locale], ...dictionary }
export const importLocaleAsync = async (locale: Locales): Promise<Translations> =>
(await localeTranslationLoaders[locale]()).default as unknown as Translations
export const loadLocaleAsync = async (locale: Locales): Promise<void> => {
updateDictionary(locale, await importLocaleAsync(locale))
loadFormatters(locale)
}
export const loadAllLocalesAsync = (): Promise<void[]> => Promise.all(locales.map(loadLocaleAsync))
export const loadFormatters = (locale: Locales): void =>
void (loadedFormatters[locale] = initFormatters(locale))

26
i18n/i18n-util.sync.ts Normal file
View file

@ -0,0 +1,26 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { initFormatters } from './formatters'
import type { Locales, Translations } from './i18n-types'
import { loadedFormatters, loadedLocales, locales } from './i18n-util'
import en from './en'
import zh from './zh'
const localeTranslations = {
en,
zh,
}
export const loadLocale = (locale: Locales): void => {
if (loadedLocales[locale]) return
loadedLocales[locale] = localeTranslations[locale] as unknown as Translations
loadFormatters(locale)
}
export const loadAllLocales = (): void => locales.forEach(loadLocale)
export const loadFormatters = (locale: Locales): void =>
void (loadedFormatters[locale] = initFormatters(locale))

38
i18n/i18n-util.ts Normal file
View file

@ -0,0 +1,38 @@
// This file was auto-generated by 'typesafe-i18n'. Any manual changes will be overwritten.
/* eslint-disable */
import { i18n as initI18n, i18nObject as initI18nObject, i18nString as initI18nString } from 'typesafe-i18n'
import type { LocaleDetector } from 'typesafe-i18n/detectors'
import type { LocaleTranslationFunctions, TranslateByString } from 'typesafe-i18n'
import { detectLocale as detectLocaleFn } from 'typesafe-i18n/detectors'
import { initExtendDictionary } from 'typesafe-i18n/utils'
import type { Formatters, Locales, Translations, TranslationFunctions } from './i18n-types'
export const baseLocale: Locales = 'en'
export const locales: Locales[] = [
'en',
'zh'
]
export const isLocale = (locale: string): locale is Locales => locales.includes(locale as Locales)
export const loadedLocales: Record<Locales, Translations> = {} as Record<Locales, Translations>
export const loadedFormatters: Record<Locales, Formatters> = {} as Record<Locales, Formatters>
export const extendDictionary = initExtendDictionary<Translations>()
export const i18nString = (locale: Locales): TranslateByString => initI18nString<Locales, Formatters>(locale, loadedFormatters[locale])
export const i18nObject = (locale: Locales): TranslationFunctions =>
initI18nObject<Locales, Translations, TranslationFunctions, Formatters>(
locale,
loadedLocales[locale],
loadedFormatters[locale]
)
export const i18n = (): LocaleTranslationFunctions<Locales, Translations, TranslationFunctions> =>
initI18n<Locales, Translations, TranslationFunctions, Formatters>(loadedLocales, loadedFormatters)
export const detectLocale = (...detectors: LocaleDetector[]): Locales => detectLocaleFn<Locales>(baseLocale, locales, ...detectors)

36
i18n/zh/index.ts Normal file
View file

@ -0,0 +1,36 @@
import type { Translation } from '../i18n-types';
const zh: Translation = {
settings: {
stopOtherPlayers: {
title: '播放时停止其他播放器',
description: '播放新音频文件时停止其他播放器',
},
waveformType: {
title: '波形类型',
description: '选择音频波形的显示样式',
options: {
bars: '柱形',
envelope: '包络',
line: '单线条',
mirror: '镜像',
wave: '波浪'
}
},
samplePoints: {
title: '波形采样点数量',
description: '设置波形图的采样点数量。数值越大,波形细节越丰富,但性能消耗也越大。',
options: {
'50': '50 (最低)',
'100': '100 (低)',
'200': '200 (默认)',
'500': '500 (中)',
'1000': '1000 (高)',
'2000': '2000 (很高)',
'5000': '5000 (极高)'
}
},
},
};
export default zh;

View file

@ -10,8 +10,8 @@
}
/* 隐藏默认音频元素 */
.internal-embed audio {
display: none;
.internal-embed.audio-embed {
display: none !important;
}
/* 播放器容器样式 */
@ -20,7 +20,6 @@
overflow: hidden;
display: block;
width: 100%;
background: var(--background-secondary);
margin: 0.5em 0;
}
@ -64,7 +63,8 @@
/* 统一播放器样式 */
.wa-obsidian-player {
background: var(--background-secondary);
border-radius: 8px;
padding: 0.5em;
padding: 16px;
background: transparent;
--wa-text-color: var(--text-normal);
}

View file

@ -1,7 +1,7 @@
{
"id": "waveform-player",
"name": "Waveform Player",
"version": "0.0.4",
"version": "0.0.7",
"minAppVersion": "1.7.7",
"description": "Render audio files as waveforms",
"author": "Zhou Hua",

View file

@ -1,7 +1,7 @@
{
"name": "obsidian-waveform-player",
"type": "module",
"version": "0.0.4",
"version": "0.0.7",
"description": "Obsidian plugin for rendering audio files as waveforms",
"author": "zhouhua",
"license": "MIT",
@ -15,7 +15,8 @@
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
"version": "node version-bump.mjs && git add manifest.json versions.json",
"typesafe-i18n": "typesafe-i18n"
},
"devDependencies": {
"@codemirror/language": "^6.0.0",
@ -36,8 +37,9 @@
"typescript": "5.7.3"
},
"dependencies": {
"@waveform-audio/player": "^0.0.3",
"@waveform-audio/player": "^0.0.4",
"react": "^18.3.0",
"react-dom": "^18.3.0"
"react-dom": "^18.3.0",
"typesafe-i18n": "^5.26.2"
}
}

File diff suppressed because it is too large Load diff

26
src/L.ts Normal file
View file

@ -0,0 +1,26 @@
import { loadAllLocales } from '../i18n/i18n-util.sync';
import { i18n } from '../i18n/i18n-util';
import type { Locales } from '../i18n/i18n-types';
loadAllLocales();
declare global {
interface Window {
i18next: {
language: string;
};
}
}
let locale: Locales = 'en';
try {
locale = (window.i18next.language || '').startsWith('zh') ? 'zh' : 'en';
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
catch (e) {
/* empty */
}
const L = i18n()[locale];
export default L;

View file

@ -1,15 +1,100 @@
import type { EditorState, Extension } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import type { ComponentType } from 'react';
import type { ComponentType, ReactNode } from 'react';
import { StateEffect, StateField } from '@codemirror/state';
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
import WaveformPlayer from '@waveform-audio/player';
import { Plugin, TFile } from 'obsidian';
import WaveformPlayer, { AudioPlayerContextValue } from '@waveform-audio/player';
import { Plugin, TFile, PluginSettingTab, Setting, App } from 'obsidian';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import L from './L';
const PLAYER_CLASS = 'waveform-player-widget';
const AUDIO_LINK_PATTERN = '!\\[([^\\]]*)\\]\\(([^)]+\\.(?:mp3|wav|ogg|m4a|webm))\\)';
const AUDIO_LINK_PATTERN = '!\\[([^\\]]*)\\]\\(([^)]+\\.(?:mp3|wav|ogg|m4a|webm))\\)|!\\[\\[([^\\]]+\\.(?:mp3|wav|ogg|m4a|webm))\\]\\]';
// 波形类型枚举
type WaveformType = 'bars' | 'envelope' | 'line' | 'mirror' | 'wave';
// 插件设置接口
interface WaveformPlayerSettings {
stopOthersOnPlay: boolean;
waveformType: WaveformType;
samplePoints: number; // 采样点数量
}
// 默认设置
const DEFAULT_SETTINGS: WaveformPlayerSettings = {
stopOthersOnPlay: true,
waveformType: 'mirror',
samplePoints: 200, // 默认采样点数量
};
// 全局音频播放器注册表
let audioPlayerContexts: AudioPlayerContextValue[] = [];
// 阅读视图播放器信息存储
interface ReadingViewPlayerInfo {
root: ReturnType<typeof createRoot>;
container: HTMLElement;
playerDiv: HTMLElement;
audioUrl: string;
title: string;
}
// 播放器属性接口,用于统一创建播放器组件
interface PlayerProps {
src: string;
title: string;
plugin: WaveformPlayerPlugin;
id?: string;
}
// 创建播放器React元素的统一函数
function createPlayerElement({ src, title, plugin, id }: PlayerProps): ReactNode {
return createElement(WaveformPlayer as ComponentType<any>, {
className: 'wa-obsidian-player',
key: id,
samplePoints: plugin.settings.samplePoints, // 使用设置中的采样点数量
src,
showDownloadButton: false,
onPlay: (ctx: AudioPlayerContextValue) => {
if (plugin.settings.stopOthersOnPlay) {
audioPlayerContexts.forEach(player => {
if (player.instanceId !== ctx.instanceId) {
player.pause();
}
});
}
audioPlayerContexts.push(ctx);
},
onPause: (ctx: AudioPlayerContextValue) => {
audioPlayerContexts = audioPlayerContexts.filter(player => player.instanceId !== ctx.instanceId);
},
onEnded: (ctx: AudioPlayerContextValue) => {
// 音频结束时从注册表中移除
audioPlayerContexts = audioPlayerContexts.filter(player => player.instanceId !== ctx.instanceId);
},
styles: {
controls: {
width: '156px',
padding: 0,
},
header: {
padding: 0,
paddingBottom: 8,
},
title: {
fontSize: '14px',
margin: 0,
},
waveform: {
height: '100px',
},
},
title,
type: plugin.settings.waveformType,
});
}
class AudioPlayerWidget extends WidgetType {
private static counter = 0;
@ -18,6 +103,14 @@ class AudioPlayerWidget extends WidgetType {
private mounted = false;
private playerDiv: HTMLElement | null = null;
private root: null | ReturnType<typeof createRoot> = null;
// 记录创建时的设置版本,用于检测设置是否变化
private settingsVersion: number;
// 存储所有实例的静态集合
private static instances: Set<AudioPlayerWidget> = new Set();
// 静态触发器,用于手动触发重新渲染所有播放器
public static updateTrigger = StateEffect.define<void>();
constructor(
private readonly src: string,
@ -26,19 +119,41 @@ class AudioPlayerWidget extends WidgetType {
) {
super();
this.id = `audio-player-${AudioPlayerWidget.counter++}`;
// 记录当前的设置版本
this.settingsVersion = this.plugin.settingsVersion;
// 将实例添加到集合中
AudioPlayerWidget.instances.add(this);
}
destroy() {
// 从集合中移除实例
AudioPlayerWidget.instances.delete(this);
this.unmount();
this.container = null;
this.playerDiv = null;
}
// 静态方法:重新渲染所有播放器实例
static refreshAllPlayers(plugin: WaveformPlayerPlugin): void {
// 触发编辑器强制更新
plugin.triggerEditorRefresh();
}
// 重新挂载播放器
remount(): void {
if (this.playerDiv && this.container) {
this.unmount();
this.mount();
}
}
// 当比较两个播放器小部件是否相同时,同时考虑设置版本
eq(other: AudioPlayerWidget): boolean {
return (
other instanceof AudioPlayerWidget &&
this.src === other.src &&
this.title === other.title
this.title === other.title &&
this.settingsVersion === other.settingsVersion // 比较设置版本
);
}
@ -80,36 +195,17 @@ class AudioPlayerWidget extends WidgetType {
}
const audioUrl = this.plugin.app.vault.getResourcePath(audioFile);
console.log("audioUrl", audioUrl);
const decodedUrl = decodeURIComponent(audioUrl);
try {
this.root = createRoot(this.playerDiv);
this.root.render(
createElement(WaveformPlayer as ComponentType<any>, {
className: 'wa-obsidian-player',
key: this.id,
samplePoints: 200,
createPlayerElement({
src: decodedUrl,
styles: {
controls: {
paddingBottom: 0,
width: '156px',
},
root: {
padding: '0.5em',
},
title: {
fontSize: '14px',
margin: 0,
},
waveform: {
height: '100px',
},
},
title: this.title || audioFile.basename,
type: 'mirror',
}),
plugin: this.plugin,
id: this.id
})
);
this.mounted = true;
}
@ -136,8 +232,65 @@ class AudioPlayerWidget extends WidgetType {
}
export default class WaveformPlayerPlugin extends Plugin {
settings: WaveformPlayerSettings = DEFAULT_SETTINGS;
// 设置版本号,每次设置变更时递增
settingsVersion: number = 0;
// 存储阅读视图中渲染的播放器
private readingViewPlayers: ReadingViewPlayerInfo[] = [];
// 编辑器视图
private editorViews: Set<EditorView> = new Set();
// 触发编辑器视图刷新
triggerEditorRefresh(): void {
this.editorViews.forEach(view => {
view.dispatch({
effects: AudioPlayerWidget.updateTrigger.of(undefined)
});
});
}
// 更新所有播放器
refreshAllPlayers(): void {
// 增加设置版本号
this.settingsVersion++;
// 更新编辑器视图中的播放器
AudioPlayerWidget.refreshAllPlayers(this);
// 更新阅读视图中的播放器
this.refreshReadingViewPlayers();
// 通知用户刷新完成
this.app.workspace.trigger('waveform-player:refresh-complete');
}
// 刷新阅读视图播放器
private refreshReadingViewPlayers(): void {
this.readingViewPlayers.forEach(playerInfo => {
try {
// 卸载并重新渲染播放器
playerInfo.root.unmount();
const root = createRoot(playerInfo.playerDiv);
root.render(
createPlayerElement({
src: playerInfo.audioUrl,
title: playerInfo.title,
plugin: this
})
);
// 更新播放器根节点引用
playerInfo.root = root;
} catch (error) {
console.error('[WaveformPlayerPlugin] Failed to refresh reading view player:', error);
}
});
}
createEditorExtension(): Extension {
const updateAudioPlayers = StateEffect.define<void>();
const plugin = this;
const audioPlayerField = StateField.define<DecorationSet>({
create: (state) => {
@ -145,7 +298,9 @@ export default class WaveformPlayerPlugin extends Plugin {
},
provide: field => EditorView.decorations.from(field),
update: (decorations, tr) => {
if (!tr.docChanged && !tr.effects.some(e => e.is(updateAudioPlayers))) {
if (!tr.docChanged &&
!tr.effects.some(e => e.is(updateAudioPlayers)) &&
!tr.effects.some(e => e.is(AudioPlayerWidget.updateTrigger))) {
return decorations;
}
@ -164,6 +319,11 @@ export default class WaveformPlayerPlugin extends Plugin {
});
}
// 如果是设置更新触发器,强制更新
if (tr.effects.some(e => e.is(AudioPlayerWidget.updateTrigger))) {
needsUpdate = true;
}
if (!needsUpdate && !tr.effects.some(e => e.is(updateAudioPlayers))) {
return decorations;
}
@ -174,16 +334,20 @@ export default class WaveformPlayerPlugin extends Plugin {
const viewportPlugin = ViewPlugin.fromClass(class {
private updateScheduled = false;
private timeout: NodeJS.Timeout | null = null;
private timeout: number | null = null;
constructor(private readonly view: EditorView) {
this.scheduleUpdate();
// 注册编辑器视图
plugin.editorViews.add(view);
}
destroy() {
if (this.timeout) {
clearTimeout(this.timeout);
}
// 移除编辑器视图
plugin.editorViews.delete(this.view);
}
update(update: ViewUpdate) {
@ -202,7 +366,7 @@ export default class WaveformPlayerPlugin extends Plugin {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(() => {
this.timeout = window.setTimeout(() => {
this.updateScheduled = false;
this.view.dispatch({
effects: updateAudioPlayers.of(undefined),
@ -215,26 +379,17 @@ export default class WaveformPlayerPlugin extends Plugin {
}
getAudioFile(src: string): null | TFile {
const decodedSrc = decodeURIComponent(src);
let audioFile = this.app.vault.getAbstractFileByPath(decodedSrc);
if (!audioFile || !(audioFile instanceof TFile)) {
const attachPath = this.app.vault.config.attachmentFolderPath;
if (!attachPath) {
return null;
}
audioFile = this.app.vault.getAbstractFileByPath(`${attachPath}/${decodedSrc}`);
if (!audioFile || !(audioFile instanceof TFile)) {
audioFile = this.app.vault.getAbstractFileByPath(`${attachPath}/${src}`);
if (!audioFile || !(audioFile instanceof TFile)) {
return null;
}
}
}
const audioFile = this.app.metadataCache.getFirstLinkpathDest(src, '');
return audioFile instanceof TFile ? audioFile : null;
}
async onload() {
// 加载设置
await this.loadSettings();
// 添加设置选项卡
this.addSettingTab(new WaveformPlayerSettingTab(this.app, this));
// 注册编辑器扩展
this.registerEditorExtension(this.createEditorExtension());
@ -267,35 +422,72 @@ export default class WaveformPlayerPlugin extends Plugin {
div.parentNode?.insertBefore(container, div.nextSibling);
const root = createRoot(playerDiv);
// 创建播放器信息并存储
const playerInfo: ReadingViewPlayerInfo = {
root,
container,
playerDiv,
audioUrl: decodedUrl,
title: audioFile.basename,
};
// 将播放器信息添加到列表中
this.readingViewPlayers.push(playerInfo);
// 当组件卸载时,从列表中移除
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of Array.from(mutation.removedNodes)) {
if (node.contains(container)) {
this.readingViewPlayers = this.readingViewPlayers.filter(p => p !== playerInfo);
observer.disconnect();
return;
}
}
}
});
// 监视 DOM 变化
if (div.parentNode) {
observer.observe(div.parentNode, { childList: true, subtree: true });
}
root.render(
createElement(WaveformPlayer as ComponentType<any>, {
className: 'wa-obsidian-player',
samplePoints: 200,
createPlayerElement({
src: decodedUrl,
styles: {
controls: {
paddingBottom: 0,
width: '156px',
},
root: {
padding: '0.5em',
},
title: {
fontSize: '14px',
margin: 0,
},
waveform: {
height: '100px',
},
},
type: 'mirror',
}),
title: audioFile.basename,
plugin: this
})
);
});
});
}
onunload() {
// 清理所有播放器
this.readingViewPlayers.forEach(playerInfo => {
try {
playerInfo.root.unmount();
} catch (error) {
console.error('[WaveformPlayerPlugin] Failed to unmount reading view player:', error);
}
});
this.readingViewPlayers = [];
this.editorViews.clear();
audioPlayerContexts.forEach(ctx => ctx.stop());
audioPlayerContexts = [];
}
// 加载设置
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
// 保存设置
async saveSettings() {
await this.saveData(this.settings);
}
private buildDecorations(state: EditorState): DecorationSet {
@ -310,9 +502,12 @@ export default class WaveformPlayerPlugin extends Plugin {
// 每次创建新的正则表达式实例
const regex = new RegExp(AUDIO_LINK_PATTERN, 'gi');
// 使用 matchAll 替代 while 循环
Array.from(lineText.matchAll(regex)).forEach((match) => {
const [, title, src] = match;
const [_, title, mdSrc, obsidianSrc] = match;
// 如果是 Obsidian 格式的链接,使用 obsidianSrc 作为源和标题
const src = obsidianSrc || mdSrc;
const effectiveTitle = obsidianSrc ? obsidianSrc.split('/').pop()?.replace(/\.[^.]+$/, '') || '' : (title || '');
if (!src) {
return;
}
@ -326,7 +521,7 @@ export default class WaveformPlayerPlugin extends Plugin {
block: true, // 添加 block 属性
persistent: true,
side: 1, // 在匹配文本后面插入
widget: new AudioPlayerWidget(src, title || '', this),
widget: new AudioPlayerWidget(src, effectiveTitle, this),
}).range(matchEnd),
);
});
@ -335,3 +530,71 @@ export default class WaveformPlayerPlugin extends Plugin {
return Decoration.set(widgets, true);
}
}
// 设置选项卡类
class WaveformPlayerSettingTab extends PluginSettingTab {
plugin: WaveformPlayerPlugin;
constructor(app: App, plugin: WaveformPlayerPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
new Setting(containerEl)
.setName(L.settings.stopOtherPlayers.title())
.setDesc(L.settings.stopOtherPlayers.description())
.addToggle(toggle => toggle
.setValue(this.plugin.settings.stopOthersOnPlay)
.onChange(async (value) => {
this.plugin.settings.stopOthersOnPlay = value;
await this.plugin.saveSettings();
// 设置变化时刷新所有播放器
this.plugin.refreshAllPlayers();
}));
new Setting(containerEl)
.setName(L.settings.waveformType.title())
.setDesc(L.settings.waveformType.description())
.addDropdown(dropdown => dropdown
.addOptions({
'bars': L.settings.waveformType.options.bars(),
'envelope': L.settings.waveformType.options.envelope(),
'line': L.settings.waveformType.options.line(),
'mirror': L.settings.waveformType.options.mirror(),
'wave': L.settings.waveformType.options.wave()
})
.setValue(this.plugin.settings.waveformType)
.onChange(async (value: string) => {
this.plugin.settings.waveformType = value as WaveformType;
await this.plugin.saveSettings();
// 设置变化时刷新所有播放器
this.plugin.refreshAllPlayers();
}));
new Setting(containerEl)
.setName(L.settings.samplePoints.title())
.setDesc(L.settings.samplePoints.description())
.addDropdown(dropdown => dropdown
.addOptions({
'50': L.settings.samplePoints.options[50](),
'100': L.settings.samplePoints.options[100](),
'200': L.settings.samplePoints.options[200](),
'500': L.settings.samplePoints.options[500](),
'1000': L.settings.samplePoints.options[1000](),
'2000': L.settings.samplePoints.options[2000](),
'5000': L.settings.samplePoints.options[5000]()
})
.setValue(this.plugin.settings.samplePoints.toString())
.onChange(async (value: string) => {
this.plugin.settings.samplePoints = parseInt(value, 10);
await this.plugin.saveSettings();
// 设置变化时刷新所有播放器
this.plugin.refreshAllPlayers();
}));
}
}

View file

@ -2,5 +2,8 @@
"0.0.1": "1.7.7",
"0.0.2": "1.7.7",
"0.0.3": "1.7.7",
"0.0.4": "1.7.7"
"0.0.4": "1.7.7",
"0.0.5": "1.7.7",
"0.0.6": "1.7.7",
"0.0.7": "1.7.7"
}