Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
46 changes: 22 additions & 24 deletions src/editor/EmbeddableMarkdownEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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
);
}
},
Expand All @@ -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)));
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion src/markdown/AlphaTexBlock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};
Expand Down
49 changes: 28 additions & 21 deletions src/player/PlayerController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class PlayerController {
* @param container - AlphaTab 渲染目标容器
* @param viewport - 滚动视口容器(可选)
*/
public async init(container: HTMLElement, viewport?: HTMLElement): Promise<void> {
public init(container: HTMLElement, viewport?: HTMLElement): void {
if (!container) {
console.error(
`[PlayerController #${this.instanceId}] Container not provided to init()`
Expand All @@ -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();
} /**
* 销毁控制器
*/
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand All @@ -1110,9 +1116,9 @@ export class PlayerController {
}
}

async loadScoreFromAlphaTex(tex: string): Promise<void> {
loadScoreFromAlphaTex(tex: string): Promise<void> {
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...');
Expand All @@ -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);
}
Expand Down
9 changes: 6 additions & 3 deletions src/player/ReactView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
console.debug('[ReactView] Opening view...');

// 1. 创建 stores(使用 StoreFactory)
Expand Down Expand Up @@ -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<void> {
console.debug('[ReactView] Closing view...');

// 注意: 不移除全局字体样式,因为可能有其他实例在使用
Expand Down Expand Up @@ -144,6 +145,7 @@ export class ReactView extends FileView {
console.debug('[ReactView] View closed');

// 注意:controller.destroy() 会清理实例状态,无需额外重置全局状态
await Promise.resolve();
}

async onLoadFile(file: TFile): Promise<void> {
Expand All @@ -163,6 +165,7 @@ export class ReactView extends FileView {
this.updateSwitchToEditorButton();
// 针对可打印谱面类型,添加打印预览按钮
this.updatePrintPreviewButton();
await Promise.resolve();
}

async onUnloadFile(file: TFile): Promise<void> {
Expand Down
14 changes: 9 additions & 5 deletions src/player/components/ExportModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export const ExportModal: React.FC<ExportModalProps> = ({ controller, isOpen, on
await exportMP3();
break;
case 'midi':
await exportMIDI();
exportMIDI();
break;
case 'gp':
await exportGP();
Expand Down Expand Up @@ -259,11 +259,12 @@ export const ExportModal: React.FC<ExportModalProps> = ({ controller, isOpen, on
];

return (
<div className="modal-container mod-dim" onClick={onClose}>
<div className="modal mod-settings" onClick={(e) => e.stopPropagation()}>
<div className="modal-container mod-dim">
<div className="modal mod-settings">
<div className="modal-header">
<div className="modal-title">导出乐谱</div>
<button
type="button"
className="clickable-icon modal-close-button"
onClick={onClose}
aria-label="关闭"
Expand Down Expand Up @@ -418,13 +419,16 @@ export const ExportModal: React.FC<ExportModalProps> = ({ controller, isOpen, on

<div className="modal-button-container">
<button
type="button"
className="mod-cta"
onClick={handleExport}
onClick={() => {
void handleExport();
}}
disabled={status === 'exporting'}
>
{status === 'exporting' ? '导出中...' : '开始导出'}
</button>
<button onClick={onClose} disabled={status === 'exporting'}>
<button type="button" onClick={onClose} disabled={status === 'exporting'}>
{status === 'success' ? '关闭' : '取消'}
</button>
</div>
Expand Down
Loading
Loading