diff --git a/README.md b/README.md index 8169dfc..f25cfd4 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,13 @@ It is designed for practical daily work: IDEs, chats, notes, CRM fields, email, ## Latest Changes +### v0.3.6 + +- Fixed permissions onboarding recovery for existing installs so updates do not reopen the full permissions screen when required macOS access is already granted. +- Switched the app icon set from Lucide to Phosphor through a local icon facade for softer, more consistent product UI icons. +- Added a macOS Vite/esbuild runner workaround for development and release builds from external drives. +- Kept local STT failures separate from API-key authentication errors when a local model is selected. + ### v0.3.5 - Fixed local STT mode so managed localhost runtimes are called without a stale API bearer token and no longer show an API-key error for local runtime failures. diff --git a/bun.lock b/bun.lock index cfb80c0..9776baa 100644 --- a/bun.lock +++ b/bun.lock @@ -1,10 +1,10 @@ { "lockfileVersion": 1, - "configVersion": 0, "workspaces": { "": { "name": "talk-flow", "dependencies": { + "@phosphor-icons/react": "^2.1.10", "@tauri-apps/api": "^2", "@tauri-apps/plugin-autostart": "^2.5.1", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", @@ -15,7 +15,6 @@ "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-store": "^2.4.2", "@tauri-apps/plugin-updater": "^2.10.1", - "lucide-react": "^0.577.0", "react": "^19.1.0", "react-dom": "^19.1.0", }, @@ -135,6 +134,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@phosphor-icons/react": ["@phosphor-icons/react@2.1.10", "", { "peerDependencies": { "react": ">= 16.8", "react-dom": ">= 16.8" } }, "sha512-vt8Tvq8GLjheAZZYa+YG/pW7HDbov8El/MANW8pOAz4eGxrwhnbfrQZq0Cp4q8zBEu8NIhHdnr+r8thnfRSNYA=="], + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], @@ -361,8 +362,6 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.577.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A=="], - "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], diff --git a/docs/release/review-v0.3.6.md b/docs/release/review-v0.3.6.md new file mode 100644 index 0000000..274170e --- /dev/null +++ b/docs/release/review-v0.3.6.md @@ -0,0 +1,61 @@ +# Release Review v0.3.6 + +## Release + +- Version: 0.3.6 +- Release branch: release/v0.3.6 +- Target tag: v0.3.6 +- Reviewer: Codex +- Date: 2026-07-02 + +## Scope + +- Key changes included in this release: + - Recover completed permissions onboarding for existing installs when required macOS permissions are already granted. + - Replace Lucide icons with Phosphor icons through a local icon facade. + - Add a Vite runner that copies the macOS esbuild binary to the system temp directory before dev/build, avoiding `write EPIPE` failures when the repo lives on an external drive. + - Preserve the local-model API-key fix and widget notice alignment fix from the hotfix work. +- User-facing changes: + - Updates should no longer reopen the full permissions screen for users who already granted microphone and Accessibility access. + - Local STT mode should no longer surface API-key wording for local runtime failures. + - Settings and widget UI use the softer Phosphor icon set. + - `bun run tauri dev` works from `/Volumes/KINGSTON` without manual `ESBUILD_BINARY_PATH`. +- Risky areas: + - Permissions onboarding recovery relies on existing history plus current OS permission checks. + - Icon migration touches many UI imports, though through one facade. + - Local macOS updater signing requires `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`; the local environment did not provide it. + +## Checks run + +- `bun run check:release`: passed. +- `bun run build:release:macos`: app bundle and updater tarball built, then failed at updater signing because the local environment did not provide the private-key password. +- Native/GitHub Windows build: pending GitHub Actions tag build. +- Native/GitHub Linux build: pending GitHub Actions tag build. +- Additional manual checks: + - `bun run dev -- --host 127.0.0.1`: passed after the Vite/esbuild runner fix. + - `bunx tsc --noEmit`: passed during icon rollback. + - Verified `lucide-react`, `@tabler/icons-react`, and `@hugeicons/*` are absent from app dependencies and source. + +## Manual review + +- Hotkey flow: smoke test passed through `src/windows/widget/services/hotkeyFsm.test.js`. +- Onboarding permissions: reviewed startup gating in `SettingsApp`; system-audio permission is no longer required for the initial settings app gate, and existing installs with history can recover the completed flag. +- Widget position and notice behavior: reviewed notice icon facade usage and centered icon layout. +- Transcription quality and short-utterance handling: unchanged in this release. +- README refreshed: yes, `README.md` includes `v0.3.6` notes. + +## Findings + +- Blockers: + - None for GitHub Actions release, assuming repository signing secrets include the updater private key password. +- Non-blocking issues: + - Local `bun run build:release:macos` cannot complete updater signing without `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`. + - Production frontend bundle remains above the default Vite 500 kB warning threshold. +- Follow-ups after release: + - Consider documenting the local updater signing password requirement for macOS release builds. + - Consider code-splitting settings tabs or model assets to reduce the main frontend bundle size. + +## Decision + +- Ready for `main` merge: yes. +- Ready for tag publish: yes, with GitHub Actions signing secrets expected to complete updater signing. diff --git a/package.json b/package.json index bc6a672..54d55eb 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "talkis", "private": true, - "version": "0.3.5", + "version": "0.3.6", "license": "AGPL-3.0-or-later", "type": "module", "packageManager": "bun@1.2.13", @@ -9,8 +9,8 @@ "prepare:ffmpeg-sidecar": "bun scripts/prepare-ffmpeg-sidecar.mjs", "prepare:stt-sidecar": "bun scripts/prepare-stt-sidecar.mjs", "prepare:sidecars": "bun run prepare:ffmpeg-sidecar && bun run prepare:stt-sidecar", - "dev": "vite", - "build": "tsc && vite build", + "dev": "bun scripts/run-vite.mjs", + "build": "tsc && bun scripts/run-vite.mjs build", "check": "bun run prepare:sidecars && bunx tsc --noEmit && cargo check --manifest-path src-tauri/Cargo.toml", "test": "bun test", "smoke:hotkey": "bun test ./src/windows/widget/services/hotkeyFsm.test.js", @@ -21,12 +21,13 @@ "build:release:macos": "bun scripts/build-release.mjs macos", "build:release:windows": "bun scripts/build-release.mjs windows", "build:release:linux": "bun scripts/build-release.mjs linux", - "preview": "vite preview", + "preview": "bun scripts/run-vite.mjs preview", "tauri": "tauri", "logs": "tail -f ~/.talkis/talkis.log", "logs:clear": "rm -f ~/.talkis/talkis.log" }, "dependencies": { + "@phosphor-icons/react": "^2.1.10", "@tauri-apps/api": "^2", "@tauri-apps/plugin-autostart": "^2.5.1", "@tauri-apps/plugin-clipboard-manager": "^2.3.2", @@ -37,7 +38,6 @@ "@tauri-apps/plugin-process": "^2.3.1", "@tauri-apps/plugin-store": "^2.4.2", "@tauri-apps/plugin-updater": "^2.10.1", - "lucide-react": "^0.577.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/scripts/run-vite.mjs b/scripts/run-vite.mjs new file mode 100644 index 0000000..cd93f8c --- /dev/null +++ b/scripts/run-vite.mjs @@ -0,0 +1,56 @@ +import { spawn } from "node:child_process"; +import { existsSync, mkdirSync, copyFileSync, chmodSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { tmpdir } from "node:os"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +const rootDir = dirname(dirname(new URL(import.meta.url).pathname)); + +function resolvePackageFile(packageName, relativePath) { + return join(dirname(require.resolve(`${packageName}/package.json`)), relativePath); +} + +function prepareEsbuildBinary() { + if (process.platform !== "darwin") return undefined; + + const source = resolvePackageFile("esbuild", "bin/esbuild"); + if (!existsSync(source)) return undefined; + + const targetDir = join(tmpdir(), "talkis-esbuild"); + const target = join(targetDir, `esbuild-${process.platform}-${process.arch}`); + + mkdirSync(targetDir, { recursive: true }); + copyFileSync(source, target); + chmodSync(target, 0o755); + + return target; +} + +function resolveViteBin() { + const binName = process.platform === "win32" ? "vite.cmd" : "vite"; + return join(rootDir, "node_modules", ".bin", binName); +} + +const env = { ...process.env }; +const esbuildBinary = prepareEsbuildBinary(); + +if (esbuildBinary) { + env.ESBUILD_BINARY_PATH = esbuildBinary; +} + +const child = spawn(resolveViteBin(), process.argv.slice(2), { + cwd: rootDir, + env, + stdio: "inherit", + shell: process.platform === "win32", +}); + +child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + + process.exit(code ?? 1); +}); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d30cb48..a6262de 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4969,7 +4969,7 @@ dependencies = [ [[package]] name = "talkis" -version = "0.3.5" +version = "0.3.6" dependencies = [ "base64 0.22.1", "block2 0.6.2", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index a63cb4f..cf08ea9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "talkis" -version = "0.3.5" +version = "0.3.6" description = "Talkis - Voice to Text Desktop Widget" authors = ["trixter"] license = "AGPL-3.0-or-later" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e668a1d..a8d538b 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -2,7 +2,7 @@ "$schema": "https://schema.tauri.app/config/2", "productName": "Talkis", "mainBinaryName": "Talkis", - "version": "0.3.5", + "version": "0.3.6", "identifier": "com.trixter.talkis", "build": { "beforeDevCommand": "bun run prepare:sidecars && bun run dev", diff --git a/src/components/Dropdown.tsx b/src/components/Dropdown.tsx index 5c0ac35..f5e5bed 100644 --- a/src/components/Dropdown.tsx +++ b/src/components/Dropdown.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from "react"; -import { Check, ChevronDown } from "lucide-react"; +import { IconCheck, IconChevronDown } from "../lib/icons"; export interface DropdownOption { value: string; @@ -59,9 +59,9 @@ export function Dropdown({ {current?.label ?? value} - @@ -113,7 +113,7 @@ export function Dropdown({ onMouseLeave={(e) => (e.currentTarget.style.background = active ? "var(--dropdown-active)" : "transparent")} > {option.label} - {active && } + {active && } ); })} diff --git a/src/components/LocalLlmModels.tsx b/src/components/LocalLlmModels.tsx index 3ed93fb..560dbd9 100644 --- a/src/components/LocalLlmModels.tsx +++ b/src/components/LocalLlmModels.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; -import { AlertCircle, Check, Download, HardDrive, Loader2, MemoryStick, Trash2, X } from "lucide-react"; +import { IconAlertCircle, IconCheck, IconDownload, IconDeviceDesktopCog, IconLoader2, IconCpu, IconTrash, IconX } from "../lib/icons"; import { AppSettings } from "../lib/store"; import { useI18n } from "../lib/i18n"; @@ -225,7 +225,7 @@ export function LocalLlmModels({ border: "1px solid var(--border-subtle)", }} > - + {t("localLlm.required")} )} @@ -344,7 +344,7 @@ export function LocalLlmModels({ aria-label={t("localLlm.diskSize", { size: model.size_label })} style={{ display: "flex", alignItems: "center", gap: 6 }} > - + {model.size_label} @@ -354,7 +354,7 @@ export function LocalLlmModels({ aria-label={t("localLlm.ramRequired", { ram: model.min_ram_gb })} style={{ display: "flex", alignItems: "center", gap: 6 }} > - + {t("localLlm.ramShort", { ram: model.min_ram_gb })} @@ -442,7 +442,7 @@ export function LocalLlmModels({ color: "var(--text-hi)", }} > - + {t("localLlm.cancel")} ) : ( @@ -454,7 +454,7 @@ export function LocalLlmModels({ color: "var(--accent-contrast)", }} > - + {t("localLlm.download")} ) @@ -472,13 +472,13 @@ export function LocalLlmModels({ }} > {isBusy ? ( - ) : ( - + )} {t("localLlm.select")} @@ -492,7 +492,7 @@ export function LocalLlmModels({ color: "var(--text-hi)", }} > - + {t("localLlm.delete")} diff --git a/src/components/PermissionScreen.tsx b/src/components/PermissionScreen.tsx index 4ea93b6..4ae0b92 100644 --- a/src/components/PermissionScreen.tsx +++ b/src/components/PermissionScreen.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; import { openPath } from "@tauri-apps/plugin-opener"; -import { Mic, Keyboard, Check, AlertCircle, Volume2 } from "lucide-react"; +import { IconMicrophone, IconKeyboard, IconCheck, IconAlertCircle, IconVolume } from "../lib/icons"; import { PermissionStatus, checkAccessibilityPermission, @@ -66,7 +66,7 @@ function PermissionRow({ flexShrink: 0, }} > - {isGranted ? : icon} + {isGranted ? : icon}
@@ -452,7 +452,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) { border: "1px solid var(--danger-border)", }} > - } + icon={} title={t("permission.mic.title")} description={t("permission.mic.desc")} status={micStatus} @@ -493,7 +493,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) { {requiresSystemAudio && ( } + icon={} title={t("permission.systemAudio.title")} description={t("permission.systemAudio.desc")} status={systemAudioStatus} @@ -503,7 +503,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) { )} } + icon={} title={pastePermissionTitle} description={pastePermissionDescription} status={ @@ -540,7 +540,7 @@ export function PermissionScreen({ onComplete }: PermissionScreenProps) { border: "1px solid var(--border-subtle)", }} > - - + {open && ( diff --git a/src/components/SlideOverModal.tsx b/src/components/SlideOverModal.tsx index 14f6260..545944c 100644 --- a/src/components/SlideOverModal.tsx +++ b/src/components/SlideOverModal.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, type ReactNode } from "react"; -import { X } from "lucide-react"; +import { IconX } from "../lib/icons"; import { useI18n } from "../lib/i18n"; @@ -98,7 +98,7 @@ export function SlideOverModal({ aria-label={t("summary.close")} style={{ width: 32, minWidth: 32, minHeight: 32, padding: 0, justifyContent: "center", flexShrink: 0 }} > - +
diff --git a/src/components/SummaryModal.tsx b/src/components/SummaryModal.tsx index e8a4509..a1327ff 100644 --- a/src/components/SummaryModal.tsx +++ b/src/components/SummaryModal.tsx @@ -1,6 +1,6 @@ import { useEffect, useMemo, useState } from "react"; import { listen } from "@tauri-apps/api/event"; -import { Check, Clipboard, Pencil, Trash2 } from "lucide-react"; +import { IconCheck, IconClipboard, IconPencil, IconTrash } from "../lib/icons"; import { SETTINGS_UPDATED_EVENT } from "../lib/hotkeyEvents"; import { @@ -295,9 +295,9 @@ export function SummaryModal({ : , onSelect: () => void copy(item) }, - { key: "edit", label: t("summary.edit"), icon: , onSelect: () => startEdit(item) }, - { key: "delete", label: t("summary.delete"), icon: , danger: true, onSelect: () => void remove(item) }, + { key: "copy", label: copiedId === item.id ? t("summary.copied") : t("summary.copy"), icon: copiedId === item.id ? : , onSelect: () => void copy(item) }, + { key: "edit", label: t("summary.edit"), icon: , onSelect: () => startEdit(item) }, + { key: "delete", label: t("summary.delete"), icon: , danger: true, onSelect: () => void remove(item) }, ] as RowActionItem[]} /> diff --git a/src/components/UserPanel.tsx b/src/components/UserPanel.tsx index f891c56..8cf6bbd 100644 --- a/src/components/UserPanel.tsx +++ b/src/components/UserPanel.tsx @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { openUrl } from "@tauri-apps/plugin-opener"; import { listen } from "@tauri-apps/api/event"; import { onOpenUrl } from "@tauri-apps/plugin-deep-link"; -import { LogOut, User, Crown } from "lucide-react"; +import { IconLogout, IconUser, IconCrown } from "../lib/icons"; import { beginCloudAuthFlow, @@ -221,7 +221,7 @@ export function UserPanel() { cancelLocalAuthPolling(); setProfile(null); await cloudLogout(); - logInfo("USER_PANEL", "User logged out"); + logInfo("USER_PANEL", "IconUser logged out"); }; if (loading) { @@ -247,7 +247,7 @@ export function UserPanel() {
@@ -274,7 +274,7 @@ function ProfileRow({ profile, onLogout }: { profile: CloudProfile; onLogout: () style={{ width: "100%", height: "100%", borderRadius: "50%", objectFit: "cover" }} /> ) : ( - + )}
@@ -284,7 +284,7 @@ function ProfileRow({ profile, onLogout }: { profile: CloudProfile; onLogout: ()
{profile.user.email}
); @@ -295,7 +295,7 @@ function SubscriptionCTA({ onActivate }: { onActivate: () => void }) { return (
- + {t("userPanel.cta.title")} diff --git a/src/lib/icons.ts b/src/lib/icons.ts new file mode 100644 index 0000000..1d37649 --- /dev/null +++ b/src/lib/icons.ts @@ -0,0 +1,180 @@ +import type { + ComponentPropsWithoutRef, + ForwardRefExoticComponent, + RefAttributes, +} from "react"; + +import { SlidersHorizontalIcon as RawIconAdjustmentsHorizontal } from "@phosphor-icons/react/SlidersHorizontal"; +import { WarningCircleIcon as RawIconAlertCircle } from "@phosphor-icons/react/WarningCircle"; +import { WarningIcon as RawIconAlertTriangle } from "@phosphor-icons/react/Warning"; +import { LightningIcon as RawIconBolt } from "@phosphor-icons/react/Lightning"; +import { BriefcaseIcon as RawIconBriefcase } from "@phosphor-icons/react/Briefcase"; +import { BroadcastIcon as RawIconBroadcast } from "@phosphor-icons/react/Broadcast"; +import { CheckIcon as RawIconCheck } from "@phosphor-icons/react/Check"; +import { CaretDownIcon as RawIconChevronDown } from "@phosphor-icons/react/CaretDown"; +import { ClipboardIcon as RawIconClipboard } from "@phosphor-icons/react/Clipboard"; +import { CloudIcon as RawIconCloud } from "@phosphor-icons/react/Cloud"; +import { CodeIcon as RawIconCode } from "@phosphor-icons/react/Code"; +import { CopyIcon as RawIconCopy } from "@phosphor-icons/react/Copy"; +import { CpuIcon as RawIconCpu } from "@phosphor-icons/react/Cpu"; +import { CrownIcon as RawIconCrown } from "@phosphor-icons/react/Crown"; +import { DesktopIcon as RawIconDeviceDesktop } from "@phosphor-icons/react/Desktop"; +import { GearIcon as RawIconDeviceDesktopCog } from "@phosphor-icons/react/Gear"; +import { DotsThreeIcon as RawIconDots } from "@phosphor-icons/react/DotsThree"; +import { DownloadSimpleIcon as RawIconDownload } from "@phosphor-icons/react/DownloadSimple"; +import { ArrowSquareOutIcon as RawIconExternalLink } from "@phosphor-icons/react/ArrowSquareOut"; +import { FileAudioIcon as RawIconFileMusic } from "@phosphor-icons/react/FileAudio"; +import { GaugeIcon as RawIconGauge } from "@phosphor-icons/react/Gauge"; +import { HeadphonesIcon as RawIconHeadphones } from "@phosphor-icons/react/Headphones"; +import { QuestionIcon as RawIconHelpCircle } from "@phosphor-icons/react/Question"; +import { HouseIcon as RawIconHome } from "@phosphor-icons/react/House"; +import { InfoIcon as RawIconInfoCircle } from "@phosphor-icons/react/Info"; +import { KeyboardIcon as RawIconKeyboard } from "@phosphor-icons/react/Keyboard"; +import { TranslateIcon as RawIconLanguage } from "@phosphor-icons/react/Translate"; +import { ListChecksIcon as RawIconListCheck } from "@phosphor-icons/react/ListChecks"; +import { SpinnerGapIcon as RawIconLoader2 } from "@phosphor-icons/react/SpinnerGap"; +import { SignOutIcon as RawIconLogout } from "@phosphor-icons/react/SignOut"; +import { EnvelopeIcon as RawIconMail } from "@phosphor-icons/react/Envelope"; +import { ChatCircleIcon as RawIconMessage } from "@phosphor-icons/react/ChatCircle"; +import { MicrophoneIcon as RawIconMicrophone } from "@phosphor-icons/react/Microphone"; +import { MoonIcon as RawIconMoon } from "@phosphor-icons/react/Moon"; +import { PencilSimpleIcon as RawIconPencil } from "@phosphor-icons/react/PencilSimple"; +import { PhoneCallIcon as RawIconPhoneCall } from "@phosphor-icons/react/PhoneCall"; +import { PlayIcon as RawIconPlayerPlay } from "@phosphor-icons/react/Play"; +import { PlusIcon as RawIconPlus } from "@phosphor-icons/react/Plus"; +import { ArrowClockwiseIcon as RawIconRefresh } from "@phosphor-icons/react/ArrowClockwise"; +import { ArrowsClockwiseIcon as RawIconRotate2 } from "@phosphor-icons/react/ArrowsClockwise"; +import { MagnifyingGlassIcon as RawIconSearch } from "@phosphor-icons/react/MagnifyingGlass"; +import { HardDrivesIcon as RawIconServer } from "@phosphor-icons/react/HardDrives"; +import { SparkleIcon as RawIconSparkles } from "@phosphor-icons/react/Sparkle"; +import { SpeakerHighIcon as RawIconSpeakerphone } from "@phosphor-icons/react/SpeakerHigh"; +import { SquareIcon as RawIconSquare } from "@phosphor-icons/react/Square"; +import { SunIcon as RawIconSun } from "@phosphor-icons/react/Sun"; +import { TargetIcon as RawIconTargetArrow } from "@phosphor-icons/react/Target"; +import { TrashIcon as RawIconTrash } from "@phosphor-icons/react/Trash"; +import { TextTIcon as RawIconTypography } from "@phosphor-icons/react/TextT"; +import { UserIcon as RawIconUser } from "@phosphor-icons/react/User"; +import { SpeakerHighIcon as RawIconVolume } from "@phosphor-icons/react/SpeakerHigh"; +import { XIcon as RawIconX } from "@phosphor-icons/react/X"; + +export interface IconProps + extends Omit, "stroke">, + RefAttributes { + alt?: string; + color?: string; + mirrored?: boolean; + size?: number | string; + stroke?: number | string; + title?: string; + weight?: "thin" | "light" | "regular" | "bold" | "fill" | "duotone"; +} + +export type Icon = ForwardRefExoticComponent; + +const IconAdjustmentsHorizontal = RawIconAdjustmentsHorizontal as Icon; +const IconAlertCircle = RawIconAlertCircle as Icon; +const IconAlertTriangle = RawIconAlertTriangle as Icon; +const IconBolt = RawIconBolt as Icon; +const IconBriefcase = RawIconBriefcase as Icon; +const IconBroadcast = RawIconBroadcast as Icon; +const IconCheck = RawIconCheck as Icon; +const IconChevronDown = RawIconChevronDown as Icon; +const IconClipboard = RawIconClipboard as Icon; +const IconCloud = RawIconCloud as Icon; +const IconCode = RawIconCode as Icon; +const IconCopy = RawIconCopy as Icon; +const IconCpu = RawIconCpu as Icon; +const IconCrown = RawIconCrown as Icon; +const IconDeviceDesktop = RawIconDeviceDesktop as Icon; +const IconDeviceDesktopCog = RawIconDeviceDesktopCog as Icon; +const IconDots = RawIconDots as Icon; +const IconDownload = RawIconDownload as Icon; +const IconExternalLink = RawIconExternalLink as Icon; +const IconFileMusic = RawIconFileMusic as Icon; +const IconGauge = RawIconGauge as Icon; +const IconHeadphones = RawIconHeadphones as Icon; +const IconHelpCircle = RawIconHelpCircle as Icon; +const IconHome = RawIconHome as Icon; +const IconInfoCircle = RawIconInfoCircle as Icon; +const IconKeyboard = RawIconKeyboard as Icon; +const IconLanguage = RawIconLanguage as Icon; +const IconListCheck = RawIconListCheck as Icon; +const IconLoader2 = RawIconLoader2 as Icon; +const IconLogout = RawIconLogout as Icon; +const IconMail = RawIconMail as Icon; +const IconMessage = RawIconMessage as Icon; +const IconMicrophone = RawIconMicrophone as Icon; +const IconMoon = RawIconMoon as Icon; +const IconPencil = RawIconPencil as Icon; +const IconPhoneCall = RawIconPhoneCall as Icon; +const IconPlayerPlay = RawIconPlayerPlay as Icon; +const IconPlus = RawIconPlus as Icon; +const IconRefresh = RawIconRefresh as Icon; +const IconRotate2 = RawIconRotate2 as Icon; +const IconSearch = RawIconSearch as Icon; +const IconServer = RawIconServer as Icon; +const IconSparkles = RawIconSparkles as Icon; +const IconSpeakerphone = RawIconSpeakerphone as Icon; +const IconSquare = RawIconSquare as Icon; +const IconSun = RawIconSun as Icon; +const IconTargetArrow = RawIconTargetArrow as Icon; +const IconTrash = RawIconTrash as Icon; +const IconTypography = RawIconTypography as Icon; +const IconUser = RawIconUser as Icon; +const IconVolume = RawIconVolume as Icon; +const IconX = RawIconX as Icon; + +export { + IconAdjustmentsHorizontal, + IconAlertCircle, + IconAlertTriangle, + IconBolt, + IconBriefcase, + IconBroadcast, + IconCheck, + IconChevronDown, + IconClipboard, + IconCloud, + IconCode, + IconCopy, + IconCpu, + IconCrown, + IconDeviceDesktop, + IconDeviceDesktopCog, + IconDots, + IconDownload, + IconExternalLink, + IconFileMusic, + IconGauge, + IconHeadphones, + IconHelpCircle, + IconHome, + IconInfoCircle, + IconKeyboard, + IconLanguage, + IconListCheck, + IconLoader2, + IconLogout, + IconMail, + IconMessage, + IconMicrophone, + IconMoon, + IconPencil, + IconPhoneCall, + IconPlayerPlay, + IconPlus, + IconRefresh, + IconRotate2, + IconSearch, + IconServer, + IconSparkles, + IconSpeakerphone, + IconSquare, + IconSun, + IconTargetArrow, + IconTrash, + IconTypography, + IconUser, + IconVolume, + IconX, +}; diff --git a/src/windows/settings/SettingsApp.tsx b/src/windows/settings/SettingsApp.tsx index 590ac9a..9dbe480 100644 --- a/src/windows/settings/SettingsApp.tsx +++ b/src/windows/settings/SettingsApp.tsx @@ -4,17 +4,17 @@ import { getVersion } from "@tauri-apps/api/app"; import { listen } from "@tauri-apps/api/event"; import { openUrl } from "@tauri-apps/plugin-opener"; import { - Download, - ExternalLink, - FileAudio, - Home, - Cpu, - Languages, - Loader2, - Sparkles, - Sliders, - LucideIcon, -} from "lucide-react"; + IconDownload, + IconExternalLink, + IconFileMusic, + IconHome, + IconCpu, + IconLanguage, + IconLoader2, + IconSparkles, + IconAdjustmentsHorizontal, + Icon, +} from "../../lib/icons"; import { TitleBar } from "../../components/TitleBar"; import { MainTab } from "./tabs/MainTab"; import { FileTranscriptionTab } from "./tabs/FileTranscriptionTab"; @@ -72,31 +72,31 @@ function resolveInitialTab(): Tab { return "main"; } -const TABS: { id: Tab; labelKey: MsgKey; icon: LucideIcon; note: string }[] = [ - { id: "main", labelKey: "settingsApp.tab.main", icon: Home, note: "История записей" }, +const TABS: { id: Tab; labelKey: MsgKey; icon: Icon; note: string }[] = [ + { id: "main", labelKey: "settingsApp.tab.main", icon: IconHome, note: "История записей" }, { id: "file", labelKey: "settingsApp.tab.file", - icon: FileAudio, + icon: IconFileMusic, note: "Транскрибация", }, { id: "interpreter", labelKey: "settingsApp.tab.interpreter", - icon: Languages, + icon: IconLanguage, note: "Realtime Interpreter beta", }, { id: "model", labelKey: "settingsApp.tab.model", - icon: Cpu, + icon: IconCpu, note: "Ключи и подключение модели", }, - { id: "style", labelKey: "settingsApp.tab.style", icon: Sparkles, note: "Стиль обработки и Промпты для саммари" }, + { id: "style", labelKey: "settingsApp.tab.style", icon: IconSparkles, note: "Стиль обработки и Промпты для саммари" }, { id: "settings", labelKey: "settingsApp.tab.settings", - icon: Sliders, + icon: IconAdjustmentsHorizontal, note: "Язык, микрофон и горячая клавиша", }, ]; @@ -119,7 +119,7 @@ function TabButton({ className={`nav-item ${isActive ? "active" : ""}`} style={{ width: "100%", textAlign: "left", font: "inherit" }} > - + {t(tab.labelKey)} ); @@ -267,16 +267,16 @@ function AppUpdateFooter(): ReactElement | null { }} > {installing ? ( - ) : ( - + )} {installing @@ -329,9 +329,9 @@ function AppUpdateFooter(): ReactElement | null { v{version} -
@@ -377,13 +377,21 @@ export function SettingsApp() { useEffect(() => { Promise.all([getPermissionsPassed(), checkAllPermissions(), getHistory()]) - .then(([passed, permissions, history]) => { - const hasRequiredPermissions = + .then(async ([passed, permissions, history]) => { + const hasRequiredStartupPermissions = permissions.microphone !== "denied" && - permissions.accessibility === "granted" && - (!isMacPlatform() || permissions.systemAudio === "granted"); + (!isMacPlatform() || permissions.accessibility === "granted"); + const shouldRecoverExistingInstall = + !passed && history.length > 0 && hasRequiredStartupPermissions; + + if (shouldRecoverExistingInstall) { + await setPermissionsPassed(true); + } + setInitialHistory(history); - setShowPermissions(!(passed && hasRequiredPermissions)); + setShowPermissions( + !((passed || shouldRecoverExistingInstall) && hasRequiredStartupPermissions), + ); setLoadError(null); }) .catch((error) => { diff --git a/src/windows/settings/tabs/FileTranscriptionTab.tsx b/src/windows/settings/tabs/FileTranscriptionTab.tsx index a67bcac..6e00e99 100644 --- a/src/windows/settings/tabs/FileTranscriptionTab.tsx +++ b/src/windows/settings/tabs/FileTranscriptionTab.tsx @@ -5,14 +5,14 @@ import { emit, listen } from "@tauri-apps/api/event"; import { getCurrentWebview } from "@tauri-apps/api/webview"; import { open } from "@tauri-apps/plugin-dialog"; import { - AlertCircle, - Check, - Clipboard, - FileAudio, - ListChecks, - Loader2, - X, -} from "lucide-react"; + IconAlertCircle, + IconCheck, + IconClipboard, + IconFileMusic, + IconListCheck, + IconLoader2, + IconX, +} from "../../../lib/icons"; import { addHistoryEntry, @@ -585,7 +585,7 @@ export function FileTranscriptionTab({ if (speakerMode && !settings.useOwnKey && !cloudSpeakerReady) { setSpeakerDiarization(false); await saveSettings({ fileSpeakerDiarization: false }); - throw new Error("Cloud speaker diarization unavailable"); + throw new Error("IconCloud speaker diarization unavailable"); } if (speakerMode && settings.useOwnKey && !isSpeakerSetupReady(settings)) { @@ -945,7 +945,7 @@ export function FileTranscriptionTab({ setSpeakerSetupIntent(null); setError( toFileTranscriptionErrorMessage( - new Error("Cloud speaker diarization unavailable"), + new Error("IconCloud speaker diarization unavailable"), ), ); setSpeakerDiarization(false); @@ -1092,13 +1092,13 @@ export function FileTranscriptionTab({ }} > {isProcessing ? ( - ) : ( - + )}
@@ -1166,9 +1166,9 @@ export function FileTranscriptionTab({ textAlign: "left", }} > - {error} @@ -1271,9 +1271,9 @@ export function FileTranscriptionTab({ style={{ minHeight: 32, padding: "0 12px" }} > {copied ? ( - + ) : ( - + )} {copied ? t("fileTab.result.copied") : t("fileTab.result.copy")} @@ -1288,7 +1288,7 @@ export function FileTranscriptionTab({ title={summaryAvailable ? undefined : t("summary.unavailable.tooltip")} style={{ minHeight: 32, padding: "0 12px", gap: 6, opacity: summaryAvailable ? 1 : 0.5, cursor: summaryAvailable ? "pointer" : "not-allowed" }} > - + {t("summary.button")} )} @@ -1305,7 +1305,7 @@ export function FileTranscriptionTab({ style={{ width: 32, minWidth: 32, minHeight: 32, padding: 0 }} title={t("fileTab.result.clear")} > - + )} @@ -1322,7 +1322,7 @@ export function FileTranscriptionTab({ color: "var(--text-mid)", }} > - + {t("summary.unavailable.note")} )} @@ -1573,9 +1573,9 @@ export function FileTranscriptionTab({ }} > {localWhisperDownloaded ? ( - + ) : ( - + )} {localWhisperDownloaded @@ -1594,9 +1594,9 @@ export function FileTranscriptionTab({ }} > {diarizationInstalled ? ( - + ) : ( - + )} {diarizationInstalled @@ -1675,10 +1675,10 @@ export function FileTranscriptionTab({ }} > {speakerSetupInstalling ? ( - ) : null} {speakerSetupActionLabel} diff --git a/src/windows/settings/tabs/MainTab.tsx b/src/windows/settings/tabs/MainTab.tsx index f6dcd05..1741dcf 100644 --- a/src/windows/settings/tabs/MainTab.tsx +++ b/src/windows/settings/tabs/MainTab.tsx @@ -19,17 +19,17 @@ import { type SpeakerTranscriptSegment, } from "../../../lib/store"; import { - AlertCircle, - Check, - Copy, - HelpCircle, - Loader2, - ListChecks, - Pencil, - RotateCcw, - Square, - Trash2, -} from "lucide-react"; + IconAlertCircle, + IconCheck, + IconCopy, + IconHelpCircle, + IconLoader2, + IconListCheck, + IconPencil, + IconRotate2, + IconSquare, + IconTrash, +} from "../../../lib/icons"; import { HISTORY_CLEARED_EVENT, HISTORY_DELETED_EVENT, @@ -737,7 +737,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) { color: hintHelpOpen ? "var(--text-hi)" : "var(--text-low)", }} > - + )} @@ -854,7 +854,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) { : t("mainTab.clearAllTitle") } > - {" "} + {" "} {isClearArmed ? t("mainTab.confirm") : t("mainTab.clear")} )} @@ -1087,10 +1087,10 @@ export function MainTab({ initialHistory = [] }: MainTabProps) { width: "fit-content", }} > - {t("mainTab.processing")} @@ -1113,9 +1113,9 @@ export function MainTab({ initialHistory = [] }: MainTabProps) { width: "fit-content", }} > - {item.status === "interrupted" @@ -1180,9 +1180,9 @@ export function MainTab({ initialHistory = [] }: MainTabProps) { }} title={t("mainTab.stopProcessing")} > - @@ -1210,13 +1210,13 @@ export function MainTab({ initialHistory = [] }: MainTabProps) { title={t("mainTab.retryProcess")} > {retryingId === item.id ? ( - ) : ( - + )} ) : ( @@ -1228,7 +1228,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) { key: "edit", label: t("rowMenu.edit"), icon: ( - + ), onSelect: () => editEntry(item), }, @@ -1242,12 +1242,12 @@ export function MainTab({ initialHistory = [] }: MainTabProps) { icon: copied === item.id || retrySucceededId === item.id ? ( - ) : ( - + ), onSelect: () => copyText(item.id, item.cleaned), @@ -1256,9 +1256,9 @@ export function MainTab({ initialHistory = [] }: MainTabProps) { key: "summarize", label: t("rowMenu.summarize"), icon: ( - ), disabled: !summaryAvailable, @@ -1283,7 +1283,7 @@ export function MainTab({ initialHistory = [] }: MainTabProps) { }} title={t("mainTab.delete")} > - + )} diff --git a/src/windows/settings/tabs/RealtimeInterpreterTab.tsx b/src/windows/settings/tabs/RealtimeInterpreterTab.tsx index 242481c..5c3e7d7 100644 --- a/src/windows/settings/tabs/RealtimeInterpreterTab.tsx +++ b/src/windows/settings/tabs/RealtimeInterpreterTab.tsx @@ -2,17 +2,17 @@ import { useEffect, useMemo, useState } from "react"; import type { CSSProperties, ReactElement } from "react"; import { emit, listen } from "@tauri-apps/api/event"; import { - AlertTriangle, - Headphones, - Languages, - Mic, - Play, - Radio, - RefreshCcw, - Speaker, - Square, - Volume2, -} from "lucide-react"; + IconAlertTriangle, + IconHeadphones, + IconLanguage, + IconMicrophone, + IconPlayerPlay, + IconBroadcast, + IconRefresh, + IconSpeakerphone, + IconSquare, + IconVolume, +} from "../../../lib/icons"; import { addHistoryEntry, @@ -691,7 +691,7 @@ export function RealtimeInterpreterTab(): ReactElement { className="label" style={{ display: "inline-flex", alignItems: "center", gap: 8 }} > - + Beta
- +
@@ -733,7 +733,7 @@ export function RealtimeInterpreterTab(): ReactElement { fontWeight: 700, }} > - + {status?.message || t("realtime.statusMessage.off")} @@ -831,7 +831,7 @@ export function RealtimeInterpreterTab(): ReactElement { background: "var(--control-muted)", }} > - Cloud proxy + IconCloud proxy - + {t("realtime.headphonesConfirm")} @@ -884,7 +884,7 @@ export function RealtimeInterpreterTab(): ReactElement { }} >
- +
@@ -903,7 +903,7 @@ export function RealtimeInterpreterTab(): ReactElement { }} style={{ minHeight: 36, borderRadius: 10, padding: "0 12px" }} > - + {t("realtime.button.refresh")} {status?.active ? ( @@ -934,7 +934,7 @@ export function RealtimeInterpreterTab(): ReactElement { }} style={{ minHeight: 36, borderRadius: 10, padding: "0 12px" }} > - + {t("realtime.button.stop")} ) : ( @@ -948,7 +948,7 @@ export function RealtimeInterpreterTab(): ReactElement { }} style={{ minHeight: 36, borderRadius: 10, padding: "0 12px" }} > - + {t("realtime.button.start")} )} @@ -973,9 +973,9 @@ export function RealtimeInterpreterTab(): ReactElement { lineHeight: 1.5, }} > - {error} @@ -1026,7 +1026,7 @@ export function RealtimeInterpreterTab(): ReactElement { >
- +
{t("realtime.panel.youToCall")}
- +
{t("realtime.panel.callToYou")}
= [ - { id: "system", Icon: Monitor }, - { id: "light", Icon: Sun }, - { id: "dark", Icon: Moon }, +const THEME_OPTIONS: Array<{ id: AppSettings["theme"]; Icon: Icon }> = [ + { id: "system", Icon: IconDeviceDesktop }, + { id: "light", Icon: IconSun }, + { id: "dark", Icon: IconMoon }, ]; export function SettingsTab() { @@ -340,7 +340,7 @@ export function SettingsTab() { setMicStatus("ready"); setMicMessage(t("settingsGeneralExtra.mic.inUse", { label: activeLabel })); } catch (err) { - void logError("SETTINGS", `Mic enumeration error: ${err instanceof Error ? err.message : String(err)}`); + void logError("SETTINGS", `IconMicrophone enumeration error: ${err instanceof Error ? err.message : String(err)}`); setMicStatus("empty"); setMicMessage(t("settingsGeneralExtra.mic.enumFailed")); } @@ -734,7 +734,7 @@ export function SettingsTab() { gap: 4, }} > - + {t(`settings.theme.${id}`)} ); @@ -754,12 +754,12 @@ export function SettingsTab() { {currentLang ? `${currentLang.native} (${currentLang.name})` : settings.language} - + {langOpen && (
- + setLangSearch(e.target.value)} placeholder={t("settings.recognitionLang.searchPlaceholder")} style={{ border: "none", outline: "none", background: "transparent", fontSize: 12, color: "var(--text-hi)", flex: 1 }} />
@@ -789,7 +789,7 @@ export function SettingsTab() { {lang.code} {lang.native} {lang.name} - {settings.language === lang.code && } + {settings.language === lang.code && } ))}
@@ -817,7 +817,7 @@ export function SettingsTab() { {microphones.length === 0 ? t("settings.mic.systemDefault") : visibleMicrophoneLabel} - + {micOpen && microphones.length > 0 && ( @@ -830,7 +830,7 @@ export function SettingsTab() { onMouseLeave={(e) => e.currentTarget.style.background = settings.micId === "" ? "var(--dropdown-active)" : "transparent"} > {t("settings.mic.systemDefault")} - {settings.micId === "" && } + {settings.micId === "" && } {microphones.map((m, i) => ( ))}
@@ -1103,7 +1103,7 @@ export function SettingsTab() { className="btn" style={{ minHeight: CONTROL_HEIGHT, width: "100%", justifySelf: "end", justifyContent: "center", gap: 8, padding: "0 10px", borderRadius: CONTROL_RADIUS, fontSize: CONTROL_FONT_SIZE }} > - + {t("settings.support.button")}
diff --git a/src/windows/settings/tabs/SettingsTabs.tsx b/src/windows/settings/tabs/SettingsTabs.tsx index 9b2e128..34b0bbd 100644 --- a/src/windows/settings/tabs/SettingsTabs.tsx +++ b/src/windows/settings/tabs/SettingsTabs.tsx @@ -5,29 +5,29 @@ import { emit } from "@tauri-apps/api/event"; import { onOpenUrl } from "@tauri-apps/plugin-deep-link"; import { openUrl } from "@tauri-apps/plugin-opener"; import { - Briefcase, - Check, - Cloud, - Code, - Crown, - Download, - Gauge, - HardDrive, - LogOut, - LucideIcon, - MessageSquare, - Mic, - Pencil, - Plus, - Server, - Sparkles, - Target, - Trash2, - Type, - User, - X, - Zap, -} from "lucide-react"; + IconBriefcase, + IconCheck, + IconCloud, + IconCode, + IconCrown, + IconDownload, + IconGauge, + IconDeviceDesktopCog, + IconLogout, + Icon, + IconMessage, + IconMicrophone, + IconPencil, + IconPlus, + IconServer, + IconSparkles, + IconTargetArrow, + IconTrash, + IconTypography, + IconUser, + IconX, + IconBolt, +} from "../../../lib/icons"; import { AppSettings, @@ -605,7 +605,7 @@ function OptionCard({ active = false, icon, title, description, badge, onClick, {active && (
- +
)}
@@ -645,7 +645,7 @@ function CloudSubscriptionAccountCard({ style={{ width: "100%", height: "100%", borderRadius: "50%", objectFit: "cover" }} /> ) : ( - + )}
@@ -692,7 +692,7 @@ function CloudSubscriptionAccountCard({ }} title={t("models.account.logout")} > - + @@ -706,7 +706,7 @@ function SubscriptionPromoContent({ onActivate }: { onActivate: () => void }) { return ( <>
- +
{t("models.guest.title")} {t("models.cta.freeTrial")} @@ -733,7 +733,7 @@ function SubscriptionGuestCard({ onActivate }: { onActivate: () => void }) { } /** - * Three-state subscription block, shared by the Models → Cloud section and the + * Three-state subscription block, shared by the Models → IconCloud section and the * dedicated "Подписка Talkis" tab: active-subscription banner, signed-in account * card (activate / log out), or guest promo (sign in + start the free trial). */ @@ -753,7 +753,7 @@ function SubscriptionCards({
- +
{t("models.subscription.active")}
@@ -1032,7 +1032,7 @@ function PromptLibrary({ } + icon={} title={preset.name} description={} onClick={() => update({ defaultSummaryPromptId: preset.id })} @@ -1067,7 +1067,7 @@ function PromptLibrary({ fontFamily: "var(--font-main)", }} > - + {t("models.prompt.create")} @@ -1090,7 +1090,7 @@ function PromptLibrary({ fontFamily: "var(--font-main)", }} > - + {t("models.common.edit")} @@ -1153,7 +1153,7 @@ function TextModelCard({ style={{ width: "100%", border: "none", background: "transparent", padding: "12px 14px", display: "flex", alignItems: "center", gap: 12, cursor: "pointer", textAlign: "left", fontFamily: "var(--font-main)" }} >
- +
@@ -1356,7 +1356,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { void syncSettings().catch(() => {}); }, [syncSettings]); - // Cloud profile — always fetch (regardless of tab) so hooks are stable + // IconCloud profile — always fetch (regardless of tab) so hooks are stable useEffect(() => { if (getCachedCloudProfile() === undefined) { loadCloudProfile().catch(() => {}); @@ -1602,7 +1602,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { }); }; - // Subscription activation / logout — shared by the Models → Cloud section and + // Subscription activation / logout — shared by the Models → IconCloud section and // the dedicated "Подписка Talkis" tab so both surfaces drive the same flow. const handleActivateSubscription = async () => { try { @@ -1657,22 +1657,22 @@ export function SettingsTabs({ type }: SettingsTabsProps) { const modeOptions: Array<{ id: ModelMode; label: string; - Icon: LucideIcon; + Icon: Icon; }> = [ { id: "cloud", label: t("models.mode.cloud"), - Icon: Cloud, + Icon: IconCloud, }, { id: "api", label: "API", - Icon: Code, + Icon: IconCode, }, { id: "local", label: t("models.mode.local"), - Icon: Server, + Icon: IconServer, }, ]; @@ -2054,7 +2054,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { return (
- {isActive && } + {isActive && } {isActive ? t("models.modeCommit.active") : t("models.modeCommit.label", { mode: modeLabel })}
{!isActive && ( @@ -2252,9 +2252,9 @@ export function SettingsTabs({ type }: SettingsTabsProps) { const renderLocalModelStats = (model: LocalModelOption) => { const speedValueLabel = translateSpeedValue(model.speed); const accuracyValueLabel = translateAccuracyValue(model.accuracy); - const stats: { key: string; title: string; label: string; Icon: LucideIcon; level: number }[] = [ - { key: "speed", title: t("models.stat.speedTitle", { value: speedValueLabel }), label: t("models.stat.speed"), Icon: Gauge, level: getLocalModelLevel("speed", model.speed) }, - { key: "accuracy", title: t("models.stat.accuracyTitle", { value: accuracyValueLabel }), label: t("models.stat.accuracy"), Icon: Target, level: getLocalModelLevel("accuracy", model.accuracy) }, + const stats: { key: string; title: string; label: string; Icon: Icon; level: number }[] = [ + { key: "speed", title: t("models.stat.speedTitle", { value: speedValueLabel }), label: t("models.stat.speed"), Icon: IconGauge, level: getLocalModelLevel("speed", model.speed) }, + { key: "accuracy", title: t("models.stat.accuracyTitle", { value: accuracyValueLabel }), label: t("models.stat.accuracy"), Icon: IconTargetArrow, level: getLocalModelLevel("accuracy", model.accuracy) }, ]; const storageLabel = getLocalModelStorageLabel(model); @@ -2265,7 +2265,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { aria-label={t("models.stat.downloadSizeTitle", { value: storageLabel })} style={{ display: "flex", alignItems: "center", gap: 6 }} > - + {storageLabel} @@ -2278,7 +2278,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { aria-label={title} style={{ display: "flex", alignItems: "center", gap: 6 }} > - + {label} @@ -2356,7 +2356,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { } catch (err) { const message = err instanceof Error ? err.message : String(err); if (message.includes("отменена")) { - // User cancelled — reset to the not-downloaded state silently. + // IconUser cancelled — reset to the not-downloaded state silently. setLocalModelActionStates((prev) => { const next = { ...prev }; delete next[model.id]; @@ -2474,7 +2474,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { gap: 7, }} > - + {label} ); @@ -2504,7 +2504,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { {(isCloudSelected || hasActiveSubscription) && (
- + {isCloudSelected ? t("models.connection.usedForRecognition") : t("models.cloud.proReady")}
@@ -2521,7 +2521,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { alignItems: "center", gap: 8, }}> - + {t("models.common.selected")}
) : hasActiveSubscription ? ( @@ -2543,7 +2543,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { gap: 8, }} > - + {t("models.common.select")} ) : null} @@ -2558,8 +2558,8 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
{([ - { id: "transcription", label: t("models.local.tabTranscription"), Icon: Mic }, - { id: "text", label: t("models.local.tabText"), Icon: MessageSquare }, + { id: "transcription", label: t("models.local.tabTranscription"), Icon: IconMicrophone }, + { id: "text", label: t("models.local.tabText"), Icon: IconMessage }, ] as const).map(({ id, label, Icon }) => { const active = apiModelKind === id; @@ -2585,7 +2585,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { gap: 7, }} > - + {label} {id === "text" && !apiTextModelConfigured && (
- {adapterStatus.status === "success" && } + {adapterStatus.status === "success" && } {adapterStatus.connectionLabel}
{isAdapterSelected ? ( @@ -2760,7 +2760,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { alignItems: "center", gap: 8, }}> - + {t("models.common.selected")}
) : ( @@ -2784,7 +2784,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { gap: 8, }} > - + {t("models.common.select")} )} @@ -2814,7 +2814,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { ) : ( <> - + {adapter.testable ? t("models.test.testAndSave") : t("models.test.saveButton")} )} @@ -2839,8 +2839,8 @@ export function SettingsTabs({ type }: SettingsTabsProps) {
{([ - { id: "transcription", label: t("models.local.tabTranscription"), Icon: Mic }, - { id: "text", label: t("models.local.tabText"), Icon: MessageSquare }, + { id: "transcription", label: t("models.local.tabTranscription"), Icon: IconMicrophone }, + { id: "text", label: t("models.local.tabText"), Icon: IconMessage }, ] as const).map(({ id, label, Icon }) => { const active = localModelKind === id; @@ -2866,7 +2866,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { gap: 7, }} > - + {label} {id === "text" && !localTextModelSelected && ( {modelStatus.connectionLabel && (
- {(modelStatus.status === "installed" || modelStatus.status === "selected") && } + {(modelStatus.status === "installed" || modelStatus.status === "selected") && } {modelStatus.connectionLabel}
)} @@ -3029,7 +3029,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { gap: 8, }} > - + {t("models.common.select")} )} @@ -3062,12 +3062,12 @@ export function SettingsTabs({ type }: SettingsTabsProps) { > {isDownloaded ? ( <> - + {t("models.common.delete")} ) : ( <> - + {isRuntimeReady ? t("models.common.download") : t("models.common.unavailable")} )} @@ -3092,7 +3092,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { gap: 8, }} > - + {t("models.common.cancel")} )} @@ -3189,19 +3189,19 @@ export function SettingsTabs({ type }: SettingsTabsProps) { ); } - const STYLE_ICONS: Record = { - classic: MessageSquare, - business: Briefcase, - tech: Code, + const STYLE_ICONS: Record = { + classic: IconMessage, + business: IconBriefcase, + tech: IconCode, }; const styleTabOptions: Array<{ id: "style" | "prompts"; label: string; - Icon: LucideIcon; + Icon: Icon; }> = [ - { id: "style", label: t("models.styleTab.style"), Icon: Type }, - { id: "prompts", label: t("models.styleTab.prompts"), Icon: MessageSquare }, + { id: "style", label: t("models.styleTab.style"), Icon: IconTypography }, + { id: "prompts", label: t("models.styleTab.prompts"), Icon: IconMessage }, ]; return ( @@ -3239,7 +3239,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { gap: 7, }} > - + {label} ); @@ -3256,7 +3256,7 @@ export function SettingsTabs({ type }: SettingsTabsProps) { } + icon={} title={st.title} description={st.description} onClick={() => diff --git a/src/windows/widget/Widget.tsx b/src/windows/widget/Widget.tsx index ce6cb8c..ebac953 100644 --- a/src/windows/widget/Widget.tsx +++ b/src/windows/widget/Widget.tsx @@ -5,7 +5,7 @@ import { listen } from "@tauri-apps/api/event"; import { getCurrentWebview } from "@tauri-apps/api/webview"; import { cursorPosition, getCurrentWindow } from "@tauri-apps/api/window"; import { writeText } from "@tauri-apps/plugin-clipboard-manager"; -import { Check, Copy, FileAudio, Loader2, PhoneCall } from "lucide-react"; +import { IconCheck, IconCopy, IconFileMusic, IconLoader2, IconPhoneCall } from "../../lib/icons"; import { HISTORY_CLEARED_EVENT, @@ -1292,15 +1292,15 @@ function FileDropPill({ }} > {isProcessing ? ( - ) : isSuccess ? ( - + ) : ( - + )}
{isStarting || isProcessing ? ( - ) : isSuccess ? ( - + ) : isError ? ( ) : ( - + )}
@@ -1627,9 +1627,9 @@ function IdlePill({ }} > {copySucceeded ? ( - + ) : ( - + )} )} @@ -1783,8 +1783,8 @@ function FlowRecordingWidget({ )} {state === "long" && ( - {longMark === "phone" && } - {longMark === "success" && } + {longMark === "phone" && } + {longMark === "success" && } {longMark === "error" && "!"} )} diff --git a/src/windows/widget/WidgetNoticeOverlay.tsx b/src/windows/widget/WidgetNoticeOverlay.tsx index 2834c7d..3175e2e 100644 --- a/src/windows/widget/WidgetNoticeOverlay.tsx +++ b/src/windows/widget/WidgetNoticeOverlay.tsx @@ -2,7 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react"; import type { ReactElement } from "react"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; -import { AlertCircle, Info } from "lucide-react"; +import { IconAlertCircle, IconInfoCircle } from "../../lib/icons"; import { NOTICE_AREA_HEIGHT, NOTICE_WIDGET_WIDTH, WIDGET_NOTICE_EVENT, type WidgetNoticeState } from "./widgetConstants"; @@ -50,7 +50,7 @@ export function WidgetNoticeOverlay(): ReactElement | null { const toggleExpanded = () => { setExpanded((value) => !value); }; - const Icon = notice.tone === "error" ? AlertCircle : Info; + const Icon = notice.tone === "error" ? IconAlertCircle : IconInfoCircle; const iconColor = notice.tone === "error" ? "rgba(184,52,52,0.9)" : "rgba(0,0,0,0.58)"; return ( @@ -113,7 +113,7 @@ export function WidgetNoticeOverlay(): ReactElement | null { >