Add close-to-tray behavior and tray controls (#1254)
* Add hide-to-tray app menu action * Add close-to-tray setting for Sly window * Rework the tray: opt-in, localized, and one owner for the close button - The tray is built only while "keep running in the tray" is on and dropped when it is switched off, instead of adding an icon to everyone's menu bar. The setting now defaults to off; closing the window quits, as it always did. - The setting moved to the config file as general.closeToTray, so the tray survives the switch away from localStorage. - Menu labels come from the frontend, so the tray speaks the app's language and re-labels itself when the language changes. - Left click opens the menu on macOS and the window elsewhere, which is what each platform expects. - The window's close button has a single owner again: the frontend hook decides between hiding and exiting. The backend no longer prevents exit, so quitting from the tray, from the menu or with the keyboard all take the same path and Sona is still stopped on the way out. - Linux builds pull in libayatana-appindicator3-dev, without which the tray never appears. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Let the window hide itself, and translate the tray The close button did nothing with the setting on: hiding the window is a capability the webview was never granted, so `hide()` was rejected while the close stayed prevented, leaving the window open with no way to shut it. Grant `core:window:allow-hide`, and log it if the hide ever fails again rather than failing in silence — leaving the window open beats quitting unasked. Also fills in the tray strings for the other 21 locales. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Bring the window back when the dock icon is clicked A window hidden to the tray left the dock icon inert on macOS: clicking it reports through NSApplicationDelegate's applicationShouldHandleReopen, which reaches the app as RunEvent::Reopen rather than as a window event, and nothing was listening. Show the main window when that arrives with no visible windows. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Your Name <your_github_email@example.com> Co-authored-by: thewh1teagle <61390950+thewh1teagle@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ edition = "2021"
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["protocol-asset", "devtools", "macos-private-api"] }
|
||||
tauri = { version = "2", features = ["protocol-asset", "devtools", "macos-private-api", "tray-icon"] }
|
||||
|
||||
# Plugins
|
||||
tauri-plugin-window-state = "2"
|
||||
|
||||
@@ -39,6 +39,7 @@
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-destroy",
|
||||
"core:window:allow-show",
|
||||
"core:window:allow-hide",
|
||||
"core:window:allow-is-visible",
|
||||
"window-state:allow-restore-state",
|
||||
"window-state:allow-save-window-state",
|
||||
|
||||
@@ -15,7 +15,8 @@ mod logging;
|
||||
mod setup;
|
||||
mod sona;
|
||||
mod transcript;
|
||||
use tauri::{Emitter, Manager};
|
||||
mod tray;
|
||||
use tauri::Emitter;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod dock;
|
||||
@@ -24,6 +25,7 @@ mod dock;
|
||||
mod custom_protocol;
|
||||
|
||||
use eyre::{eyre, Result};
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_window_state::StateFlags;
|
||||
|
||||
use error::LogError;
|
||||
@@ -36,19 +38,19 @@ async fn main() -> Result<()> {
|
||||
|
||||
#[allow(unused_mut)]
|
||||
let mut builder = tauri::Builder::default()
|
||||
.manage(tray::TrayState::default())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_http::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||
tracing::debug!("{}, {argv:?}, {cwd}", app.package_info().name);
|
||||
if let Some(webview) = app.get_webview_window("main") {
|
||||
webview.set_focus().map_err(|e| eyre!("{:?}", e)).log_error();
|
||||
}
|
||||
tray::show_main_window(app);
|
||||
app.emit("single-instance", argv).map_err(|e| eyre!("{:?}", e)).log_error();
|
||||
}))
|
||||
.setup(|app| {
|
||||
setup::setup(app)?;
|
||||
analytics::track_event(app, analytics::events::APP_STARTED);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.plugin(
|
||||
@@ -89,6 +91,7 @@ async fn main() -> Result<()> {
|
||||
cmd::app::get_cargo_features,
|
||||
cmd::config::write_config_atomically,
|
||||
cmd::config::get_config_path,
|
||||
tray::set_tray,
|
||||
cmd::transcribe::transcribe,
|
||||
cmd::files::glob_files,
|
||||
cmd::files::pick_media_paths,
|
||||
@@ -135,6 +138,14 @@ async fn main() -> Result<()> {
|
||||
.expect("error while building tauri application");
|
||||
|
||||
app.run(|app, event| match event {
|
||||
// Clicking the dock icon while the window is hidden in the tray has to bring it back;
|
||||
// macOS reports the click here rather than as a window event.
|
||||
#[cfg(target_os = "macos")]
|
||||
tauri::RunEvent::Reopen { has_visible_windows, .. } => {
|
||||
if !has_visible_windows {
|
||||
tray::show_main_window(app);
|
||||
}
|
||||
}
|
||||
tauri::RunEvent::ExitRequested { .. } | tauri::RunEvent::Exit => {
|
||||
let mutex = app.state::<tokio::sync::Mutex<setup::SonaState>>();
|
||||
if let Ok(mut guard) = mutex.try_lock() {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
//! System tray icon, present only while "close to tray" is switched on.
|
||||
//!
|
||||
//! The tray is opt-in: an icon that appears in every user's menu bar or notification area for a
|
||||
//! feature they never asked for is clutter, so it is built when the setting is enabled and dropped
|
||||
//! when it is turned off. Its menu labels come from the frontend, which is where the translations
|
||||
//! live — the backend has none.
|
||||
|
||||
use eyre::{eyre, Result};
|
||||
use serde::Deserialize;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{
|
||||
menu::{MenuBuilder, MenuItemBuilder},
|
||||
tray::{TrayIcon, TrayIconBuilder},
|
||||
AppHandle, Manager,
|
||||
};
|
||||
|
||||
use crate::error::LogError;
|
||||
|
||||
/// Menu labels in the app's language, handed over by the frontend.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct TrayLabels {
|
||||
pub show: String,
|
||||
pub hide: String,
|
||||
pub quit: String,
|
||||
}
|
||||
|
||||
/// Holds the icon so it lives as long as the app, and so it can be dropped to remove the tray.
|
||||
#[derive(Default)]
|
||||
pub struct TrayState(Mutex<Option<TrayIcon>>);
|
||||
|
||||
pub fn show_main_window(app: &AppHandle) {
|
||||
let Some(window) = app.get_webview_window("main") else {
|
||||
return;
|
||||
};
|
||||
window.unminimize().map_err(|error| eyre!("{error:?}")).log_error();
|
||||
window.show().map_err(|error| eyre!("{error:?}")).log_error();
|
||||
window.set_focus().map_err(|error| eyre!("{error:?}")).log_error();
|
||||
}
|
||||
|
||||
fn hide_main_window(app: &AppHandle) {
|
||||
let Some(window) = app.get_webview_window("main") else {
|
||||
return;
|
||||
};
|
||||
window.hide().map_err(|error| eyre!("{error:?}")).log_error();
|
||||
}
|
||||
|
||||
/// Build the tray when `enabled`, take it down when not. Safe to call repeatedly — the labels are
|
||||
/// re-applied on every call, which is how a language change reaches the menu.
|
||||
pub fn apply(app: &AppHandle, enabled: bool, labels: TrayLabels) -> Result<()> {
|
||||
let state = app.state::<TrayState>();
|
||||
let mut current = state.0.lock().map_err(|error| eyre!("tray state poisoned: {error}"))?;
|
||||
|
||||
if !enabled {
|
||||
// Dropping the icon removes it from the menu bar / notification area.
|
||||
*current = None;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let show = MenuItemBuilder::with_id("show", labels.show).build(app)?;
|
||||
let hide = MenuItemBuilder::with_id("hide", labels.hide).build(app)?;
|
||||
let quit = MenuItemBuilder::with_id("quit", labels.quit).build(app)?;
|
||||
let menu = MenuBuilder::new(app).items(&[&show, &hide, &quit]).build()?;
|
||||
|
||||
if let Some(tray) = current.as_ref() {
|
||||
tray.set_menu(Some(menu))?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let icon = app
|
||||
.default_window_icon()
|
||||
.cloned()
|
||||
.ok_or_else(|| eyre!("the app has no icon to put in the tray"))?;
|
||||
let tray = TrayIconBuilder::with_id("main")
|
||||
.icon(icon)
|
||||
.tooltip("Vibe")
|
||||
.menu(&menu)
|
||||
// macOS puts the menu on a left click; Windows and Linux expect it to open the window.
|
||||
.show_menu_on_left_click(cfg!(target_os = "macos"))
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconEvent};
|
||||
if cfg!(target_os = "macos") {
|
||||
return;
|
||||
}
|
||||
if let TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} = event
|
||||
{
|
||||
show_main_window(tray.app_handle());
|
||||
}
|
||||
})
|
||||
.on_menu_event(|app, event| match event.id.as_ref() {
|
||||
"show" => show_main_window(app),
|
||||
"hide" => hide_main_window(app),
|
||||
// Exit rather than close the window: the window's close handler hides it instead.
|
||||
"quit" => app.exit(0),
|
||||
_ => {}
|
||||
})
|
||||
.build(app)?;
|
||||
|
||||
*current = Some(tray);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Called by the frontend whenever the setting or the app language changes, and once at startup.
|
||||
#[tauri::command]
|
||||
pub fn set_tray(app: AppHandle, enabled: bool, labels: TrayLabels) -> Result<()> {
|
||||
apply(&app, enabled, labels)
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from 'react'
|
||||
import { getTextDirection } from '~/paraglide/runtime.js'
|
||||
import { useTray } from '~/lib/tray'
|
||||
import { Navigate, Route, Routes } from 'react-router-dom'
|
||||
import UpdateProgress from '~/components/updater-progress'
|
||||
import '~/globals.css'
|
||||
@@ -28,8 +29,9 @@ export default function App() {
|
||||
}
|
||||
|
||||
function AppContent() {
|
||||
const { displayLanguage } = usePreferenceProvider()
|
||||
const { displayLanguage, closeToTray } = usePreferenceProvider()
|
||||
const dir = getTextDirection(displayLanguage)
|
||||
useTray(closeToTray, displayLanguage)
|
||||
|
||||
useEffect(() => {
|
||||
document.body.dir = dir
|
||||
|
||||
@@ -11,6 +11,7 @@ export const CONFIG_KEYS = {
|
||||
theme: 'general.theme',
|
||||
firstRun: 'general.firstRun',
|
||||
skippedSetup: 'general.skippedSetup',
|
||||
closeToTray: 'general.closeToTray',
|
||||
analyticsEnabled: 'analytics_enabled',
|
||||
|
||||
// Model
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useEffect } from 'react'
|
||||
import { m } from '~/paraglide/messages.js'
|
||||
|
||||
/**
|
||||
* Keeps the system tray in step with the setting.
|
||||
*
|
||||
* The tray only exists while "keep running in the tray" is on, and its menu is built in Rust — which
|
||||
* has no translations — so the labels travel with every call. That also means a language change
|
||||
* re-labels the menu, since this runs again when the messages change.
|
||||
*/
|
||||
export function useTray(enabled: boolean, locale: string) {
|
||||
useEffect(() => {
|
||||
void invoke('set_tray', {
|
||||
enabled,
|
||||
labels: { show: m.trayShow(), hide: m.trayHide(), quit: m.trayQuit() },
|
||||
}).catch((error) => console.error('failed to update the tray:', error))
|
||||
}, [enabled, locale])
|
||||
}
|
||||
@@ -4,12 +4,25 @@ import { exit } from '@tauri-apps/plugin-process'
|
||||
import { m } from '~/paraglide/messages.js'
|
||||
import { UnlistenFn } from '@tauri-apps/api/event'
|
||||
|
||||
export function useConfirmExit(shouldConfirm: boolean) {
|
||||
/**
|
||||
* Owns what the window's close button does. With "keep running in the tray" on, closing hides the
|
||||
* window and the app stays alive for global dictation; otherwise it exits, asking first when a
|
||||
* transcription would be lost. Quitting from the tray goes through `app.exit` and never lands here.
|
||||
*/
|
||||
export function useConfirmExit(closeToTray: boolean, shouldConfirm: boolean) {
|
||||
useEffect(() => {
|
||||
let unlistenFn: UnlistenFn | null = null
|
||||
getCurrentWebviewWindow()
|
||||
.listen('tauri://close-requested', async () => {
|
||||
const currentWindow = getCurrentWebviewWindow()
|
||||
currentWindow
|
||||
.onCloseRequested(async (event) => {
|
||||
if (closeToTray) {
|
||||
event.preventDefault()
|
||||
// Leaving the window open is the safer failure: quitting unasked would lose work.
|
||||
await currentWindow.hide().catch((error) => console.error('failed to hide the window:', error))
|
||||
return
|
||||
}
|
||||
if (shouldConfirm) {
|
||||
event.preventDefault()
|
||||
if (await confirm(m.confirmExit())) {
|
||||
await exit(0)
|
||||
}
|
||||
@@ -21,5 +34,5 @@ export function useConfirmExit(shouldConfirm: boolean) {
|
||||
unlistenFn = unlisten
|
||||
})
|
||||
return () => unlistenFn?.()
|
||||
}, [shouldConfirm])
|
||||
}, [closeToTray, shouldConfirm])
|
||||
}
|
||||
|
||||
@@ -138,6 +138,8 @@ export const mediaMiscHandlers: CommandHandlerMap = {
|
||||
|
||||
pick_media_paths: async () => null,
|
||||
|
||||
set_tray: async () => null,
|
||||
|
||||
download_audio: async (args) => {
|
||||
const outPath = String(args?.outPath ?? `${APP_LOCAL_DATA}/tmp.m4a`)
|
||||
let cancelled = false
|
||||
|
||||
@@ -59,8 +59,7 @@ export function viewModel() {
|
||||
setSummarizeSegments(null)
|
||||
setTranscriptTab('transcript')
|
||||
})
|
||||
useConfirmExit((segments?.length ?? 0) > 0 || loading)
|
||||
|
||||
const shouldConfirmExit = (segments?.length ?? 0) > 0 || loading
|
||||
const {
|
||||
files,
|
||||
setFiles,
|
||||
@@ -75,6 +74,8 @@ export function viewModel() {
|
||||
clearFolderSelection,
|
||||
} = useMediaSelection()
|
||||
const preference = usePreferenceProvider()
|
||||
useConfirmExit(preference.closeToTray, shouldConfirmExit)
|
||||
|
||||
const {
|
||||
cancelYtDlpRef,
|
||||
cancelYtDlpDownload,
|
||||
|
||||
@@ -211,7 +211,7 @@ export function SessionProvider({ children }: { children: ReactNode }) {
|
||||
// actually be lost: a run in flight, or finished results that never reached disk
|
||||
// (saving disabled or failed).
|
||||
const hasUnsavedResults = queue.jobs.some((job) => job.status === 'done' && !job.hydrated && !job.savedPath)
|
||||
useConfirmExit(queue.running || hasUnsavedResults)
|
||||
useConfirmExit(preference.closeToTray, queue.running || hasUnsavedResults)
|
||||
|
||||
const mode: SessionMode = queue.jobs.length === 0 ? 'idle' : queue.running || queue.jobs.some((job) => job.status === 'queued') ? 'working' : 'done'
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ReactComponent as LinkIcon } from '~/icons/link.svg'
|
||||
import * as config from '~/lib/config'
|
||||
import { DisplayLanguageInput } from '~/components/display-language-input'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '~/components/ui/select'
|
||||
import { Switch } from '~/components/ui/switch'
|
||||
import { ActionRow, SettingsGroup, SettingsRow, rowControlClass, type SettingsViewModel } from './shared'
|
||||
|
||||
export function GeneralSection({ vm }: { vm: SettingsViewModel }) {
|
||||
@@ -20,6 +21,9 @@ export function GeneralSection({ vm }: { vm: SettingsViewModel }) {
|
||||
<SettingsRow label={m.language()}>
|
||||
<DisplayLanguageInput value={vm.preference.displayLanguage} onSelect={vm.preference.setDisplayLanguage} className="w-52" />
|
||||
</SettingsRow>
|
||||
<SettingsRow label={m.closeToTray()} description={m.closeToTrayInfo()}>
|
||||
<Switch checked={vm.preference.closeToTray} onCheckedChange={vm.preference.setCloseToTray} />
|
||||
</SettingsRow>
|
||||
<SettingsRow label={m.theme()}>
|
||||
<Select value={vm.preference.theme} onValueChange={(value) => vm.preference.setTheme(value as 'light' | 'dark')}>
|
||||
<SelectTrigger className={`w-36 ${rowControlClass}`}>
|
||||
|
||||
@@ -39,6 +39,8 @@ export interface Preference {
|
||||
setModelDisplayNames: ModifyState<Record<string, string>>
|
||||
skippedSetup: boolean
|
||||
setSkippedSetup: ModifyState<boolean>
|
||||
closeToTray: boolean
|
||||
setCloseToTray: ModifyState<boolean>
|
||||
textAreaDirection: Direction
|
||||
setTextAreaDirection: ModifyState<Direction>
|
||||
textFormatTranscript: TextFormat
|
||||
@@ -160,6 +162,8 @@ export function PreferenceProvider({ children }: { children: ReactNode }) {
|
||||
const [modelMetadata, setModelMetadata] = useState<ModelMetadata | null>(null)
|
||||
const [modelDisplayNames, setModelDisplayNames] = usePersisted<Record<string, string>>(CONFIG_KEYS.modelDisplayNames, {})
|
||||
const [skippedSetup, setSkippedSetup] = usePersisted<boolean>(CONFIG_KEYS.skippedSetup, false)
|
||||
// Opt-in: a tray icon nobody asked for is clutter, and quitting from the X is what people expect.
|
||||
const [closeToTray, setCloseToTray] = usePersisted<boolean>(CONFIG_KEYS.closeToTray, false)
|
||||
const [textAreaDirection, setTextAreaDirection] = usePersisted<Direction>(CONFIG_KEYS.textAreaDirection, 'ltr')
|
||||
const [textFormatTranscript, setTextFormatTranscript] = usePersisted<TextFormat>(CONFIG_KEYS.textFormatTranscript, 'pdf')
|
||||
const [textFormatSummary, setTextFormatSummary] = usePersisted<TextFormat>(CONFIG_KEYS.textFormatSummary, 'md')
|
||||
@@ -305,6 +309,8 @@ export function PreferenceProvider({ children }: { children: ReactNode }) {
|
||||
setTextAreaDirection,
|
||||
skippedSetup,
|
||||
setSkippedSetup,
|
||||
closeToTray,
|
||||
setCloseToTray,
|
||||
displayLanguage: language,
|
||||
setDisplayLanguage,
|
||||
soundOnFinish,
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"shortcutNeedsModifier": "Задръжте също ⌘, ⌃, ⌥ или ⇧",
|
||||
"changeShortcut": "Смяна на клавишната комбинация",
|
||||
"never": "Никога",
|
||||
"configFile": "Конфигурационен файл"
|
||||
"configFile": "Конфигурационен файл",
|
||||
"closeToTray": "Оставане в системния трей",
|
||||
"closeToTrayInfo": "Затварянето на прозореца оставя Vibe да работи в системния трей, така че глобалната диктовка продължава да работи. Затворете го от менюто на трея.",
|
||||
"trayShow": "Отваряне на Vibe",
|
||||
"trayHide": "Скриване на Vibe",
|
||||
"trayQuit": "Изход от Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "En curs",
|
||||
"queued": "A la cua",
|
||||
"never": "Mai",
|
||||
"configFile": "Fitxer de configuració"
|
||||
"configFile": "Fitxer de configuració",
|
||||
"closeToTray": "Mantén l'execució a la safata del sistema",
|
||||
"closeToTrayInfo": "En tancar la finestra, Vibe continua executant-se a la safata del sistema, de manera que el dictat global segueix funcionant. Pots sortir-ne des del menú de la safata.",
|
||||
"trayShow": "Obre Vibe",
|
||||
"trayHide": "Amaga Vibe",
|
||||
"trayQuit": "Surt de Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"shortcutNeedsModifier": "Podržte také ⌘, ⌃, ⌥ nebo ⇧",
|
||||
"changeShortcut": "Změnit zkratku",
|
||||
"never": "Nikdy",
|
||||
"configFile": "Konfigurační soubor"
|
||||
"configFile": "Konfigurační soubor",
|
||||
"closeToTray": "Ponechat běžet v oznamovací oblasti",
|
||||
"closeToTrayInfo": "Zavření okna ponechá Vibe běžet v oznamovací oblasti, takže globální diktování dál funguje. Ukončit ho můžete z nabídky ikony.",
|
||||
"trayShow": "Otevřít Vibe",
|
||||
"trayHide": "Skrýt Vibe",
|
||||
"trayQuit": "Ukončit Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"shortcutNeedsModifier": "Halten Sie zusätzlich ⌘, ⌃, ⌥ oder ⇧ gedrückt",
|
||||
"changeShortcut": "Tastenkombination ändern",
|
||||
"never": "Nie",
|
||||
"configFile": "Konfigurationsdatei"
|
||||
"configFile": "Konfigurationsdatei",
|
||||
"closeToTray": "Im Infobereich weiterlaufen lassen",
|
||||
"closeToTrayInfo": "Beim Schließen des Fensters läuft Vibe im Infobereich weiter, sodass das globale Diktat weiterhin funktioniert. Beenden können Sie es über das Menü im Infobereich.",
|
||||
"trayShow": "Vibe öffnen",
|
||||
"trayHide": "Vibe ausblenden",
|
||||
"trayQuit": "Vibe beenden"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"shortcutNeedsModifier": "Hold ⌘, ⌃, ⌥ or ⇧ as well",
|
||||
"changeShortcut": "Change shortcut",
|
||||
"never": "Never",
|
||||
"configFile": "Config file"
|
||||
"configFile": "Config file",
|
||||
"closeToTray": "Keep running in the tray",
|
||||
"closeToTrayInfo": "Closing the window leaves Vibe running in the tray, so global dictation keeps working. Quit it from the tray menu.",
|
||||
"trayShow": "Open Vibe",
|
||||
"trayHide": "Hide Vibe",
|
||||
"trayQuit": "Quit Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "En curso",
|
||||
"queued": "En cola",
|
||||
"never": "Nunca",
|
||||
"configFile": "Archivo de configuración"
|
||||
"configFile": "Archivo de configuración",
|
||||
"closeToTray": "Mantener en la bandeja del sistema",
|
||||
"closeToTrayInfo": "Al cerrar la ventana, Vibe sigue ejecutándose en la bandeja del sistema, así que el dictado global continúa funcionando. Puedes salir desde el menú de la bandeja.",
|
||||
"trayShow": "Abrir Vibe",
|
||||
"trayHide": "Ocultar Vibe",
|
||||
"trayQuit": "Salir de Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "En curso",
|
||||
"queued": "En cola",
|
||||
"never": "Nunca",
|
||||
"configFile": "Archivo de configuración"
|
||||
"configFile": "Archivo de configuración",
|
||||
"closeToTray": "Mantener en la bandeja del sistema",
|
||||
"closeToTrayInfo": "Al cerrar la ventana, Vibe sigue ejecutándose en la bandeja del sistema, así que el dictado global continúa funcionando. Puedes salir desde el menú de la bandeja.",
|
||||
"trayShow": "Abrir Vibe",
|
||||
"trayHide": "Ocultar Vibe",
|
||||
"trayQuit": "Salir de Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "En cours",
|
||||
"queued": "En attente",
|
||||
"never": "Jamais",
|
||||
"configFile": "Fichier de configuration"
|
||||
"configFile": "Fichier de configuration",
|
||||
"closeToTray": "Garder Vibe dans la zone de notification",
|
||||
"closeToTrayInfo": "Fermer la fenêtre laisse Vibe s'exécuter dans la zone de notification, la dictée globale continue donc de fonctionner. Quittez-le depuis le menu de l'icône.",
|
||||
"trayShow": "Ouvrir Vibe",
|
||||
"trayHide": "Masquer Vibe",
|
||||
"trayQuit": "Quitter Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "בתהליך",
|
||||
"queued": "בתור",
|
||||
"never": "אף פעם",
|
||||
"configFile": "קובץ הגדרות"
|
||||
"configFile": "קובץ הגדרות",
|
||||
"closeToTray": "המשך פעולה במגש המערכת",
|
||||
"closeToTrayInfo": "סגירת החלון משאירה את Vibe פועל במגש המערכת, כך שההכתבה הגלובלית ממשיכה לעבוד. אפשר לצאת מתפריט המגש.",
|
||||
"trayShow": "פתיחת Vibe",
|
||||
"trayHide": "הסתרת Vibe",
|
||||
"trayQuit": "יציאה מ-Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "चल रहा है",
|
||||
"queued": "कतार में",
|
||||
"never": "कभी नहीं",
|
||||
"configFile": "कॉन्फ़िग फ़ाइल"
|
||||
"configFile": "कॉन्फ़िग फ़ाइल",
|
||||
"closeToTray": "ट्रे में चलता रहने दें",
|
||||
"closeToTrayInfo": "विंडो बंद करने पर Vibe सिस्टम ट्रे में चलता रहता है, जिससे ग्लोबल डिक्टेशन काम करता रहता है। इसे ट्रे मेन्यू से बंद करें।",
|
||||
"trayShow": "Vibe खोलें",
|
||||
"trayHide": "Vibe छिपाएँ",
|
||||
"trayQuit": "Vibe बंद करें"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "In corso",
|
||||
"queued": "In coda",
|
||||
"never": "Mai",
|
||||
"configFile": "File di configurazione"
|
||||
"configFile": "File di configurazione",
|
||||
"closeToTray": "Continua a funzionare nell'area di notifica",
|
||||
"closeToTrayInfo": "Chiudendo la finestra, Vibe resta in esecuzione nell'area di notifica, così la dettatura globale continua a funzionare. Puoi uscire dal menu dell'icona.",
|
||||
"trayShow": "Apri Vibe",
|
||||
"trayHide": "Nascondi Vibe",
|
||||
"trayQuit": "Esci da Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "処理中",
|
||||
"queued": "待機中",
|
||||
"never": "しない",
|
||||
"configFile": "設定ファイル"
|
||||
"configFile": "設定ファイル",
|
||||
"closeToTray": "閉じても通知領域で実行を続ける",
|
||||
"closeToTrayInfo": "ウィンドウを閉じてもVibeは通知領域で動作を続けるため、グローバル音声入力をそのまま使えます。終了するには通知領域のメニューから操作してください。",
|
||||
"trayShow": "Vibeを開く",
|
||||
"trayHide": "Vibeを隠す",
|
||||
"trayQuit": "Vibeを終了"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "진행 중",
|
||||
"queued": "대기 중",
|
||||
"never": "안 함",
|
||||
"configFile": "설정 파일"
|
||||
"configFile": "설정 파일",
|
||||
"closeToTray": "트레이에서 계속 실행",
|
||||
"closeToTrayInfo": "창을 닫아도 Vibe가 시스템 트레이에서 계속 실행되어 전역 받아쓰기를 그대로 사용할 수 있습니다. 종료하려면 트레이 메뉴를 사용하세요.",
|
||||
"trayShow": "Vibe 열기",
|
||||
"trayHide": "Vibe 숨기기",
|
||||
"trayQuit": "Vibe 종료"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "Pågår",
|
||||
"queued": "I kø",
|
||||
"never": "Aldri",
|
||||
"configFile": "Konfigurasjonsfil"
|
||||
"configFile": "Konfigurasjonsfil",
|
||||
"closeToTray": "Fortsett å kjøre i systemstatusfeltet",
|
||||
"closeToTrayInfo": "Når du lukker vinduet, fortsetter Vibe å kjøre i systemstatusfeltet, slik at global diktering fortsatt virker. Avslutt det fra menyen i systemstatusfeltet.",
|
||||
"trayShow": "Åpne Vibe",
|
||||
"trayHide": "Skjul Vibe",
|
||||
"trayQuit": "Avslutt Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "W toku",
|
||||
"queued": "W kolejce",
|
||||
"never": "Nigdy",
|
||||
"configFile": "Plik konfiguracyjny"
|
||||
"configFile": "Plik konfiguracyjny",
|
||||
"closeToTray": "Działaj dalej w zasobniku systemowym",
|
||||
"closeToTrayInfo": "Zamknięcie okna pozostawia Vibe działający w zasobniku systemowym, więc dyktowanie globalne nadal działa. Zamknij go z menu w zasobniku.",
|
||||
"trayShow": "Otwórz Vibe",
|
||||
"trayHide": "Ukryj Vibe",
|
||||
"trayQuit": "Zakończ Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "Em andamento",
|
||||
"queued": "Na fila",
|
||||
"never": "Nunca",
|
||||
"configFile": "Arquivo de configuração"
|
||||
"configFile": "Arquivo de configuração",
|
||||
"closeToTray": "Manter na bandeja do sistema",
|
||||
"closeToTrayInfo": "Ao fechar a janela, o Vibe continua em execução na bandeja do sistema, então o ditado global continua funcionando. Saia pelo menu da bandeja.",
|
||||
"trayShow": "Abrir Vibe",
|
||||
"trayHide": "Ocultar Vibe",
|
||||
"trayQuit": "Sair do Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "Выполняется",
|
||||
"queued": "В очереди",
|
||||
"never": "Никогда",
|
||||
"configFile": "Файл конфигурации"
|
||||
"configFile": "Файл конфигурации",
|
||||
"closeToTray": "Оставлять работать в трее",
|
||||
"closeToTrayInfo": "При закрытии окна Vibe продолжает работать в трее, поэтому глобальная диктовка остаётся доступной. Выйти можно из меню в трее.",
|
||||
"trayShow": "Открыть Vibe",
|
||||
"trayHide": "Скрыть Vibe",
|
||||
"trayQuit": "Выйти из Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "Pågår",
|
||||
"queued": "I kö",
|
||||
"never": "Aldrig",
|
||||
"configFile": "Konfigurationsfil"
|
||||
"configFile": "Konfigurationsfil",
|
||||
"closeToTray": "Fortsätt köra i systemfältet",
|
||||
"closeToTrayInfo": "När du stänger fönstret fortsätter Vibe att köra i systemfältet, så global diktering fungerar fortfarande. Avsluta det från menyn i systemfältet.",
|
||||
"trayShow": "Öppna Vibe",
|
||||
"trayHide": "Dölj Vibe",
|
||||
"trayQuit": "Avsluta Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "Devam ediyor",
|
||||
"queued": "Sırada",
|
||||
"never": "Asla",
|
||||
"configFile": "Yapılandırma dosyası"
|
||||
"configFile": "Yapılandırma dosyası",
|
||||
"closeToTray": "Sistem tepsisinde çalışmaya devam et",
|
||||
"closeToTrayInfo": "Pencereyi kapattığınızda Vibe sistem tepsisinde çalışmayı sürdürür, böylece genel dikte çalışmaya devam eder. Tepsi menüsünden çıkabilirsiniz.",
|
||||
"trayShow": "Vibe'ı aç",
|
||||
"trayHide": "Vibe'ı gizle",
|
||||
"trayQuit": "Vibe'dan çık"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "Đang xử lý",
|
||||
"queued": "Trong hàng đợi",
|
||||
"never": "Không bao giờ",
|
||||
"configFile": "Tệp cấu hình"
|
||||
"configFile": "Tệp cấu hình",
|
||||
"closeToTray": "Tiếp tục chạy trong khay hệ thống",
|
||||
"closeToTrayInfo": "Đóng cửa sổ sẽ để Vibe tiếp tục chạy trong khay hệ thống, nên đọc chính tả toàn cục vẫn hoạt động. Thoát bằng menu ở khay hệ thống.",
|
||||
"trayShow": "Mở Vibe",
|
||||
"trayHide": "Ẩn Vibe",
|
||||
"trayQuit": "Thoát Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "进行中",
|
||||
"queued": "排队中",
|
||||
"never": "从不",
|
||||
"configFile": "配置文件"
|
||||
"configFile": "配置文件",
|
||||
"closeToTray": "关闭窗口后保留在托盘",
|
||||
"closeToTrayInfo": "关闭窗口后 Vibe 会继续在系统托盘中运行,全局听写仍然可用。可从托盘菜单退出。",
|
||||
"trayShow": "打开 Vibe",
|
||||
"trayHide": "隐藏 Vibe",
|
||||
"trayQuit": "退出 Vibe"
|
||||
}
|
||||
|
||||
@@ -440,5 +440,10 @@
|
||||
"inProgress": "進行中",
|
||||
"queued": "排隊中",
|
||||
"never": "永不",
|
||||
"configFile": "設定檔"
|
||||
"configFile": "設定檔",
|
||||
"closeToTray": "關閉視窗後保留在系統匣",
|
||||
"closeToTrayInfo": "關閉視窗後 Vibe 會繼續在系統匣中運行,全域語音輸入仍然可用。可從系統匣選單結束程式。",
|
||||
"trayShow": "開啟 Vibe",
|
||||
"trayHide": "隱藏 Vibe",
|
||||
"trayQuit": "結束 Vibe"
|
||||
}
|
||||
|
||||
@@ -194,6 +194,8 @@ def main() -> int:
|
||||
"cmake",
|
||||
"libasound2-dev",
|
||||
"libxdo-dev",
|
||||
# Tray icon support on Linux goes through the appindicator bindings.
|
||||
"libayatana-appindicator3-dev",
|
||||
]
|
||||
run_cmd("sudo", "apt-get", "update")
|
||||
for pkg in apt_packages:
|
||||
|
||||
Reference in New Issue
Block a user