10 Commits

Author SHA1 Message Date
Steve Lau
42e41d2890 release notes 2025-12-24 16:08:18 +08:00
Steve Lau
bc20635e00 feat: support app search even if Spotlight is disabled
Previously, we relied on Spotlight (mdfind) to fetch the app list,
which means it won't work if users disable their Spotlight index.

This commit bumps the applications-rs library, which can now list
apps using `lsregister`, macOS's launch service tool. (See commit [1]
for details). With this, our app search works even tough Spotlight
is disabled.

[1]: ec174b7761
2025-12-24 16:01:55 +08:00
BiggerRain
03af1d46c5 fix: skip window resize when UI size is missing (#1023)
* fix: skip window resize when UI size is missing

* chore: padding

* chore: update

* refactor: update

* chore: height

* chore: height

* chore: defalut value

---------

Co-authored-by: ayang <473033518@qq.com>
2025-12-23 22:02:19 +08:00
SteveLauC
8638724e68 chore: remove file foo (#1026)
It was a placeholder that I added in PR #1009, I forgot to remove it
before merging that PR. Let's remove it now.
2025-12-22 16:12:14 +08:00
ayangweb
6ae2ed0832 fix: esc key fails to close the popover (#1024)
* fix: esc key fails to close the popover

* refactor: update
2025-12-22 11:51:04 +08:00
Hardy
81dab997a9 chore: update release notes for publish 0.10.0-2619 (#1021)
* chore: update release notes for publish 0.10.0-2619

* Add section headers to release notes

Added section headers for breaking changes, features, bug fixes, and improvements.

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: BiggerRain <15911122312@163.COM>
2025-12-22 09:51:59 +08:00
BiggerRain
7a4364665e chore: bump version to 0.10.0 (#1022) 2025-12-19 17:56:32 +08:00
BiggerRain
444ba968c4 chore: hide selection settings (#1020) 2025-12-19 16:15:03 +08:00
ayangweb
206d8db4f4 refactor: disable input auto-correction (#1019) 2025-12-19 15:46:53 +08:00
BiggerRain
7e40632c29 style: adjust delete button color (#1018) 2025-12-19 12:59:58 +08:00
37 changed files with 234 additions and 673 deletions

View File

@@ -10,6 +10,7 @@
"dataurl",
"deeplink",
"deepthink",
"Detch",
"dtolnay",
"dyld",
"elif",
@@ -56,10 +57,10 @@
"rgba",
"rustup",
"screenshotable",
"seprate",
"serde",
"Shadcn",
"swatinem",
"systempreferences",
"tailwindcss",
"tauri",
"thiserror",

View File

@@ -7,7 +7,19 @@ title: "Release Notes"
Information about release notes of Coco App is provided here.
## Latest (In development)
## Latest (In development)
### ❌ Breaking changes
### 🚀 Features
- feat: support app search even if Spotlight is disabled #1028
### 🐛 Bug fix
### ✈️ Improvements
## 0.10.0 (2025-12-19)
### ❌ Breaking changes

0
foo
View File

View File

@@ -1,7 +1,7 @@
{
"name": "coco",
"private": true,
"version": "0.9.1",
"version": "0.10.0",
"type": "module",
"scripts": {
"dev": "vite",

4
src-tauri/Cargo.lock generated
View File

@@ -332,7 +332,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "applications"
version = "0.3.1"
source = "git+https://github.com/infinilabs/applications-rs?rev=b5fac4034a40d42e72f727f1aa1cc1f19fe86653#b5fac4034a40d42e72f727f1aa1cc1f19fe86653"
source = "git+https://github.com/infinilabs/applications-rs?rev=ec174b7761bfa5eb7af0a93218b014e2d1505643#ec174b7761bfa5eb7af0a93218b014e2d1505643"
dependencies = [
"anyhow",
"core-foundation 0.9.4",
@@ -1132,7 +1132,7 @@ dependencies = [
[[package]]
name = "coco"
version = "0.9.1"
version = "0.10.0"
dependencies = [
"actix-files",
"actix-web",

View File

@@ -1,6 +1,6 @@
[package]
name = "coco"
version = "0.9.1"
version = "0.10.0"
description = "Search, connect, collaborate all in one place."
authors = ["INFINI Labs"]
edition = "2024"
@@ -62,7 +62,7 @@ tauri-plugin-drag = "2"
tauri-plugin-macos-permissions = "2"
tauri-plugin-fs-pro = "2"
tauri-plugin-screenshots = "2"
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "b5fac4034a40d42e72f727f1aa1cc1f19fe86653" }
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "ec174b7761bfa5eb7af0a93218b014e2d1505643" }
tokio-native-tls = "0.3" # For wss connections
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }

View File

@@ -156,13 +156,13 @@ pub struct Extension {
#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
pub(crate) struct ViewExtensionUISettings {
/// Show the search bar
#[serde_inline_default(true)]
#[serde_inline_default(false)]
search_bar: bool,
/// Show the filter bar
#[serde_inline_default(true)]
#[serde_inline_default(false)]
filter_bar: bool,
/// Show the footer
#[serde_inline_default(true)]
#[serde_inline_default(false)]
footer: bool,
/// The recommended width of the window for this extension
width: Option<u32>,

View File

@@ -2,14 +2,12 @@ mod assistant;
mod autostart;
mod common;
mod extension;
mod macos;
mod search;
mod selection_monitor;
mod server;
mod settings;
mod setup;
mod shortcut;
// We need this in main.rs, so it has to be pub
pub mod util;
@@ -31,7 +29,6 @@ use tauri_plugin_autostart::MacosLauncher;
/// Tauri store name
pub(crate) const COCO_TAURI_STORE: &str = "coco_tauri_store";
pub(crate) const WINDOW_CENTER_BASELINE_HEIGHT: i32 = 590;
lazy_static! {
static ref PREVIOUS_MONITOR_NAME: Mutex<Option<String>> = Mutex::new(None);
@@ -46,37 +43,6 @@ lazy_static! {
/// you access it.
pub(crate) static GLOBAL_TAURI_APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
#[tauri::command]
async fn change_window_height(handle: AppHandle, height: u32) {
let window: WebviewWindow = handle.get_webview_window(MAIN_WINDOW_LABEL).unwrap();
let mut size = window.outer_size().unwrap();
size.height = height;
window.set_size(size).unwrap();
// Center the window horizontally and vertically based on the baseline height of 590
let monitor = window.primary_monitor().ok().flatten().or_else(|| {
window
.available_monitors()
.ok()
.and_then(|ms| ms.into_iter().next())
});
if let Some(monitor) = monitor {
let monitor_position = monitor.position();
let monitor_size = monitor.size();
let outer_size = window.outer_size().unwrap();
let window_width = outer_size.width as i32;
let x = monitor_position.x + (monitor_size.width as i32 - window_width) / 2;
let y =
monitor_position.y + (monitor_size.height as i32 - WINDOW_CENTER_BASELINE_HEIGHT) / 2;
let _ = window.set_position(PhysicalPosition::new(x, y));
}
}
// Removed unused Payload to avoid unnecessary serde derive macro invocations
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -125,7 +91,6 @@ pub fn run() {
let app = app_builder
.invoke_handler(tauri::generate_handler![
change_window_height,
shortcut::change_shortcut,
shortcut::unregister_shortcut,
shortcut::get_current_shortcut,
@@ -208,10 +173,6 @@ pub fn run() {
util::logging::app_log_dir,
selection_monitor::set_selection_enabled,
selection_monitor::get_selection_enabled,
macos::permissions::check_accessibility_trusted,
macos::permissions::open_accessibility_settings,
macos::permissions::open_screen_recording_settings,
macos::permissions::open_microphone_settings,
])
.setup(|app| {
#[cfg(target_os = "macos")]
@@ -386,12 +347,13 @@ fn move_window_to_active_monitor(window: &WebviewWindow) {
return;
}
};
let window_width = window_size.width as i32;
let window_height = 590 * scale_factor as i32;
// Horizontal center uses actual width, vertical center uses 590 baseline
let window_x = monitor_position.x + (monitor_size.width as i32 - window_width) / 2;
let window_y = monitor_position.y
+ (monitor_size.height as i32 - WINDOW_CENTER_BASELINE_HEIGHT) / 2;
let window_y = monitor_position.y + (monitor_size.height as i32 - window_height) / 2;
if let Err(e) = window.set_position(PhysicalPosition::new(window_x, window_y)) {
log::error!("Failed to move window: {}", e);

View File

@@ -1 +0,0 @@
pub mod permissions;

View File

@@ -1,58 +0,0 @@
#[tauri::command]
pub fn check_accessibility_trusted() -> bool {
cfg_if::cfg_if! {
if #[cfg(target_os = "macos")] {
let trusted = macos_accessibility_client::accessibility::application_is_trusted();
log::info!(target: "coco_lib::permissions", "check_accessibility_trusted invoked: {}", trusted);
trusted
} else {
log::info!(target: "coco_lib::permissions", "check_accessibility_trusted invoked on non-macOS: false");
false
}
}
}
#[tauri::command]
pub fn open_accessibility_settings() {
cfg_if::cfg_if! {
if #[cfg(target_os = "macos")] {
use std::process::Command;
log::info!(target: "coco_lib::permissions", "open_accessibility_settings invoked");
let _ = Command::new("open")
.arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility")
.status();
} else {
// no-op on non-macOS
}
}
}
#[tauri::command]
pub fn open_screen_recording_settings() {
cfg_if::cfg_if! {
if #[cfg(target_os = "macos")] {
use std::process::Command;
log::info!(target: "coco_lib::permissions", "open_screen_recording_settings invoked");
let _ = Command::new("open")
.arg("x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenRecording")
.status();
} else {
// no-op on non-macOS
}
}
}
#[tauri::command]
pub fn open_microphone_settings() {
cfg_if::cfg_if! {
if #[cfg(target_os = "macos")] {
use std::process::Command;
log::info!(target: "coco_lib::permissions", "open_microphone_settings invoked");
let _ = Command::new("open")
.arg("x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")
.status();
} else {
// no-op on non-macOS
}
}
}

View File

@@ -2,7 +2,6 @@
/// Coordinates use logical (Quartz) points with a top-left origin.
/// Note: `y` is flipped on the backend to match the frontends usage.
use tauri::Emitter;
use tauri::Manager;
#[derive(serde::Serialize, Clone)]
struct SelectionEventPayload {
@@ -15,8 +14,8 @@ use once_cell::sync::Lazy;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
/// Global toggle: selection monitoring enabled for this release.
static SELECTION_ENABLED: AtomicBool = AtomicBool::new(true);
/// Global toggle: selection monitoring disabled for this release.
static SELECTION_ENABLED: AtomicBool = AtomicBool::new(false);
/// Ensure we only start the monitor thread once. Allows delayed start after
/// Accessibility permission is granted post-launch.
@@ -25,9 +24,6 @@ static MONITOR_THREAD_STARTED: AtomicBool = AtomicBool::new(false);
/// Guard to avoid spawning multiple permission watcher threads.
#[cfg(target_os = "macos")]
static PERMISSION_WATCHER_STARTED: AtomicBool = AtomicBool::new(false);
/// Guard to avoid spawning multiple selection store watcher threads.
#[cfg(target_os = "macos")]
static SELECTION_STORE_WATCHER_STARTED: AtomicBool = AtomicBool::new(false);
/// Session flags for controlling macOS Accessibility prompts.
#[cfg(target_os = "macos")]
@@ -35,8 +31,6 @@ static SEEN_ACCESSIBILITY_TRUSTED_ONCE: AtomicBool = AtomicBool::new(false);
#[cfg(target_os = "macos")]
static LAST_ACCESSIBILITY_PROMPT: Lazy<Mutex<Option<std::time::Instant>>> =
Lazy::new(|| Mutex::new(None));
#[cfg(target_os = "macos")]
static LAST_READ_WARN: Lazy<Mutex<Option<std::time::Instant>>> = Lazy::new(|| Mutex::new(None));
#[derive(serde::Serialize, Clone)]
struct SelectionEnabledPayload {
@@ -101,19 +95,7 @@ pub fn start_selection_monitor(app_handle: tauri::AppHandle) {
use tauri::Emitter;
// Sync initial enabled state to the frontend on startup.
// Prefer disk-persisted Zustand store if present
#[cfg(target_os = "macos")]
ensure_selection_store_bootstrap(&app_handle);
if let Some(enabled) = read_selection_enabled_from_store(&app_handle) {
log::info!(target: "coco_lib::selection_monitor", "initial selection-enabled loaded from store: {}", enabled);
set_selection_enabled_internal(&app_handle, enabled);
} else {
log::warn!(target: "coco_lib::selection_monitor", "initial selection-enabled not found in store, falling back to in-memory flag");
set_selection_enabled_internal(&app_handle, is_selection_enabled());
}
// Start a light watcher to keep SELECTION_ENABLED in sync with disk
start_selection_store_watcher(app_handle.clone());
log::info!(target: "coco_lib::selection_monitor", "selection store watcher started");
set_selection_enabled_internal(&app_handle, is_selection_enabled());
// Accessibility permission is required to read selected text in the foreground app.
// If not granted, prompt the user once; if still not granted, skip starting the watcher.
@@ -305,7 +287,6 @@ pub fn start_selection_monitor(app_handle: tauri::AppHandle) {
// If disabled: do not read AX / do not show popup; hide if currently visible.
if !is_selection_enabled() {
log::debug!(target: "coco_lib::selection_monitor", "monitor loop: selection disabled");
if popup_visible {
let _ = app_handle.emit("selection-detected", "");
popup_visible = false;
@@ -463,118 +444,6 @@ fn ensure_accessibility_permission(app_handle: &tauri::AppHandle) -> bool {
false
}
/// Resolve the path to the zustand store file `selection-store.json`.
#[cfg(target_os = "macos")]
fn selection_store_path(app_handle: &tauri::AppHandle) -> std::path::PathBuf {
let mut dir = app_handle
.path()
.app_data_dir()
.expect("failed to find the local dir");
dir.push("zustand");
dir.push("selection-store.json");
log::debug!(target: "coco_lib::selection_monitor", "selection_store_path resolved: {}", dir.display());
dir
}
#[cfg(target_os = "macos")]
fn ensure_selection_store_bootstrap(app_handle: &tauri::AppHandle) {
use std::fs;
use std::io::Write;
let mut dir = app_handle
.path()
.app_data_dir()
.expect("failed to find the local dir");
dir.push("zustand");
let _ = fs::create_dir_all(&dir);
let file = dir.join("selection-store.json");
if !file.exists() {
let initial = serde_json::json!({
"selectionEnabled": true,
"iconsOnly": false,
"toolbarConfig": []
});
if let Ok(mut f) = fs::File::create(&file) {
let _ = f.write_all(
serde_json::to_string(&initial)
.unwrap_or_else(|_| "{}".to_string())
.as_bytes(),
);
log::info!(target: "coco_lib::selection_monitor", "bootstrap selection-store.json created: {}", file.display());
}
}
}
/// Read `selectionEnabled` from the persisted zustand store.
/// Returns Some(bool) if read succeeds; None otherwise.
#[cfg(target_os = "macos")]
fn read_selection_enabled_from_store(app_handle: &tauri::AppHandle) -> Option<bool> {
use std::fs;
let path = selection_store_path(app_handle);
match fs::read_to_string(&path) {
Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
Ok(v) => {
let val = v.get("selectionEnabled").and_then(|b| b.as_bool());
log::info!(target: "coco_lib::selection_monitor", "read_selection_enabled_from_store: {} -> {:?}", path.display(), val);
val
}
Err(e) => {
log::warn!(target: "coco_lib::selection_monitor", "read_selection_enabled_from_store: JSON parse failed for {}: {}", path.display(), e);
None
}
},
Err(e) => {
use std::time::Duration;
use std::time::Instant;
let mut last = LAST_READ_WARN.lock().unwrap();
let now = Instant::now();
let allow = match *last {
Some(ts) => now.duration_since(ts) > Duration::from_secs(30),
None => true,
};
if allow {
log::warn!(target: "coco_lib::selection_monitor", "read_selection_enabled_from_store: read failed for {}: {}", path.display(), e);
*last = Some(now);
} else {
log::debug!(target: "coco_lib::selection_monitor", "read_selection_enabled_from_store: read failed suppressed for {}", path.display());
}
None
}
}
}
/// Spawn a background watcher to sync `SELECTION_ENABLED` with disk every ~1s.
#[cfg(target_os = "macos")]
fn start_selection_store_watcher(app_handle: tauri::AppHandle) {
if SELECTION_STORE_WATCHER_STARTED.swap(true, Ordering::Relaxed) {
return;
}
std::thread::Builder::new()
.name("selection-store-watcher".into())
.spawn(move || {
use std::time::{Duration, Instant};
let mut last_check = Instant::now();
let mut last_val: Option<bool> = None;
loop {
// Check approximately every second
if last_check.elapsed() >= Duration::from_secs(1) {
let current = read_selection_enabled_from_store(&app_handle);
if current.is_some() && current != last_val {
let enabled = current.unwrap();
set_selection_enabled_internal(&app_handle, enabled);
log::info!(target: "coco_lib::selection_monitor", "selection-store-watcher: detected change, enabled={}", enabled);
last_val = current;
}
last_check = Instant::now();
}
std::thread::sleep(Duration::from_millis(200));
}
})
.unwrap_or_else(|e| {
SELECTION_STORE_WATCHER_STARTED.store(false, Ordering::Relaxed);
panic!("selection-store-watcher: failed to spawn: {}", e);
});
}
#[cfg(target_os = "macos")]
fn collect_selection_permission_info() -> SelectionPermissionInfo {
let exe_path = std::env::current_exe()

View File

@@ -110,7 +110,6 @@ pub(crate) async fn backend_setup(tauri_app_handle: AppHandle, app_lang: String)
// Start system-wide selection monitor (macOS-only currently)
#[cfg(target_os = "macos")]
{
log::info!("backend_setup: starting system-wide selection monitor");
crate::selection_monitor::start_selection_monitor(tauri_app_handle.clone());
}

View File

@@ -15,12 +15,12 @@ import { useConnectStore } from "@/stores/connectStore";
import FontIcon from "@/components/Common/Icons/FontIcon";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import NoDataImage from "@/components/Common/NoDataImage";
import PopoverInput from "@/components/Common/PopoverInput";
import { AssistantFetcher } from "./AssistantFetcher";
import AssistantItem from "./AssistantItem";
import Pagination from "@/components/Common/Pagination";
import { useSearchStore } from "@/stores/searchStore";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
interface AssistantListProps {
assistantIDs?: string[];
@@ -241,9 +241,10 @@ export function AssistantList({ assistantIDs = [] }: AssistantListProps) {
searchInputRef.current?.focus();
}}
>
<PopoverInput
<Input
ref={searchInputRef}
autoFocus
autoCorrect="off"
value={keyword}
placeholder={t("assistant.popover.search")}
className="w-full h-8"

View File

@@ -63,6 +63,7 @@ export function Connect({ setIsConnect, onAddServer }: ConnectServiceProps) {
type="text"
id="endpoint"
value={endpointLink}
autoCorrect="off"
placeholder={t("cloud.connect.serverPlaceholder")}
onChange={onChangeEndpoint}
className="text-[#101010] dark:text-white flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent dark:bg-gray-800"

View File

@@ -1,36 +0,0 @@
import type { InputProps } from "@/components/ui/input";
import { Input } from "@/components/ui/input";
import { useKeyPress } from "ahooks";
import { forwardRef, useImperativeHandle, useRef } from "react";
import { POPOVER_PANEL_SELECTOR } from "@/constants";
const PopoverInput = forwardRef<HTMLInputElement, InputProps>((props, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => inputRef.current!);
useKeyPress(
"esc",
(event) => {
if (inputRef.current === document.activeElement) {
event.preventDefault();
event.stopPropagation();
inputRef.current?.blur();
const parentPanel = inputRef.current?.closest(POPOVER_PANEL_SELECTOR);
if (parentPanel instanceof HTMLElement) {
parentPanel.focus();
}
}
},
{
target: inputRef,
}
);
return <Input autoCorrect="off" ref={inputRef} {...(props as any)} />;
});
export default PopoverInput;

View File

@@ -1,9 +1,11 @@
import { FC, HTMLAttributes, useEffect, useRef, useState } from "react";
import { useKeyPress } from "ahooks";
import clsx from "clsx";
import { last } from "lodash-es";
import { POPOVER_PANEL_SELECTOR } from "@/constants";
import {
OPENED_POPOVER_TRIGGER_SELECTOR,
POPOVER_PANEL_SELECTOR,
} from "@/constants";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import { useAppStore } from "@/stores/appStore";
import { KeyType } from "ahooks/lib/useKeyPress";
@@ -43,22 +45,21 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
const [visibleShortcut, setVisibleShortcut] = useState<boolean>();
useEffect(() => {
const popoverPanelEls = document.querySelectorAll(POPOVER_PANEL_SELECTOR);
const popoverPanelEl = last(popoverPanelEls);
const popoverPanelEl = document.querySelector(POPOVER_PANEL_SELECTOR);
const openedPopoverTriggerEl = document.querySelector(
OPENED_POPOVER_TRIGGER_SELECTOR
);
if (!openPopover || !popoverPanelEl) {
return setVisibleShortcut(modifierKeyPressed);
}
const popoverButtonEl = document.querySelector(
`[aria-controls="${popoverPanelEl.id}"]`
const isChildInPanel = popoverPanelEl?.contains(childrenRef.current);
const isChildInTrigger = openedPopoverTriggerEl?.contains(
childrenRef.current
);
const isChildInPanel = popoverPanelEl?.contains(childrenRef.current);
const isChildInButton = popoverButtonEl?.contains(childrenRef.current);
const isChildInPopover = isChildInPanel || isChildInButton;
const isChildInPopover = isChildInPanel || isChildInTrigger;
setVisibleShortcut(isChildInPopover && modifierKeyPressed);
}, [openPopover, modifierKeyPressed]);
@@ -111,7 +112,7 @@ const VisibleKey: FC<VisibleKeyProps> = (props) => {
{showTooltip && visibleShortcut ? (
<div
className={clsx(
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-[14px] bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
"size-4 flex items-center justify-center font-normal text-xs text-[#333] leading-3.5 bg-[#ccc] dark:bg-[#6B6B6B] rounded-md shadow-[-6px_0px_6px_2px_#fff] dark:shadow-[-6px_0px_6px_2px_#000] absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2",
shortcutClassName
)}
>

View File

@@ -243,7 +243,7 @@ const ExtensionDetail: FC<ExtensionDetailProps> = (props) => {
}}
deleteButtonProps={{
className:
"!text-[#FF4949] bg-[#F8F9FA] dark:text-white dark:bg-[#202126] border-[#E6E6E6] dark:border-white/10",
"text-white bg-[#FF4949] hover:bg-[#FF4949] border-[#E6E6E6] dark:border-white/10",
}}
setIsOpen={setIsOpen}
onCancel={handleCancel}

View File

@@ -17,10 +17,10 @@ import Checkbox from "@/components/Common/Checkbox";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import VisibleKey from "@/components/Common/VisibleKey";
import NoDataImage from "@/components/Common/NoDataImage";
import PopoverInput from "@/components/Common/PopoverInput";
import Pagination from "@/components/Common/Pagination";
import { SearchQuery } from "@/utils";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
interface MCPPopoverProps {
mcp_servers: any;
@@ -263,8 +263,9 @@ export default function MCPPopover({
/>
</div>
<PopoverInput
<Input
autoFocus
autoCorrect="off"
value={keyword}
ref={searchInputRef}
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"

View File

@@ -17,9 +17,9 @@ import Checkbox from "@/components/Common/Checkbox";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import VisibleKey from "@/components/Common/VisibleKey";
import NoDataImage from "@/components/Common/NoDataImage";
import PopoverInput from "@/components/Common/PopoverInput";
import Pagination from "@/components/Common/Pagination";
import { Button } from "../ui/button";
import { Input } from "../ui/input";
interface SearchPopoverProps {
datasource: any;
@@ -271,8 +271,9 @@ export default function SearchPopover({
/>
</div>
<PopoverInput
<Input
autoFocus
autoCorrect="off"
value={keyword}
ref={searchInputRef}
className="size-full px-2 rounded-lg border dark:border-white/10 bg-transparent"

View File

@@ -7,7 +7,7 @@ import { useSearchStore } from "@/stores/searchStore";
import {
ExtensionFileSystemPermission,
FileSystemAccess,
ViewExtensionUISettings,
ViewExtensionUISettingsOrNull,
} from "../Settings/Extensions";
import platformAdapter from "@/utils/platformAdapter";
import { useShortcutsStore } from "@/stores/shortcutsStore";
@@ -18,7 +18,7 @@ const ViewExtension: React.FC = () => {
const { viewExtensionOpened } = useSearchStore();
const isTauri = useAppStore((state) => state.isTauri);
// Complete list of the backend APIs, grouped by their category.
const [apis, setApis] = useState<Map<string, string[]> | null>(null);
const { setModifierKeyPressed } = useShortcutsStore();
@@ -39,9 +39,14 @@ const ViewExtension: React.FC = () => {
y: number;
} | null>(null);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const DEFAULT_VIEW_WIDTH = 1200;
const DEFAULT_VIEW_HEIGHT = 900;
const [scale, setScale] = useState(1);
const [fallbackViewSize, setFallbackViewSize] = useState<{
width: number;
height: number;
} | null>(() => {
if (typeof window === "undefined") return null;
return { width: window.innerWidth, height: window.innerHeight };
});
if (viewExtensionOpened == null) {
// When this view gets loaded, this state should not be NULL.
@@ -191,23 +196,38 @@ const ViewExtension: React.FC = () => {
}, [reversedApis, permission]); // Add apiPermissions as dependency
const fileUrl = viewExtensionOpened[2];
const ui: ViewExtensionUISettings | undefined = useMemo(() => {
return viewExtensionOpened[4] as ViewExtensionUISettings | undefined;
const ui: ViewExtensionUISettingsOrNull = useMemo(() => {
return viewExtensionOpened[4] as ViewExtensionUISettingsOrNull;
}, [viewExtensionOpened]);
const resizable = ui?.resizable;
const uiWidth = ui && typeof ui.width === "number" ? ui.width : null;
const uiHeight = ui && typeof ui.height === "number" ? ui.height : null;
const hasExplicitWindowSize = uiWidth != null && uiHeight != null;
const baseWidth = useMemo(() => {
return ui && typeof ui?.width === "number" ? ui.width : DEFAULT_VIEW_WIDTH;
}, [ui]);
if (uiWidth != null) return uiWidth;
if (fallbackViewSize != null) return fallbackViewSize.width;
return 0;
}, [uiWidth, fallbackViewSize]);
const baseHeight = useMemo(() => {
return ui && typeof ui?.height === "number" ? ui.height : DEFAULT_VIEW_HEIGHT;
}, [ui]);
if (uiHeight != null) return uiHeight;
if (fallbackViewSize != null) return fallbackViewSize.height;
return 0;
}, [uiHeight, fallbackViewSize]);
const recomputeScale = useCallback(async () => {
if (!hasExplicitWindowSize) {
setScale(1);
return;
}
const size = await platformAdapter.getWindowSize();
const nextScale = Math.min(size.width / baseWidth, size.height / baseHeight);
const nextScale = Math.min(
size.width / baseWidth,
size.height / baseHeight
);
setScale(Math.max(nextScale, 0.1));
}, [baseWidth, baseHeight]);
}, [hasExplicitWindowSize, baseWidth, baseHeight]);
const applyFullscreen = useCallback(
async (next: boolean) => {
@@ -247,16 +267,23 @@ const ViewExtension: React.FC = () => {
if (!isMac) {
await platformAdapter.setWindowFullscreen(false);
}
const nextWidth =
ui && typeof ui.width === "number" ? ui.width : DEFAULT_VIEW_WIDTH;
const nextHeight =
ui && typeof ui.height === "number" ? ui.height : DEFAULT_VIEW_HEIGHT;
const nextResizable =
ui && typeof ui.resizable === "boolean" ? ui.resizable : true;
await platformAdapter.setWindowSize(nextWidth, nextHeight);
await platformAdapter.setWindowResizable(nextResizable);
await platformAdapter.centerOnCurrentMonitor();
await recomputeScale();
if (fullscreenPrevRef.current) {
const prev = fullscreenPrevRef.current;
await platformAdapter.setWindowSize(prev.width, prev.height);
await platformAdapter.setWindowResizable(prev.resizable);
await platformAdapter.setWindowPosition(prev.x, prev.y);
fullscreenPrevRef.current = null;
await recomputeScale();
} else if (hasExplicitWindowSize) {
const nextResizable =
ui && typeof ui.resizable === "boolean" ? ui.resizable : true;
await platformAdapter.setWindowSize(uiWidth, uiHeight);
await platformAdapter.setWindowResizable(nextResizable);
await platformAdapter.centerOnCurrentMonitor();
await recomputeScale();
} else {
await recomputeScale();
}
setTimeout(() => {
iframeRef.current?.focus();
try {
@@ -274,6 +301,7 @@ const ViewExtension: React.FC = () => {
const size = await platformAdapter.getWindowSize();
const resizable = await platformAdapter.isWindowResizable();
const pos = await platformAdapter.getWindowPosition();
setFallbackViewSize({ width: size.width, height: size.height });
prevWindowRef.current = {
width: size.width,
height: size.height,
@@ -282,17 +310,16 @@ const ViewExtension: React.FC = () => {
y: pos.y,
};
const nextWidth =
ui && typeof ui.width === "number" ? ui.width : DEFAULT_VIEW_WIDTH;
const nextHeight =
ui && typeof ui.height === "number" ? ui.height : DEFAULT_VIEW_HEIGHT;
const nextResizable =
ui && typeof ui.resizable === "boolean" ? ui.resizable : true;
await platformAdapter.setWindowSize(nextWidth, nextHeight);
await platformAdapter.setWindowResizable(nextResizable);
await platformAdapter.centerOnCurrentMonitor();
await recomputeScale();
if (hasExplicitWindowSize) {
const nextResizable =
ui && typeof ui.resizable === "boolean" ? ui.resizable : true;
await platformAdapter.setWindowSize(uiWidth, uiHeight);
await platformAdapter.setWindowResizable(nextResizable);
await platformAdapter.centerOnCurrentMonitor();
await recomputeScale();
} else {
await recomputeScale();
}
setTimeout(() => {
iframeRef.current?.focus();
try {
@@ -324,7 +351,14 @@ const ViewExtension: React.FC = () => {
prevWindowRef.current = null;
}
};
}, [viewExtensionOpened]);
}, [
viewExtensionOpened,
ui,
hasExplicitWindowSize,
uiWidth,
uiHeight,
recomputeScale,
]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isFullscreen) {
@@ -342,7 +376,6 @@ const ViewExtension: React.FC = () => {
return (
<div className="relative w-full h-full">
{isFullscreen && <div className="absolute inset-0 pointer-events-none" />}
{resizable && (
<button
aria-label={
@@ -371,18 +404,20 @@ const ViewExtension: React.FC = () => {
</button>
)}
{/* Focus helper button */}
<button
aria-label={t("viewExtension.focus")}
className="absolute top-2 right-12 z-10 rounded-md bg-black/40 text-white p-2 hover:bg-black/60 focus:outline-none"
onClick={() => {
iframeRef.current?.focus();
try {
iframeRef.current?.contentWindow?.focus();
} catch {}
}}
>
<Focus className="size-4"/>
</button>
{resizable && (
<button
aria-label={t("viewExtension.focus")}
className="absolute top-2 right-12 z-10 rounded-md bg-black/40 text-white p-2 hover:bg-black/60 focus:outline-none"
onClick={() => {
iframeRef.current?.focus();
try {
iframeRef.current?.contentWindow?.focus();
} catch {}
}}
>
<Focus className="size-4" />
</button>
)}
<div
className="w-full h-full flex items-center justify-center"
onMouseDownCapture={() => {
@@ -398,10 +433,8 @@ const ViewExtension: React.FC = () => {
<iframe
ref={iframeRef}
src={fileUrl}
className="border-0"
className="border-0 w-full h-full"
style={{
width: `${baseWidth}px`,
height: `${baseHeight}px`,
transform: `scale(${scale})`,
transformOrigin: "center center",
outline: "none",

View File

@@ -182,12 +182,9 @@ function SearchChat({
});
useEffect(() => {
const unlisten = platformAdapter.listenEvent(
"refresh-window-size",
() => {
debouncedSetWindowSize();
}
);
const unlisten = platformAdapter.listenEvent("refresh-window-size", () => {
debouncedSetWindowSize();
});
return () => {
unlisten
.then((fn) => {

View File

@@ -1,207 +0,0 @@
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useMount } from "ahooks";
import { ShieldCheck, Monitor, Mic, RotateCcw } from "lucide-react";
import clsx from "clsx";
import platformAdapter from "@/utils/platformAdapter";
import SettingsItem from "@/components/Settings/SettingsItem";
const Permissions = () => {
const { t } = useTranslation();
const [accessibilityAuthorized, setAccessibilityAuthorized] = useState<boolean | null>(null);
const [screenAuthorized, setScreenAuthorized] = useState<boolean | null>(null);
const [microphoneAuthorized, setMicrophoneAuthorized] = useState<boolean | null>(null);
const refresh = async () => {
const [ax, sr, mic] = await Promise.all([
platformAdapter.invokeBackend<boolean>("check_accessibility_trusted"),
platformAdapter.checkScreenRecordingPermission(),
platformAdapter.checkMicrophonePermission(),
]);
console.info("[permissions] refreshed", { accessibility: ax, screenRecording: sr, microphone: mic });
setAccessibilityAuthorized(ax);
setScreenAuthorized(sr);
setMicrophoneAuthorized(mic);
};
useMount(refresh);
const openAccessibilitySettings = async () => {
const window = await platformAdapter.getCurrentWebviewWindow();
await window.setAlwaysOnTop(false);
console.info("[permissions] open accessibility settings");
await platformAdapter.invokeBackend("open_accessibility_settings");
await refresh();
};
const requestScreenRecording = async () => {
const window = await platformAdapter.getCurrentWebviewWindow();
await window.setAlwaysOnTop(false);
console.info("[permissions] request screen recording");
await platformAdapter.requestScreenRecordingPermission();
await platformAdapter.invokeBackend("open_screen_recording_settings");
await refresh();
};
const requestMicrophone = async () => {
const window = await platformAdapter.getCurrentWebviewWindow();
await window.setAlwaysOnTop(false);
console.info("[permissions] request microphone");
await platformAdapter.requestMicrophonePermission();
await platformAdapter.invokeBackend("open_microphone_settings");
await refresh();
};
const [refreshing, setRefreshing] = useState(false);
const handleRefresh = async () => {
if (refreshing) return;
setRefreshing(true);
try {
await refresh();
} finally {
setRefreshing(false);
}
};
useEffect(() => {
const unlisten1 = platformAdapter.listenEvent("selection-permission-required", async () => {
console.info("[permissions] selection-permission-required received");
await refresh();
});
const unlisten2 = platformAdapter.listenEvent("selection-permission-info", async (evt: any) => {
console.info("[permissions] selection-permission-info", evt?.payload);
await refresh();
});
return () => {
unlisten1.then((fn) => fn());
unlisten2.then((fn) => fn());
};
}, []);
return (
<>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
{t("settings.advanced.permissions.title")}
</h2>
<div className="space-y-6">
<SettingsItem
icon={ShieldCheck}
title={t("settings.advanced.permissions.accessibility.title")}
description={t("settings.advanced.permissions.accessibility.description")}
>
<div className="flex items-center gap-3">
{accessibilityAuthorized ? (
<span className="text-sm font-medium text-green-600 dark:text-green-500">
{t("settings.common.status.authorized")}
</span>
) : (
<span className="text-sm font-medium text-red-600 dark:text-red-500">
{t("settings.common.status.notAuthorized")}
</span>
)}
<button
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm"
onClick={openAccessibilitySettings}
>
{t("settings.common.actions.openNow")}
</button>
<button
className={clsx(
"flex items-center justify-center size-8 rounded-[6px] border border-black/5 dark:border-white/10 transition bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100",
{ "opacity-70 cursor-not-allowed": refreshing }
)}
onClick={handleRefresh}
title={t("settings.common.actions.refresh")}
>
<RotateCcw
className={clsx("size-4", {
"animate-spin": refreshing,
})}
/>
</button>
</div>
</SettingsItem>
<SettingsItem
icon={Monitor}
title={t("settings.advanced.permissions.screenRecording.title")}
description={t("settings.advanced.permissions.screenRecording.description")}
>
<div className="flex items-center gap-3">
{screenAuthorized ? (
<span className="text-sm font-medium text-green-600 dark:text-green-500">
{t("settings.common.status.authorized")}
</span>
) : (
<span className="text-sm font-medium text-red-600 dark:text-red-500">
{t("settings.common.status.notAuthorized")}
</span>
)}
<button
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm"
onClick={requestScreenRecording}
>
{t("settings.common.actions.openNow")}
</button>
<button
className={clsx(
"flex items-center justify-center size-8 rounded-[6px] border border-black/5 dark:border-white/10 transition bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100",
{ "opacity-70 cursor-not-allowed": refreshing }
)}
onClick={handleRefresh}
title={t("settings.common.actions.refresh")}
>
<RotateCcw
className={clsx("size-4", {
"animate-spin": refreshing,
})}
/>
</button>
</div>
</SettingsItem>
<SettingsItem
icon={Mic}
title={t("settings.advanced.permissions.microphone.title")}
description={t("settings.advanced.permissions.microphone.description")}
>
<div className="flex items-center gap-3">
{microphoneAuthorized ? (
<span className="text-sm font-medium text-green-600 dark:text-green-500">
{t("settings.common.status.authorized")}
</span>
) : (
<span className="text-sm font-medium text-red-600 dark:text-red-500">
{t("settings.common.status.notAuthorized")}
</span>
)}
<button
className="px-3 py-1.5 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm"
onClick={requestMicrophone}
>
{t("settings.common.actions.openNow")}
</button>
<button
className={clsx(
"flex items-center justify-center size-8 rounded-[6px] border border-black/5 dark:border-white/10 transition bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100",
{ "opacity-70 cursor-not-allowed": refreshing }
)}
onClick={handleRefresh}
title={t("settings.common.actions.refresh")}
>
<RotateCcw
className={clsx("size-4", {
"animate-spin": refreshing,
})}
/>
</button>
</div>
</SettingsItem>
</div>
</>
);
};
export default Permissions;

View File

@@ -11,13 +11,6 @@ import {
} from "lucide-react";
import { useMount } from "ahooks";
import { isNil } from "lodash-es";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
import Shortcuts from "./components/Shortcuts";
import SettingsItem from "../SettingsItem";
@@ -28,10 +21,15 @@ import SettingsInput from "@/components//Settings/SettingsInput";
import platformAdapter from "@/utils/platformAdapter";
import UpdateSettings from "./components/UpdateSettings";
import SettingsToggle from "../SettingsToggle";
import SelectionSettings from "./components/Selection";
import { isMac } from "@/utils/platform";
import Permissions from "./components/Permissions";
// import SelectionSettings from "./components/Selection";
// import { isMac } from "@/utils/platform";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
const Advanced = () => {
const { t } = useTranslation();
@@ -198,9 +196,7 @@ const Advanced = () => {
})}
</div>
{isMac && <Permissions />}
{isMac && <SelectionSettings />}
{/* {isMac && <SelectionSettings />} */}
<Shortcuts />

View File

@@ -81,6 +81,8 @@ export interface ViewExtensionUISettings {
detachable: boolean;
}
export type ViewExtensionUISettingsOrNull = ViewExtensionUISettings | null | undefined;
export interface Extension {
id: ExtensionId;
type: ExtensionType;

View File

@@ -14,19 +14,19 @@ export default function SettingsItem({
children,
}: SettingsItemProps) {
return (
<div className="flex items-center justify-between gap-6 min-w-0">
<div className="flex items-center space-x-3 min-w-0">
<div className="flex items-center justify-between gap-6">
<div className="flex items-center space-x-3">
<Icon className="h-5 min-w-5 text-gray-400 dark:text-gray-500" />
<div className="max-w-[680px] min-w-0">
<div>
<h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
{title}
</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 whitespace-normal break-words">
<p className="text-sm text-gray-500 dark:text-gray-400">
{description}
</p>
</div>
</div>
<div className="flex-shrink-0">{children}</div>
{children}
</div>
);
}

View File

@@ -1,6 +1,7 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { cn } from "@/lib/utils";
import { OPENED_POPOVER_TRIGGER_SELECTOR } from "@/constants";
const Popover = PopoverPrimitive.Root;
const PopoverTrigger = PopoverPrimitive.Trigger;
@@ -34,6 +35,24 @@ const PopoverContent = React.forwardRef<
)}
data-popover-panel
id={panelId}
onEscapeKeyDown={(event) => {
event.stopPropagation();
event.preventDefault();
if (
document.activeElement instanceof HTMLInputElement ||
document.activeElement instanceof HTMLTextAreaElement
) {
return document.activeElement.blur();
}
const el = document.querySelector(OPENED_POPOVER_TRIGGER_SELECTOR);
if (el instanceof HTMLElement) {
el.click();
}
}}
{...props}
/>
)

View File

@@ -1,8 +1,11 @@
export const POPOVER_PANEL_SELECTOR = '[data-popover-panel]';
export const POPOVER_PANEL_SELECTOR = "[data-radix-popper-content-wrapper]";
export const HISTORY_PANEL_ID = "headlessui-popover-panel:history-panel";
export const OPENED_POPOVER_TRIGGER_SELECTOR =
"[aria-haspopup='dialog'][aria-expanded='true'][data-state='open']";
export const CONTEXT_MENU_PANEL_ID = "headlessui-popover-panel:context-menu";
export const HISTORY_PANEL_ID = "popover-panel:history-panel";
export const CONTEXT_MENU_PANEL_ID = "popover-panel:context-menu";
export const DEFAULT_COCO_SERVER_ID = "default_coco_server";

View File

@@ -5,20 +5,15 @@ import { HISTORY_PANEL_ID } from "@/constants";
import { closeHistoryPanel } from "@/utils";
const useEscape = () => {
const visibleContextMenu = useSearchStore((state) => {
return state.visibleContextMenu;
});
const setVisibleContextMenu = useSearchStore((state) => {
return state.setVisibleContextMenu;
});
const viewExtensionOpened = useSearchStore((state) => {
return state.viewExtensionOpened;
});
const { setVisibleContextMenu } = useSearchStore();
useKeyPress("esc", (event) => {
event.preventDefault();
event.stopPropagation();
const { visibleContextMenu, viewExtensionOpened } =
useSearchStore.getState();
if (
document.activeElement instanceof HTMLInputElement ||
document.activeElement instanceof HTMLTextAreaElement
@@ -39,6 +34,7 @@ const useEscape = () => {
if (viewExtensionOpened != null) {
return;
}
platformAdapter.hideWindow();
});
};

View File

@@ -16,8 +16,9 @@ export const useModifierKeyPress = () => {
useKeyPress(
modifierKey,
(event) => {
const popoverPanelEl = document.querySelector(POPOVER_PANEL_SELECTOR);
setOpenPopover(Boolean(popoverPanelEl));
const el = document.querySelector(POPOVER_PANEL_SELECTOR);
setOpenPopover(Boolean(el));
setModifierKeyPressed(event.type === "keydown");
},

View File

@@ -0,0 +1,26 @@
import { useMount } from "ahooks";
import platformAdapter from "@/utils/platformAdapter";
import { useSelectionStore } from "@/stores/selectionStore";
export default function useSelectionEnabled() {
useMount(async () => {
try {
const enabled = await platformAdapter.invokeBackend<boolean>("get_selection_enabled");
useSelectionStore.getState().setSelectionEnabled(!!enabled);
} catch (e) {
console.error("get_selection_enabled failed:", e);
}
const unlisten = await platformAdapter.listenEvent(
"selection-enabled",
({ payload }: any) => {
useSelectionStore.getState().setSelectionEnabled(!!payload?.enabled);
}
);
return () => {
unlisten && unlisten();
};
});
}

View File

@@ -18,7 +18,7 @@ export const useTray = () => {
const showCocoShortcuts = useAppStore((state) => state.showCocoShortcuts);
const selectionEnabled = useSelectionStore((state) => state.selectionEnabled);
const setSelectionEnabled = useSelectionStore((state) => state.setSelectionEnabled);
// const setSelectionEnabled = useSelectionStore((state) => state.setSelectionEnabled);
useUpdateEffect(() => {
if (showCocoShortcuts.length === 0) return;
@@ -65,18 +65,18 @@ export const useTray = () => {
itemPromises.push(PredefinedMenuItem.new({ item: "Separator" }));
if (isMac) {
itemPromises.push(
MenuItem.new({
text: selectionEnabled
? t("tray.selectionDisable")
: t("tray.selectionEnable"),
action: async () => {
setSelectionEnabled(!selectionEnabled);
},
})
);
}
// if (isMac) {
// itemPromises.push(
// MenuItem.new({
// text: selectionEnabled
// ? t("tray.selectionDisable")
// : t("tray.selectionEnable"),
// action: async () => {
// setSelectionEnabled(!selectionEnabled);
// },
// })
// );
// }
itemPromises.push(
MenuItem.new({

View File

@@ -187,21 +187,6 @@
"description": "Get early access to new features. May be unstable."
}
},
"permissions": {
"title": "Permissions",
"accessibility": {
"title": "Accessibility",
"description": "Required to read selected text in the foreground app. Grant in System Settings → Privacy & Security → Accessibility."
},
"screenRecording": {
"title": "Screen Recording",
"description": "Required for window/screen screenshots and sharing. Grant in System Settings → Privacy & Security → Screen Recording."
},
"microphone": {
"title": "Microphone",
"description": "Required for voice input and recording. Grant in System Settings → Privacy & Security → Microphone."
}
},
"other": {
"title": "Other Settings",
"connectionTimeout": {
@@ -244,16 +229,6 @@
"extensionsContent": "Extensions settings content",
"advancedContent": "Advanced Settings content"
},
"common": {
"status": {
"authorized": "Authorized",
"notAuthorized": "Not Authorized"
},
"actions": {
"openNow": "Open Settings",
"refresh": "Refresh"
}
},
"extensions": {
"title": "Extensions",
"list": {

View File

@@ -187,21 +187,6 @@
"description": "抢先体验新功能,可能不稳定。"
}
},
"permissions": {
"title": "权限设置",
"accessibility": {
"title": "辅助功能Accessibility",
"description": "用于读取前台应用的选中文本,需在「隐私与安全 → 辅助功能」中授权。"
},
"screenRecording": {
"title": "屏幕录制",
"description": "用于窗口/屏幕截图与共享,需要在「隐私与安全 → 屏幕录制」中授权。"
},
"microphone": {
"title": "麦克风",
"description": "用于语音输入与录音功能,需要在「隐私与安全 → 麦克风」中授权。"
}
},
"other": {
"title": "其它设置",
"connectionTimeout": {
@@ -244,16 +229,6 @@
"extensionsContent": "扩展设置内容",
"advancedContent": "高级设置内容"
},
"common": {
"status": {
"authorized": "已授权",
"notAuthorized": "未授权"
},
"actions": {
"openNow": "去授权",
"refresh": "刷新状态"
}
},
"extensions": {
"title": "扩展",
"list": {

View File

@@ -18,7 +18,7 @@ import { useExtensionsStore } from "@/stores/extensionsStore";
import { useSelectionStore, startSelectionStorePersistence } from "@/stores/selectionStore";
import { useServers } from "@/hooks/useServers";
import { useDeepLinkManager } from "@/hooks/useDeepLinkManager";
import { useSelectionWindow } from "@/hooks/useSelectionWindow";
// import { useSelectionWindow } from "@/hooks/useSelectionWindow";
export default function LayoutOutlet() {
const location = useLocation();
@@ -128,7 +128,7 @@ export default function LayoutOutlet() {
});
// --- Selection window ---
useSelectionWindow();
// useSelectionWindow();
return (
<>

View File

@@ -33,7 +33,7 @@ export const useSelectionStore = create<SelectionStore>((set) => ({
setIconsOnly: (iconsOnly) => set({ iconsOnly }),
toolbarConfig: [],
setToolbarConfig: (toolbarConfig) => set({ toolbarConfig }),
selectionEnabled: true,
selectionEnabled: false,
setSelectionEnabled: (selectionEnabled) => set({ selectionEnabled }),
}));

View File

@@ -58,14 +58,6 @@ export interface EventPayloads {
"selection-detected": string;
"selection-enabled": boolean;
"change-selection-store": any;
"selection-permission-required": boolean;
"selection-permission-info": {
bundle_id: string;
exe_path: string;
in_applications: boolean;
is_dmg: boolean;
is_dev_guess: boolean;
};
}
// Window operation interface

View File

@@ -303,7 +303,7 @@ export const visibleSearchBar = () => {
const ui = viewExtensionOpened[4];
return ui?.search_bar ?? true;
return ui?.search_bar ?? false;
};
export const visibleFilterBar = () => {
@@ -316,7 +316,7 @@ export const visibleFilterBar = () => {
const ui = viewExtensionOpened[4];
return ui?.filter_bar ?? true;
return ui?.filter_bar ?? false;
};
export const visibleFooterBar = () => {
@@ -326,7 +326,7 @@ export const visibleFooterBar = () => {
const ui = viewExtensionOpened[4];
return ui?.footer ?? true;
return ui?.footer ?? false;
};
export const installExtensionError = (error: any) => {