mirror of
https://github.com/asyouplz/SpeechNote.git
synced 2026-07-22 06:43:33 +00:00
* fix(review): resolve obsidian bot validation errors (metadata, lint, ui) * feat(release): prepare version 4.1.0 * fix(ci): add semantic-release configuration * fix(review): address pr feedback and stabilize tests Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(types): resolve typecheck regressions Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
291 lines
8.3 KiB
TypeScript
291 lines
8.3 KiB
TypeScript
import { Plugin } from 'obsidian';
|
|
import type { AppState } from '../../types';
|
|
import { Logger } from '../../infrastructure/logging/Logger';
|
|
import { StateManager } from '../../application/StateManager';
|
|
import { IDisposable } from '../../architecture/DependencyContainer';
|
|
|
|
/**
|
|
* StatusBar 아이템 설정
|
|
*/
|
|
interface StatusBarConfig {
|
|
text: string;
|
|
tooltip?: string;
|
|
className?: string;
|
|
hideAfter?: number; // 밀리초 단위
|
|
}
|
|
|
|
type StatusBarItem = HTMLElement & {
|
|
setText?: (text: string | DocumentFragment) => void;
|
|
};
|
|
|
|
/**
|
|
* StatusBar 관리자
|
|
* StatusBar 아이템의 안전한 생성과 관리를 담당
|
|
*/
|
|
export class StatusBarManager implements IDisposable {
|
|
private statusBarItem: StatusBarItem | null = null;
|
|
private hideTimeout: NodeJS.Timeout | null = null;
|
|
private unsubscribe: (() => void) | null = null;
|
|
private logger: Logger;
|
|
private isDisposed = false;
|
|
|
|
constructor(private plugin: Plugin, private stateManager: StateManager) {
|
|
this.logger = new Logger('StatusBarManager');
|
|
}
|
|
|
|
/**
|
|
* StatusBar 초기화
|
|
*/
|
|
public initialize(): void {
|
|
try {
|
|
// workspace가 준비되었는지 확인
|
|
if (!this.plugin.app.workspace) {
|
|
this.logger.warn('Workspace not ready for StatusBar');
|
|
return;
|
|
}
|
|
|
|
// StatusBar 아이템 생성 시도
|
|
this.createStatusBarItem();
|
|
|
|
// 상태 구독 설정
|
|
if (this.statusBarItem) {
|
|
this.subscribeToStateChanges();
|
|
}
|
|
|
|
this.logger.info('StatusBar initialized successfully');
|
|
} catch (error) {
|
|
this.logger.error(
|
|
'Failed to initialize StatusBar',
|
|
error instanceof Error ? error : undefined
|
|
);
|
|
// StatusBar 실패는 치명적이지 않으므로 에러를 던지지 않음
|
|
}
|
|
}
|
|
|
|
/**
|
|
* StatusBar 아이템 생성
|
|
*/
|
|
private createStatusBarItem(): void {
|
|
try {
|
|
// addStatusBarItem이 함수인지 확인
|
|
if (typeof this.plugin.addStatusBarItem !== 'function') {
|
|
this.logger.warn('addStatusBarItem is not available');
|
|
return;
|
|
}
|
|
|
|
// StatusBar 아이템 생성
|
|
const item = this.plugin.addStatusBarItem();
|
|
|
|
// 반환값 검증
|
|
if (!item || typeof item !== 'object') {
|
|
this.logger.warn('Invalid StatusBar item returned');
|
|
return;
|
|
}
|
|
|
|
this.statusBarItem = item;
|
|
this.logger.debug('StatusBar item created successfully');
|
|
} catch (error) {
|
|
this.logger.error(
|
|
'Error creating StatusBar item',
|
|
error instanceof Error ? error : undefined
|
|
);
|
|
this.statusBarItem = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 상태 변경 구독
|
|
*/
|
|
private subscribeToStateChanges(): void {
|
|
this.unsubscribe = this.stateManager.subscribe((state) => {
|
|
if (this.isDisposed) return;
|
|
|
|
const config = this.getStatusConfigForState(state);
|
|
this.updateStatus(config);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 상태에 따른 StatusBar 설정 생성
|
|
*/
|
|
private getStatusConfigForState(state: AppState): StatusBarConfig {
|
|
switch (state.status) {
|
|
case 'idle':
|
|
return { text: '' };
|
|
|
|
case 'processing':
|
|
return {
|
|
text: '🎙️ Transcribing...',
|
|
tooltip: 'Processing audio transcription',
|
|
className: 'status-processing',
|
|
};
|
|
|
|
case 'completed':
|
|
return {
|
|
text: '✅ Transcription complete',
|
|
tooltip: 'Transcription completed successfully',
|
|
className: 'status-success',
|
|
hideAfter: 3000,
|
|
};
|
|
|
|
case 'error':
|
|
return {
|
|
text: '❌ Transcription failed',
|
|
tooltip: state.error?.message || 'An error occurred',
|
|
className: 'status-error',
|
|
hideAfter: 5000,
|
|
};
|
|
|
|
default:
|
|
return { text: '' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* StatusBar 업데이트
|
|
*/
|
|
public updateStatus(config: StatusBarConfig): void {
|
|
if (!this.statusBarItem || this.isDisposed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 이전 타이머 취소
|
|
if (this.hideTimeout) {
|
|
clearTimeout(this.hideTimeout);
|
|
this.hideTimeout = null;
|
|
}
|
|
|
|
// setText 메서드 존재 확인
|
|
if (this.statusBarItem.setText) {
|
|
this.statusBarItem.setText(config.text);
|
|
} else if ('textContent' in this.statusBarItem) {
|
|
// textContent 직접 설정
|
|
this.statusBarItem.textContent = config.text;
|
|
} else {
|
|
this.logger.warn('Cannot update StatusBar text');
|
|
return;
|
|
}
|
|
|
|
// 툴팁 설정
|
|
if (config.tooltip) {
|
|
this.statusBarItem.setAttribute('aria-label', config.tooltip);
|
|
this.statusBarItem.setAttribute('title', config.tooltip);
|
|
}
|
|
|
|
// CSS 클래스 설정
|
|
if (config.className) {
|
|
this.statusBarItem.className = `status-bar-item ${config.className}`;
|
|
}
|
|
|
|
// 자동 숨김 설정
|
|
if (config.hideAfter && config.hideAfter > 0) {
|
|
this.hideTimeout = setTimeout(() => {
|
|
this.clearStatus();
|
|
}, config.hideAfter);
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(
|
|
'Failed to update StatusBar',
|
|
error instanceof Error ? error : undefined
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* StatusBar 텍스트 직접 설정
|
|
*/
|
|
public setText(text: string): void {
|
|
this.updateStatus({ text });
|
|
}
|
|
|
|
/**
|
|
* StatusBar 지우기
|
|
*/
|
|
public clearStatus(): void {
|
|
if (!this.statusBarItem || this.isDisposed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (this.statusBarItem.setText) {
|
|
this.statusBarItem.setText('');
|
|
} else if ('textContent' in this.statusBarItem) {
|
|
this.statusBarItem.textContent = '';
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(
|
|
'Failed to clear StatusBar',
|
|
error instanceof Error ? error : undefined
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* StatusBar 표시
|
|
*/
|
|
public show(): void {
|
|
if (this.statusBarItem) {
|
|
this.statusBarItem.classList.remove('sn-hidden');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* StatusBar 숨기기
|
|
*/
|
|
public hide(): void {
|
|
if (this.statusBarItem) {
|
|
this.statusBarItem.classList.add('sn-hidden');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* StatusBar 사용 가능 여부 확인
|
|
*/
|
|
public isAvailable(): boolean {
|
|
return this.statusBarItem !== null && !this.isDisposed;
|
|
}
|
|
|
|
/**
|
|
* 리소스 정리
|
|
*/
|
|
public dispose(): void {
|
|
if (this.isDisposed) return;
|
|
|
|
this.isDisposed = true;
|
|
|
|
// 타이머 정리
|
|
if (this.hideTimeout) {
|
|
clearTimeout(this.hideTimeout);
|
|
this.hideTimeout = null;
|
|
}
|
|
|
|
// 구독 해제
|
|
if (this.unsubscribe) {
|
|
this.unsubscribe();
|
|
this.unsubscribe = null;
|
|
}
|
|
|
|
// StatusBar 아이템 제거
|
|
if (this.statusBarItem) {
|
|
try {
|
|
// remove 메서드가 있으면 호출
|
|
if (typeof this.statusBarItem.remove === 'function') {
|
|
this.statusBarItem.remove();
|
|
} else if (this.statusBarItem.parentNode) {
|
|
// DOM에서 직접 제거
|
|
this.statusBarItem.parentNode.removeChild(this.statusBarItem);
|
|
}
|
|
} catch (error) {
|
|
this.logger.error(
|
|
'Error removing StatusBar item',
|
|
error instanceof Error ? error : undefined
|
|
);
|
|
}
|
|
|
|
this.statusBarItem = null;
|
|
}
|
|
|
|
this.logger.info('StatusBarManager disposed');
|
|
}
|
|
}
|