diff --git a/manifest.json b/manifest.json index 507f49a6..6956e679 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "id": "tab-flow", "name": "Tab Flow", - "version": "0.5.0", + "version": "0.5.2", "minAppVersion": "1.8.0", "description": "Render, play and write guitar tabs. Support guitar pro(gtp) files(.gp, .gp3, .gp4, .gp5, .gpx), write scores in alphaTex (.atex).", "author": "Jay Bridge", diff --git a/package-lock.json b/package-lock.json index c93e918a..b398f820 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "obsidian-tab-flow", - "version": "0.5.0", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "obsidian-tab-flow", - "version": "0.5.0", + "version": "0.5.2", "license": "MPL-2.0", "dependencies": { "@codemirror/autocomplete": "^6.20.1", diff --git a/package.json b/package.json index 7335acae..3901ef47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "obsidian-tab-flow", - "version": "0.5.0", + "version": "0.5.2", "description": "Render, play and create guitar tabs. Write tabs in text. Modern music font and sound! (.alphatab, .gp, .gp3, .gp4, .gp5, .gpx) Powered by alphaTab.js.", "main": "main.js", "type": "module", diff --git a/src/editor/EmbeddableMarkdownEditor.ts b/src/editor/EmbeddableMarkdownEditor.ts index e36bfb72..fe346eef 100644 --- a/src/editor/EmbeddableMarkdownEditor.ts +++ b/src/editor/EmbeddableMarkdownEditor.ts @@ -105,22 +105,20 @@ export class EmbeddableMarkdownEditor { this.scope = new Scope(app.scope); this.scope.register(['Mod'], 'Enter', () => true); - // eslint-disable-next-line @typescript-eslint/no-this-alias -- capture self for monkey-patched hooks - const selfRef = this; // capture instance for function-based hooks + const getOwner = () => this; const rawUninstaller = around(EditorClass.prototype, { buildLocalExtensions: (originalMethod: (this: InternalMarkdownEditor) => unknown[]) => function (this: InternalMarkdownEditor) { const extensions = originalMethod.call(this) || []; - // Note: selfRef.editor is set after EditorClass instantiation, so we check if this instance - // matches the one that will be assigned to selfRef.editor by comparing after assignment // For now, we'll apply extensions to all instances and rely on the editor being set correctly - const editorRef = selfRef.editor; + const owner = getOwner(); + const editorRef = owner.editor; // Only apply extensions if editor is already set and matches // During initial construction, editorRef will be undefined, so we apply to all instances // After editor is set, we only apply to the matching instance if (editorRef === undefined || this === editorRef) { - if (selfRef.options.placeholder) - extensions.push(placeholder(selfRef.options.placeholder)); + if (owner.options.placeholder) + extensions.push(placeholder(owner.options.placeholder)); // Disable browser spellcheck/auto-correct in the embedded editor extensions.push( EditorView.editorAttributes.of({ @@ -131,28 +129,28 @@ export class EmbeddableMarkdownEditor { ); extensions.push( EditorView.domEventHandlers({ - paste: (event) => selfRef.options.onPaste?.(event, selfRef), + paste: (event) => owner.options.onPaste?.(event, owner), blur: () => { - app.keymap.popScope(selfRef.scope); + app.keymap.popScope(owner.scope); const activeEditor = Reflect.get( app.workspace, 'activeEditor' ) as unknown; if ( EmbeddableMarkdownEditor.USE_ACTIVE_EDITOR && - activeEditor === selfRef.editor + activeEditor === owner.editor ) { Reflect.set(app.workspace, 'activeEditor', null); } - selfRef.options.onBlur?.(selfRef); + owner.options.onBlur?.(owner); }, focusin: () => { - app.keymap.pushScope(selfRef.scope); + app.keymap.pushScope(owner.scope); if (EmbeddableMarkdownEditor.USE_ACTIVE_EDITOR) { Reflect.set( app.workspace, 'activeEditor', - selfRef.editor ?? null + owner.editor ?? null ); } }, @@ -161,28 +159,28 @@ export class EmbeddableMarkdownEditor { const keyBindings = [ { key: 'Enter', - run: () => selfRef.options.onEnter?.(selfRef, false, false), - shift: () => selfRef.options.onEnter?.(selfRef, false, true), + run: () => owner.options.onEnter?.(owner, false, false), + shift: () => owner.options.onEnter?.(owner, false, true), }, { key: 'Mod-Enter', - run: () => selfRef.options.onEnter?.(selfRef, true, false), - shift: () => selfRef.options.onEnter?.(selfRef, true, true), + run: () => owner.options.onEnter?.(owner, true, false), + shift: () => owner.options.onEnter?.(owner, true, true), }, { key: 'Escape', run: () => { - selfRef.options.onEscape?.(selfRef); + owner.options.onEscape?.(owner); return true; }, preventDefault: true, }, ]; - if (selfRef.options.singleLine) { + if (owner.options.singleLine) { keyBindings[0] = { key: 'Enter', - run: () => selfRef.options.onEnter?.(selfRef, false, false), - shift: () => selfRef.options.onEnter?.(selfRef, false, true), + run: () => owner.options.onEnter?.(owner, false, false), + shift: () => owner.options.onEnter?.(owner, false, true), }; } extensions.push(Prec.highest(keymap.of(keyBindings))); @@ -191,10 +189,10 @@ export class EmbeddableMarkdownEditor { const resolveSetting = (key: string, def = true) => { try { if ( - selfRef.options.highlightSettings && - key in selfRef.options.highlightSettings + owner.options.highlightSettings && + key in owner.options.highlightSettings ) { - return !!selfRef.options.highlightSettings[key]; + return !!owner.options.highlightSettings[key]; } // fallback: some callers may expose settings on window for minimal changes const globalSettings = Reflect.get( diff --git a/src/markdown/AlphaTexBlock.ts b/src/markdown/AlphaTexBlock.ts index c40d73fd..b138ccc1 100644 --- a/src/markdown/AlphaTexBlock.ts +++ b/src/markdown/AlphaTexBlock.ts @@ -732,7 +732,7 @@ export function mountAlphaTexBlock( // compact note, no controls container const note = document.createElement('div'); note.className = 'alphatex-note'; - note.textContent = 'SoundFont missing: Playback disabled. Rendering only.'; + note.textContent = 'Soundfont missing: playback disabled. Rendering only.'; wrapper.appendChild(note); } }; diff --git a/src/player/PlayerController.ts b/src/player/PlayerController.ts index c3c15adf..dd493bac 100644 --- a/src/player/PlayerController.ts +++ b/src/player/PlayerController.ts @@ -148,7 +148,7 @@ export class PlayerController { * @param container - AlphaTab 渲染目标容器 * @param viewport - 滚动视口容器(可选) */ - public async init(container: HTMLElement, viewport?: HTMLElement): Promise { + public init(container: HTMLElement, viewport?: HTMLElement): void { if (!container) { console.error( `[PlayerController #${this.instanceId}] Container not provided to init()` @@ -169,18 +169,7 @@ export class PlayerController { ); } - try { - this.rebuildApi(); - } catch (error) { - console.error( - `[PlayerController #${this.instanceId}] API initialization failed:`, - error - ); - this.stores.runtime - .getState() - .setError('api-init', error instanceof Error ? error.message : String(error)); - throw error; - } + void this.rebuildApi(); } /** * 销毁控制器 */ @@ -291,7 +280,7 @@ export class PlayerController { if (lastScore.type === 'alphatex') { this.api.tex(lastScore.data as string); } else if (lastScore.type === 'binary') { - await this.api.load(lastScore.data as Uint8Array); + this.api.load(lastScore.data as Uint8Array); } console.debug( `[PlayerController #${this.instanceId}] Last score reloaded successfully` @@ -672,14 +661,29 @@ export class PlayerController { this.eventDisposers.push(this.api.renderFinished.on(renderFinishedHandler)); // Player Ready - const playerReadyHandler = async () => { + const playerReadyHandler = () => { console.debug('[PlayerController] Player ready - can now play music'); this.stores.runtime.getState().setApiReady(true); // 播放器就绪后,检查是否有待加载的文件 if (this.pendingFileLoad) { - await this.pendingFileLoad(); + const pendingLoad = this.pendingFileLoad; this.pendingFileLoad = null; + void pendingLoad().catch((error) => { + console.error( + `[PlayerController #${this.instanceId}] Pending file load failed:`, + error + ); + this.stores.runtime + .getState() + .setError( + 'score-load', + error instanceof Error ? error.message : String(error) + ); + this.stores.ui + .getState() + .showToast('error', 'Failed to load score after player ready'); + }); } }; this.eventDisposers.push(this.api.playerReady.on(playerReadyHandler)); @@ -1063,7 +1067,8 @@ export class PlayerController { this.stores.runtime.getState().clearError(); try { - await this.api.load(url); + this.api.load(url); + await Promise.resolve(); this.stores.workspaceConfig.getState().setScoreSource({ type: 'url', content: url }); this.stores.ui.getState().showToast('success', 'Score loaded successfully'); } catch (error) { @@ -1089,7 +1094,8 @@ export class PlayerController { try { const uint8Array = new Uint8Array(arrayBuffer); - await this.api.load(uint8Array); + this.api.load(uint8Array); + await Promise.resolve(); // 保存乐谱数据用于 API 重建后重新加载 this.stores.runtime.getState().setLastLoadedScore('binary', uint8Array, fileName); @@ -1110,9 +1116,9 @@ export class PlayerController { } } - async loadScoreFromAlphaTex(tex: string): Promise { + loadScoreFromAlphaTex(tex: string): Promise { if (!this.api) { - throw new Error('API not initialized'); + return Promise.reject(new Error('API not initialized')); } this.stores.ui.getState().setLoading(true, 'Loading score...'); @@ -1129,13 +1135,14 @@ export class PlayerController { .getState() .setScoreSource({ type: 'alphatex', content: tex }); this.stores.ui.getState().showToast('success', 'Score loaded successfully'); + return Promise.resolve(); } catch (error) { console.error('[PlayerController] Failed to load score:', error); this.stores.runtime .getState() .setError('score-load', error instanceof Error ? error.message : String(error)); this.stores.ui.getState().showToast('error', 'Failed to load score'); - throw error; + return Promise.reject(error); } finally { this.stores.ui.getState().setLoading(false); } diff --git a/src/player/ReactView.ts b/src/player/ReactView.ts index dd883937..dbee188f 100644 --- a/src/player/ReactView.ts +++ b/src/player/ReactView.ts @@ -47,10 +47,10 @@ export class ReactView extends FileView { if (this.currentFile) { return this.currentFile.basename; } - return 'Tab Player'; + return 'Tab player'; } - async onOpen() { + async onOpen(): Promise { console.debug('[ReactView] Opening view...'); // 1. 创建 stores(使用 StoreFactory) @@ -101,9 +101,10 @@ export class ReactView extends FileView { this.renderReactComponent(); console.debug('[ReactView] View opened successfully'); + await Promise.resolve(); } - async onClose() { + async onClose(): Promise { console.debug('[ReactView] Closing view...'); // 注意: 不移除全局字体样式,因为可能有其他实例在使用 @@ -144,6 +145,7 @@ export class ReactView extends FileView { console.debug('[ReactView] View closed'); // 注意:controller.destroy() 会清理实例状态,无需额外重置全局状态 + await Promise.resolve(); } async onLoadFile(file: TFile): Promise { @@ -163,6 +165,7 @@ export class ReactView extends FileView { this.updateSwitchToEditorButton(); // 针对可打印谱面类型,添加打印预览按钮 this.updatePrintPreviewButton(); + await Promise.resolve(); } async onUnloadFile(file: TFile): Promise { diff --git a/src/player/components/ExportModal.tsx b/src/player/components/ExportModal.tsx index fa9c1993..d140a7bb 100644 --- a/src/player/components/ExportModal.tsx +++ b/src/player/components/ExportModal.tsx @@ -56,7 +56,7 @@ export const ExportModal: React.FC = ({ controller, isOpen, on await exportMP3(); break; case 'midi': - await exportMIDI(); + exportMIDI(); break; case 'gp': await exportGP(); @@ -259,11 +259,12 @@ export const ExportModal: React.FC = ({ controller, isOpen, on ]; return ( -
-
e.stopPropagation()}> +
+
导出乐谱
-
diff --git a/src/player/components/MediaSync.tsx b/src/player/components/MediaSync.tsx index 610cf0e3..0df35823 100644 --- a/src/player/components/MediaSync.tsx +++ b/src/player/components/MediaSync.tsx @@ -24,6 +24,17 @@ interface MediaSyncProps { onClose?: () => void; } +function hasSuspiciousUrlChars(value: string): boolean { + for (const char of value) { + const code = char.charCodeAt(0); + if ((code >= 0 && code <= 0x1f) || code === 0x7f || char === '<' || char === '>') { + return true; + } + } + + return false; +} + /** * 清理和验证媒体 URL,防止 XSS 攻击 * 只允许 http(s)://, file://, 和 blob: 协议 @@ -44,8 +55,7 @@ function sanitizeMediaUrl(url: string): string | null { } // 拒绝包含可疑字符的 URL (控制字符、换行符、尖括号等) - // eslint-disable-next-line no-control-regex - if (/[\u0000-\u001F\u007F<>]/.test(parsed.href)) { + if (hasSuspiciousUrlChars(parsed.href)) { console.warn('[MediaSync] Blocked URL with suspicious characters'); return null; } @@ -292,6 +302,7 @@ export const MediaSync: React.FC = ({ controller, app, isOpen, o
{/* 媒体类型选择按钮 */} {mediaState.type !== MediaType.Synth && (
)} @@ -528,7 +556,9 @@ export const MediaSync: React.FC = ({ controller, app, isOpen, o onTimeUpdate={(e) => { setPlaybackTime(e.currentTarget.currentTime * 1000); }} - /> + > + +
)} @@ -536,6 +566,7 @@ export const MediaSync: React.FC = ({ controller, app, isOpen, o {mediaState.type === MediaType.YouTube && (