Skip to content
Closed
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
8 changes: 8 additions & 0 deletions openless-all/app/src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ pub struct UserPreferences {
pub enabled_modes: Vec<PolishMode>,
pub launch_at_login: bool,
pub show_capsule: bool,
/// 是否在录音胶囊中显示本次录音计时。默认关闭,保持既有胶囊视觉不变。
#[serde(default)]
pub show_capsule_elapsed_time: bool,
/// 录音期间临时静音系统输出,停止/取消/出错后恢复原静音状态。
#[serde(default)]
pub mute_during_recording: bool,
Expand Down Expand Up @@ -341,6 +344,8 @@ struct UserPreferencesWire {
launch_at_login: bool,
show_capsule: bool,
#[serde(default)]
show_capsule_elapsed_time: bool,
#[serde(default)]
mute_during_recording: bool,
#[serde(default)]
microphone_device_name: String,
Expand Down Expand Up @@ -401,6 +406,7 @@ impl Default for UserPreferencesWire {
enabled_modes: prefs.enabled_modes,
launch_at_login: prefs.launch_at_login,
show_capsule: prefs.show_capsule,
show_capsule_elapsed_time: prefs.show_capsule_elapsed_time,
mute_during_recording: prefs.mute_during_recording,
microphone_device_name: prefs.microphone_device_name,
active_asr_provider: prefs.active_asr_provider,
Expand Down Expand Up @@ -454,6 +460,7 @@ impl<'de> Deserialize<'de> for UserPreferences {
enabled_modes: wire.enabled_modes,
launch_at_login: wire.launch_at_login,
show_capsule: wire.show_capsule,
show_capsule_elapsed_time: wire.show_capsule_elapsed_time,
mute_during_recording: wire.mute_during_recording,
microphone_device_name: wire.microphone_device_name,
active_asr_provider: wire.active_asr_provider,
Expand Down Expand Up @@ -574,6 +581,7 @@ impl Default for UserPreferences {
],
launch_at_login: false,
show_capsule: true,
show_capsule_elapsed_time: false,
mute_during_recording: false,
microphone_device_name: String::new(),
active_asr_provider: default_active_asr_provider(),
Expand Down
83 changes: 79 additions & 4 deletions openless-all/app/src/components/Capsule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ import {
getCapsuleMessageLayout,
getCapsulePillMetrics,
} from '../lib/capsuleLayout';
import { invokeOrMock, isTauri } from '../lib/ipc';
import type { CapsulePayload, CapsuleState } from '../lib/types';
import { getSettings, invokeOrMock, isTauri } from '../lib/ipc';
import type { CapsulePayload, CapsuleState, UserPreferences } from '../lib/types';

interface AudioBarsProps {
level: number;
}

function formatElapsed(ms: number) {
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
}

function AudioBars({ level }: AudioBarsProps) {
const envelope = [0.55, 0.85, 1.0, 0.85, 0.55];
const base = 2;
Expand Down Expand Up @@ -144,13 +151,15 @@ interface PillProps {
os: OS;
state: CapsuleState;
level: number;
elapsedMs: number;
showElapsedTime: boolean;
insertedChars: number;
message?: string;
onCancel: () => void;
onConfirm: () => void;
}

function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }: PillProps) {
function Pill({ os, state, level, elapsedMs, showElapsedTime, insertedChars, message, onCancel, onConfirm }: PillProps) {
const { t } = useTranslation();
const metrics = getCapsulePillMetrics(os);
const processingLayout = getCapsuleMessageLayout(os, 'processing');
Expand All @@ -173,7 +182,34 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }:
let center: JSX.Element;
switch (state) {
case 'recording':
center = <AudioBars level={level} />;
center = showElapsedTime ? (
<div
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: 6,
width: '100%',
maxWidth: metrics.textWidth,
minWidth: 0,
}}
>
<AudioBars level={level} />
<span
style={{
fontSize: 10,
fontWeight: 600,
color: 'var(--ol-ink-2)',
whiteSpace: 'nowrap',
fontVariantNumeric: 'tabular-nums',
}}
>
{t('capsule.recordingElapsed', { time: formatElapsed(elapsedMs) })}
</span>
</div>
) : (
<AudioBars level={level} />
);
break;
case 'transcribing':
case 'polishing':
Expand Down Expand Up @@ -296,6 +332,8 @@ export function Capsule() {
const metrics = getCapsulePillMetrics(os);
const [state, setState] = useState<CapsuleState>(INITIAL_VISIBLE_STATE);
const [level, setLevel] = useState<number>(isTauri ? 0 : 0.6);
const [elapsedMs, setElapsedMs] = useState<number>(0);
const [showElapsedTime, setShowElapsedTime] = useState<boolean>(false);
const [insertedChars, setInsertedChars] = useState<number>(0);
const [message, setMessage] = useState<string | undefined>();
const [translation, setTranslation] = useState<boolean>(false);
Expand All @@ -319,6 +357,9 @@ export function Capsule() {
const p = event.payload;
setState(p.state);
setLevel(p.level ?? 0);
if (p.elapsedMs != null) {
setElapsedMs(p.elapsedMs);
}
setMessage(p.message ?? undefined);
if (p.insertedChars != null) setInsertedChars(p.insertedChars);
setTranslation(p.translation === true);
Expand All @@ -332,6 +373,37 @@ export function Capsule() {
};
}, []);

useEffect(() => {
let unlisten: (() => void) | undefined;
let cancelled = false;

void getSettings()
.then(prefs => {
if (!cancelled) setShowElapsedTime(prefs.showCapsuleElapsedTime === true);
})
.catch(error => {
console.warn('[capsule] failed to load preferences', error);
});

if (isTauri) {
(async () => {
const { listen } = await import('@tauri-apps/api/event');
const handle = await listen<UserPreferences>('prefs:changed', event => {
setShowElapsedTime(event.payload.showCapsuleElapsedTime === true);
});
if (cancelled) handle();
else unlisten = handle;
})().catch(error => {
console.warn('[capsule] prefs:changed listener setup failed', error);
});
}

return () => {
cancelled = true;
if (unlisten) unlisten();
};
}, []);

// 退出动画调度:在 state 真正进入 idle 时,先用 capsule-out 播放 EXIT_ANIM_MS,再卸载。
// 设计要点:
// 1. 进入非 idle:清掉 leaving,记录最新可见 state;
Expand All @@ -351,6 +423,7 @@ export function Capsule() {
const timer = setTimeout(() => {
setLeaving(false);
setLastVisibleState('idle');
setElapsedMs(0);
}, EXIT_ANIM_MS);
return () => clearTimeout(timer);
// 故意只依赖 state —— lastVisibleState / leaving 是内部派生量,
Expand Down Expand Up @@ -456,6 +529,8 @@ export function Capsule() {
os={os}
state={renderedState}
level={leaving ? 0 : level}
elapsedMs={elapsedMs}
showElapsedTime={showElapsedTime}
insertedChars={insertedChars}
message={message}
onCancel={onCancel}
Expand Down
5 changes: 4 additions & 1 deletion openless-all/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ export const en: typeof zhCN = {
},
capsule: {
thinking: 'thinking',
recordingElapsed: '{{time}}',
cancelled: 'Cancelled',
error: 'Something went wrong',
inserted: 'Inserted {{count}}',
inserted: 'Inserted {{count}} chars',
translating: 'Translating',
},
qa: {
Expand Down Expand Up @@ -322,6 +323,8 @@ export const en: typeof zhCN = {
microphoneMonitorError: 'Failed to monitor input level: {{message}}',
capsuleLabel: 'Recording capsule',
capsuleDesc: 'Show a translucent capsule at the bottom of the screen while recording.',
capsuleElapsedTimeLabel: 'Show recording timer',
capsuleElapsedTimeDesc: 'Show elapsed recording time inside the capsule while recording.',
muteDuringRecordingLabel: 'Mute while recording',
muteDuringRecordingDesc: 'Temporarily mute system output during voice input to avoid speaker echo.',
insertGroupTitle: 'Insertion & clipboard',
Expand Down
3 changes: 3 additions & 0 deletions openless-all/app/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const ja: typeof zhCN = {
},
capsule: {
thinking: 'thinking',
recordingElapsed: '{{time}}',
cancelled: 'キャンセルしました',
error: 'エラーが発生しました',
inserted: '{{count}} 文字を入力しました',
Expand Down Expand Up @@ -324,6 +325,8 @@ export const ja: typeof zhCN = {
microphoneMonitorError: '入力レベルの監視に失敗:{{message}}',
capsuleLabel: '録音カプセル',
capsuleDesc: '録音 / 転写中、画面下部に半透明のカプセルを表示。',
capsuleElapsedTimeLabel: '録音タイマーを表示',
capsuleElapsedTimeDesc: 'オンにすると、録音中の経過時間をカプセル内に表示します。',
muteDuringRecordingLabel: '録音中はミュート',
muteDuringRecordingDesc: '録音中にシステム出力を一時的にミュートし、スピーカーのエコーを防ぎます。',
insertGroupTitle: '挿入とクリップボード',
Expand Down
3 changes: 3 additions & 0 deletions openless-all/app/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const ko: typeof zhCN = {
},
capsule: {
thinking: 'thinking',
recordingElapsed: '{{time}}',
cancelled: '취소됨',
error: '오류 발생',
inserted: '{{count}}자 입력됨',
Expand Down Expand Up @@ -324,6 +325,8 @@ export const ko: typeof zhCN = {
microphoneMonitorError: '입력 레벨 모니터링 실패: {{message}}',
capsuleLabel: '녹음 캡슐',
capsuleDesc: '녹음 / 전사 중 화면 하단에 반투명 캡슐을 표시합니다.',
capsuleElapsedTimeLabel: '녹음 타이머 표시',
capsuleElapsedTimeDesc: '켜면 녹음 중 경과 시간을 캡슐 안에 표시합니다.',
muteDuringRecordingLabel: '녹음 중 음소거',
muteDuringRecordingDesc: '녹음 중 시스템 출력을 일시적으로 음소거하여 스피커 에코를 방지합니다.',
insertGroupTitle: '삽입 및 클립보드',
Expand Down
5 changes: 4 additions & 1 deletion openless-all/app/src/i18n/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ export const zhCN = {
},
capsule: {
thinking: 'thinking',
recordingElapsed: '{{time}}',
cancelled: '已取消',
error: '出错了',
inserted: '已插入 {{count}}',
inserted: '已插入 {{count}}',
translating: '正在翻译',
},
qa: {
Expand Down Expand Up @@ -320,6 +321,8 @@ export const zhCN = {
microphoneMonitorError: '输入电平监听失败:{{message}}',
capsuleLabel: '录音胶囊',
capsuleDesc: '录音 / 转写时显示屏幕底部胶囊。',
capsuleElapsedTimeLabel: '显示录音计时',
capsuleElapsedTimeDesc: '开启后在录音胶囊中显示本次录音时长。',
muteDuringRecordingLabel: '录音时静音',
muteDuringRecordingDesc: '录音期间临时静音系统输出,避免扬声器回音。',
insertGroupTitle: '插入与剪贴板',
Expand Down
5 changes: 4 additions & 1 deletion openless-all/app/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ export const zhTW: typeof zhCN = {
},
capsule: {
thinking: 'thinking',
recordingElapsed: '{{time}}',
cancelled: '已取消',
error: '出錯了',
inserted: '已插入 {{count}}',
inserted: '已插入 {{count}}',
translating: '正在翻譯',
},
qa: {
Expand Down Expand Up @@ -329,6 +330,8 @@ export const zhTW: typeof zhCN = {
microphoneMonitorError: '輸入電平監聽失敗:{{message}}',
capsuleLabel: '錄音膠囊',
capsuleDesc: '錄音 / 轉寫時在屏幕底部顯示半透明膠囊。',
capsuleElapsedTimeLabel: '顯示錄音計時',
capsuleElapsedTimeDesc: '開啟後在錄音膠囊中顯示本次錄音時長。',
muteDuringRecordingLabel: '錄音時靜音',
muteDuringRecordingDesc: '錄音期間臨時靜音系統輸出,避免揚聲器回音。',
insertGroupTitle: '插入與剪貼板',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/lib/ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const mockSettings: UserPreferences = {
enabledModes: ['raw', 'light', 'structured', 'formal'],
launchAtLogin: false,
showCapsule: true,
showCapsuleElapsedTime: false,
muteDuringRecording: false,
microphoneDeviceName: '',
activeAsrProvider: 'foundry-local-whisper',
Expand Down
1 change: 1 addition & 0 deletions openless-all/app/src/lib/stylePrefs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const previousPrefs: UserPreferences = {
enabledModes: ['raw', 'light', 'structured'],
launchAtLogin: false,
showCapsule: true,
showCapsuleElapsedTime: false,
muteDuringRecording: false,
microphoneDeviceName: '',
activeAsrProvider: 'volcengine',
Expand Down
2 changes: 2 additions & 0 deletions openless-all/app/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ export interface UserPreferences {
enabledModes: PolishMode[];
launchAtLogin: boolean;
showCapsule: boolean;
/** 在录音胶囊中显示本次录音计时。默认关闭,保持原胶囊视觉不变。 */
showCapsuleElapsedTime: boolean;
/** 录音期间临时静音系统输出,停止/取消/出错后恢复原静音状态。 */
muteDuringRecording: boolean;
/** 录音输入设备名称。空字符串 = 使用系统默认麦克风。 */
Expand Down
12 changes: 10 additions & 2 deletions openless-all/app/src/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,8 +288,10 @@ function RecordingSection() {

const onModeChange = (mode: HotkeyMode) =>
savePrefs({ ...prefs, hotkey: { ...prefs.hotkey, mode } });
const onShowCapsuleChange = (showCapsule: boolean) =>
savePrefs({ ...prefs, showCapsule });
const onShowCapsuleChange = (nextShowCapsule: boolean) =>
savePrefs({ ...prefs, showCapsule: nextShowCapsule });
const onShowCapsuleElapsedTimeChange = (showCapsuleElapsedTime: boolean) =>
savePrefs({ ...prefs, showCapsuleElapsedTime });
const onMuteDuringRecordingChange = (muteDuringRecording: boolean) =>
savePrefs({ ...prefs, muteDuringRecording });
const onMicrophoneDeviceChange = (microphoneDeviceName: string) =>
Expand Down Expand Up @@ -454,6 +456,12 @@ function RecordingSection() {
<SettingRow label={t('settings.recording.capsuleLabel')} desc={t('settings.recording.capsuleDesc')}>
<Toggle on={prefs.showCapsule} onToggle={onShowCapsuleChange} />
</SettingRow>
<SettingRow
label={t('settings.recording.capsuleElapsedTimeLabel')}
desc={t('settings.recording.capsuleElapsedTimeDesc')}
>
<Toggle on={prefs.showCapsuleElapsedTime} onToggle={onShowCapsuleElapsedTimeChange} />
</SettingRow>
<SettingRow
label={t('settings.recording.muteDuringRecordingLabel')}
desc={t('settings.recording.muteDuringRecordingDesc')}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.