<script lang="ts">
|
import { onMount } from 'svelte'
|
import { Call, Events } from '@wailsio/runtime'
|
import { t } from '../../lib/i18n'
|
|
interface TextOutputResult {
|
ok?: boolean
|
needsPermission?: boolean
|
directInserted?: boolean
|
fallbackAvailable?: boolean
|
createdAt?: string
|
}
|
|
interface ClipboardFallbackResult {
|
ok?: boolean
|
prepared?: boolean
|
restored?: boolean
|
token?: string
|
}
|
|
let textOutput = $state<TextOutputResult | null>(null)
|
let outputStatusText = $state('')
|
let outputActionText = $state('')
|
let fallbackToken = $state('')
|
let fallbackPrepared = $state(false)
|
let fallbackBusy = $state(false)
|
let lastOutputCreatedAt = $state('')
|
|
onMount(() => {
|
const offTextOutputUpdated = Events.On('text-output:updated', () => {
|
void refreshTextOutput()
|
})
|
const pollTextOutput = window.setInterval(() => {
|
void refreshTextOutput(true)
|
}, 1000)
|
void refreshTextOutput()
|
return () => {
|
window.clearInterval(pollTextOutput)
|
offTextOutputUpdated()
|
}
|
})
|
|
async function refreshTextOutput(preserveActionText = false) {
|
try {
|
const result: TextOutputResult = await Call.ByName('voicesnap/services.TextOutputService.GetLastResult')
|
const nextOutputCreatedAt = outputResultKey(result)
|
const isNewOutput = !!nextOutputCreatedAt && !!lastOutputCreatedAt && nextOutputCreatedAt !== lastOutputCreatedAt
|
if (isNewOutput) {
|
fallbackToken = ''
|
fallbackPrepared = false
|
outputActionText = ''
|
}
|
if (nextOutputCreatedAt) {
|
lastOutputCreatedAt = nextOutputCreatedAt
|
}
|
textOutput = result || null
|
outputStatusText = textOutputStatus(result)
|
if (!isNewOutput && !preserveActionText && !fallbackPrepared) {
|
outputActionText = ''
|
}
|
} catch {
|
textOutput = null
|
outputStatusText = t('settings.textOutputUnavailable')
|
}
|
}
|
|
async function prepareClipboardFallback() {
|
if (fallbackPrepared) return
|
fallbackBusy = true
|
try {
|
const result: ClipboardFallbackResult = await Call.ByName('voicesnap/services.TextOutputService.PrepareLastClipboardFallback')
|
if (result?.ok && result?.prepared && result?.token) {
|
fallbackToken = result.token
|
fallbackPrepared = true
|
outputActionText = t('settings.textOutputFallbackPrepared')
|
} else {
|
outputActionText = t('settings.textOutputFallbackUnavailable')
|
}
|
} catch {
|
outputActionText = t('settings.textOutputFallbackUnavailable')
|
} finally {
|
fallbackBusy = false
|
void refreshTextOutput()
|
}
|
}
|
|
async function restoreClipboardFallback() {
|
if (!fallbackToken) return
|
fallbackBusy = true
|
try {
|
const result: ClipboardFallbackResult = await Call.ByName('voicesnap/services.TextOutputService.RestoreClipboardFallback', fallbackToken)
|
fallbackToken = ''
|
fallbackPrepared = false
|
if (result?.ok && result?.restored) {
|
outputActionText = t('settings.textOutputClipboardRestored')
|
} else {
|
outputActionText = t('settings.textOutputClipboardKept')
|
}
|
} catch {
|
outputActionText = t('settings.textOutputClipboardRestoreFailed')
|
} finally {
|
fallbackBusy = false
|
void refreshTextOutput(true)
|
}
|
}
|
|
function textOutputStatus(result: TextOutputResult | null | undefined): string {
|
if (!hasOutputResult(result)) {
|
return t('settings.textOutputNoResult')
|
}
|
if (result.ok && result.directInserted) {
|
return t('settings.textOutputInserted')
|
}
|
if (result.ok) {
|
return t('settings.textOutputCompleted')
|
}
|
if (result.needsPermission) {
|
return t('settings.textOutputNeedsPermission')
|
}
|
if (result.fallbackAvailable) {
|
return t('settings.textOutputFallbackAvailable')
|
}
|
return t('settings.textOutputFailed')
|
}
|
|
function hasOutputResult(result: TextOutputResult | null | undefined): result is TextOutputResult {
|
if (!result) return false
|
if (result.ok || result.needsPermission || result.directInserted || result.fallbackAvailable) return true
|
return !!outputResultKey(result)
|
}
|
|
function outputResultKey(result: TextOutputResult | null | undefined): string {
|
const createdAt = result?.createdAt || ''
|
if (!createdAt || createdAt.startsWith('0001-')) return ''
|
return createdAt
|
}
|
</script>
|
|
<div class="text-output-panel">
|
<div class="setting-info">
|
<span class="setting-label">{t('settings.textOutputTitle')}</span>
|
<span class="setting-desc">{t('settings.textOutputDesc')}</span>
|
</div>
|
<div class="text-output-status" class:success={!!textOutput?.ok} class:warn={!!textOutput && !textOutput.ok}>
|
{outputStatusText || t('settings.textOutputNoResult')}
|
</div>
|
{#if outputActionText}
|
<div class="fallback-message">{outputActionText}</div>
|
{/if}
|
<div class="fallback-actions">
|
<button class="secondary-btn" onclick={() => refreshTextOutput()} disabled={fallbackBusy}>
|
{t('settings.textOutputRefresh')}
|
</button>
|
<button
|
class="primary-btn"
|
onclick={prepareClipboardFallback}
|
disabled={fallbackBusy || fallbackPrepared || !textOutput?.fallbackAvailable}
|
>
|
{t('settings.textOutputUseClipboard')}
|
</button>
|
{#if fallbackPrepared}
|
<button class="secondary-btn" onclick={restoreClipboardFallback} disabled={fallbackBusy || !fallbackToken}>
|
{t('settings.textOutputRestoreClipboard')}
|
</button>
|
{/if}
|
</div>
|
</div>
|
|
<style>
|
.text-output-panel {
|
display: flex;
|
flex-direction: column;
|
gap: var(--spacing-sm);
|
background: var(--color-bg-grouped-secondary);
|
border-radius: var(--radius-md);
|
margin-bottom: var(--spacing-md);
|
padding: var(--spacing-lg);
|
}
|
|
.setting-info {
|
display: flex;
|
flex-direction: column;
|
gap: 2px;
|
}
|
|
.setting-label {
|
font-size: var(--font-size-base);
|
font-weight: 500;
|
}
|
|
.setting-desc {
|
color: var(--color-tertiary-label);
|
font-size: var(--font-size-xs);
|
}
|
|
.text-output-status {
|
align-self: flex-start;
|
display: inline-flex;
|
padding: 4px 10px;
|
border-radius: var(--radius-sm);
|
background: var(--color-bg-secondary);
|
color: var(--color-secondary-label);
|
font-size: var(--font-size-sm);
|
font-weight: 500;
|
}
|
|
.text-output-status.success {
|
color: var(--color-green);
|
}
|
|
.text-output-status.warn {
|
color: var(--color-orange);
|
}
|
|
.fallback-message {
|
color: var(--color-secondary-label);
|
font-size: var(--font-size-sm);
|
line-height: 1.4;
|
}
|
|
.fallback-actions {
|
display: flex;
|
flex-wrap: wrap;
|
gap: var(--spacing-sm);
|
}
|
|
.primary-btn,
|
.secondary-btn {
|
border: none;
|
border-radius: var(--radius-sm);
|
padding: 7px 12px;
|
font-size: var(--font-size-sm);
|
font-weight: 600;
|
cursor: pointer;
|
}
|
|
.primary-btn {
|
background: var(--color-blue);
|
color: white;
|
}
|
|
.secondary-btn {
|
background: var(--color-bg-secondary);
|
color: var(--color-label);
|
}
|
|
.primary-btn:disabled,
|
.secondary-btn:disabled {
|
cursor: not-allowed;
|
opacity: 0.5;
|
}
|
</style>
|