2 Commits

Author SHA1 Message Date
rain9
fb90b53bda style: adjust placeholder styles 2025-12-25 18:26:08 +08:00
SteveLauC
20597b2e86 feat: read localized names from root InfoPlist.strings (#1029)
* feat(mac): read localized names from root InfoPlist.strings

* release notes
2025-12-25 15:35:16 +08:00
11 changed files with 281 additions and 270 deletions

View File

@@ -72,7 +72,6 @@
"unlisten",
"unlistener",
"unlisteners",
"unmaximize",
"unminimize",
"uuidv",
"VITE",

View File

@@ -14,11 +14,10 @@ Information about release notes of Coco App is provided here.
### 🚀 Features
- feat: support app search even if Spotlight is disabled #1028
- feat: read localized names from root InfoPlist.strings #1029
### 🐛 Bug fix
- fix: avoid recentering when resizing to compact after leaving extension #1030
### ✈️ Improvements
- refactor: add a timeout to open() #1025
@@ -526,4 +525,4 @@ Information about release notes of Coco App is provided here.
### Bug fix
### Improvements
### Improvements

2
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=ec174b7761bfa5eb7af0a93218b014e2d1505643#ec174b7761bfa5eb7af0a93218b014e2d1505643"
source = "git+https://github.com/infinilabs/applications-rs?rev=0b086c036b13178048252cddba2c46868a55352e#0b086c036b13178048252cddba2c46868a55352e"
dependencies = [
"anyhow",
"core-foundation 0.9.4",

View File

@@ -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 = "ec174b7761bfa5eb7af0a93218b014e2d1505643" }
applications = { git = "https://github.com/infinilabs/applications-rs", rev = "0b086c036b13178048252cddba2c46868a55352e" }
tokio-native-tls = "0.3" # For wss connections
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = { version = "0.20", features = ["native-tls"] }
@@ -172,4 +172,4 @@ windows = { version = "0.61", features = ["Win32_Foundation", "Win32_System_Com"
windows-sys = { version = "0.61", features = ["Win32", "Win32_System", "Win32_System_Com"] }
[target."cfg(target_os = \"windows\")".build-dependencies]
bindgen = "0.72.1"
bindgen = "0.72.1"

View File

@@ -120,7 +120,7 @@ const AutoResizeTextarea = forwardRef<
autoCapitalize="none"
spellCheck="false"
className={cn(
"auto-resize-textarea text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] placeholder-text-xs placeholder-[#999] dark:placeholder-gray-500 bg-transparent custom-scrollbar resize-none overflow-y-auto",
"auto-resize-textarea text-base flex-1 outline-none w-full min-w-[200px] text-[#333] dark:text-[#d8d8d8] bg-transparent custom-scrollbar resize-none overflow-y-auto",
{
"overflow-y-hidden": lineCount === 1,
}

View File

@@ -1,5 +1,5 @@
import React from "react";
import { useState, useEffect, useMemo } from "react";
import { useState, useEffect, useMemo, useRef, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { Maximize2, Minimize2, Focus } from "lucide-react";
@@ -7,18 +7,46 @@ import { useSearchStore } from "@/stores/searchStore";
import {
ExtensionFileSystemPermission,
FileSystemAccess,
ViewExtensionUISettingsOrNull,
} from "../Settings/Extensions";
import platformAdapter from "@/utils/platformAdapter";
import { useShortcutsStore } from "@/stores/shortcutsStore";
import { useViewExtensionWindow } from "@/hooks/useViewExtensionWindow";
import { isMac } from "@/utils/platform";
import { useAppStore } from "@/stores/appStore";
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();
const { t } = useTranslation();
const [isFullscreen, setIsFullscreen] = useState(false);
const prevWindowRef = useRef<{
width: number;
height: number;
resizable: boolean;
x: number;
y: number;
} | null>(null);
const fullscreenPrevRef = useRef<{
width: number;
height: number;
resizable: boolean;
x: number;
y: number;
} | null>(null);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
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.
@@ -168,15 +196,183 @@ const ViewExtension: React.FC = () => {
}, [reversedApis, permission]); // Add apiPermissions as dependency
const fileUrl = viewExtensionOpened[2];
const {
resizable,
scale,
iframeRef,
isFullscreen,
toggleFullscreen,
focusIframe,
} = useViewExtensionWindow();
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(() => {
if (uiWidth != null) return uiWidth;
if (fallbackViewSize != null) return fallbackViewSize.width;
return 0;
}, [uiWidth, fallbackViewSize]);
const baseHeight = useMemo(() => {
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
);
setScale(Math.max(nextScale, 0.1));
}, [hasExplicitWindowSize, baseWidth, baseHeight]);
const applyFullscreen = useCallback(
async (next: boolean) => {
if (next) {
const size = await platformAdapter.getWindowSize();
const resizable = await platformAdapter.isWindowResizable();
const pos = await platformAdapter.getWindowPosition();
fullscreenPrevRef.current = {
width: size.width,
height: size.height,
resizable,
x: pos.x,
y: pos.y,
};
if (isMac && isTauri) {
const monitor = await platformAdapter.getMonitorFromCursor();
if (!monitor) return;
const window = await platformAdapter.getCurrentWebviewWindow();
const factor = await window.scaleFactor();
const { size, position } = monitor;
const { width, height } = size.toLogical(factor);
const { x, y } = position.toLogical(factor);
await platformAdapter.setWindowSize(width, height);
await platformAdapter.setWindowPosition(x, y);
await platformAdapter.setWindowResizable(true);
await recomputeScale();
} else {
await platformAdapter.setWindowFullscreen(true);
await recomputeScale();
}
} else {
if (!isMac) {
await platformAdapter.setWindowFullscreen(false);
}
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 {
iframeRef.current?.contentWindow?.focus();
} catch {}
}, 0);
}
},
[ui, recomputeScale]
);
useEffect(() => {
const applyWindowSettings = async () => {
if (viewExtensionOpened != null) {
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,
resizable,
x: pos.x,
y: pos.y,
};
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 {
iframeRef.current?.contentWindow?.focus();
} catch {}
}, 0);
} else {
if (prevWindowRef.current) {
const prev = prevWindowRef.current;
await platformAdapter.setWindowSize(prev.width, prev.height);
await platformAdapter.setWindowResizable(prev.resizable);
await platformAdapter.centerOnCurrentMonitor();
prevWindowRef.current = null;
await recomputeScale();
setTimeout(() => {
iframeRef.current?.focus();
}, 0);
}
}
};
applyWindowSettings();
return () => {
if (prevWindowRef.current) {
const prev = prevWindowRef.current;
platformAdapter.setWindowSize(prev.width, prev.height);
platformAdapter.setWindowResizable(prev.resizable);
platformAdapter.centerOnCurrentMonitor();
prevWindowRef.current = null;
}
};
}, [
viewExtensionOpened,
ui,
hasExplicitWindowSize,
uiWidth,
uiHeight,
recomputeScale,
]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isFullscreen) {
applyFullscreen(false);
setIsFullscreen(false);
}
};
window.addEventListener("keydown", handleKeyDown, { capture: true });
return () => {
window.removeEventListener("keydown", handleKeyDown, {
capture: true,
} as any);
};
}, [isFullscreen, applyFullscreen]);
return (
<div className="relative w-full h-full">
@@ -188,7 +384,17 @@ const ViewExtension: React.FC = () => {
: t("viewExtension.fullscreen.enter")
}
className="absolute top-2 right-2 z-10 rounded-md bg-black/40 text-white p-2 hover:bg-black/60 focus:outline-none"
onClick={toggleFullscreen}
onClick={async () => {
const next = !isFullscreen;
await applyFullscreen(next);
setIsFullscreen(next);
if (next) {
iframeRef.current?.focus();
try {
iframeRef.current?.contentWindow?.focus();
} catch {}
}
}}
>
{isFullscreen ? (
<Minimize2 className="size-4" />
@@ -202,16 +408,27 @@ const ViewExtension: React.FC = () => {
<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={focusIframe}
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 overflow-hidden"
onMouseDownCapture={focusIframe}
onPointerDown={focusIframe}
onClickCapture={focusIframe}
className="w-full h-full flex items-center justify-center"
onMouseDownCapture={() => {
iframeRef.current?.focus();
}}
onPointerDown={() => {
iframeRef.current?.focus();
}}
onClickCapture={() => {
iframeRef.current?.focus();
}}
>
<iframe
ref={iframeRef}

View File

@@ -1,243 +0,0 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import platformAdapter from "@/utils/platformAdapter";
import { isMac } from "@/utils/platform";
import type { ViewExtensionUISettingsOrNull } from "@/components/Settings/Extensions";
import { useAppStore } from "@/stores/appStore";
import { useSearchStore } from "@/stores/searchStore";
type WindowSnapshot = {
width: number;
height: number;
resizable: boolean;
x: number;
y: number;
};
export function useViewExtensionWindow() {
const isTauri = useAppStore((state) => state.isTauri);
const viewExtensionOpened = useSearchStore((state) => state.viewExtensionOpened);
if (viewExtensionOpened == null) {
throw new Error(
"ViewExtension Error: viewExtensionOpened is null. This should not happen."
);
}
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 iframeRef = useRef<HTMLIFrameElement | null>(null);
const [isFullscreen, setIsFullscreen] = useState(false);
const prevWindowRef = useRef<WindowSnapshot | null>(null);
const fullscreenPrevRef = useRef<WindowSnapshot | null>(null);
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 };
});
const baseWidth = useMemo(() => {
if (uiWidth != null) return uiWidth;
if (fallbackViewSize != null) return fallbackViewSize.width;
return 0;
}, [uiWidth, fallbackViewSize]);
const baseHeight = useMemo(() => {
if (uiHeight != null) return uiHeight;
if (fallbackViewSize != null) return fallbackViewSize.height;
return 0;
}, [uiHeight, fallbackViewSize]);
const focusIframe = useCallback(() => {
iframeRef.current?.focus();
try {
iframeRef.current?.contentWindow?.focus();
} catch {}
}, []);
const focusIframeSoon = useCallback(() => {
setTimeout(() => {
focusIframe();
}, 0);
}, [focusIframe]);
const recomputeScale = useCallback(async () => {
if (!hasExplicitWindowSize) {
setScale(1);
return;
}
const size = await platformAdapter.getWindowSize();
const nextScale = Math.min(size.width / baseWidth, size.height / baseHeight);
setScale(Math.max(nextScale, 0.1));
}, [baseHeight, baseWidth, hasExplicitWindowSize]);
const applyFullscreen = useCallback(
async (next: boolean, options?: { centerOnExit?: boolean }) => {
const centerOnExit = options?.centerOnExit ?? true;
if (next) {
const size = await platformAdapter.getWindowSize();
const resizable = await platformAdapter.isWindowResizable();
const pos = await platformAdapter.getWindowPosition();
fullscreenPrevRef.current = {
width: size.width,
height: size.height,
resizable,
x: pos.x,
y: pos.y,
};
if (isMac && isTauri) {
const monitor = await platformAdapter.getMonitorFromCursor();
if (!monitor) return;
const window = await platformAdapter.getCurrentWebviewWindow();
const factor = await window.scaleFactor();
const { size, position } = monitor;
const { width, height } = size.toLogical(factor);
const { x, y } = position.toLogical(factor);
await platformAdapter.setWindowSize(width, height);
await platformAdapter.setWindowPosition(x, y);
await platformAdapter.setWindowResizable(true);
await recomputeScale();
} else {
await platformAdapter.setWindowFullscreen(true);
await recomputeScale();
}
} else {
const prevPos =
fullscreenPrevRef.current != null
? { x: fullscreenPrevRef.current.x, y: fullscreenPrevRef.current.y }
: null;
if (!isMac) {
await platformAdapter.setWindowFullscreen(false);
}
if (fullscreenPrevRef.current) {
const prev = fullscreenPrevRef.current;
await platformAdapter.setWindowSize(prev.width, prev.height);
await platformAdapter.setWindowResizable(prev.resizable);
fullscreenPrevRef.current = null;
} else if (hasExplicitWindowSize) {
const nextResizable =
ui && typeof ui.resizable === "boolean" ? ui.resizable : true;
await platformAdapter.setWindowSize(uiWidth, uiHeight);
await platformAdapter.setWindowResizable(nextResizable);
}
if (centerOnExit) {
await platformAdapter.centerOnCurrentMonitor();
} else if (prevPos != null) {
await platformAdapter.setWindowPosition(prevPos.x, prevPos.y);
}
await recomputeScale();
focusIframeSoon();
}
},
[
focusIframeSoon,
hasExplicitWindowSize,
isTauri,
recomputeScale,
ui,
uiHeight,
uiWidth,
]
);
const toggleFullscreen = useCallback(async () => {
const next = !isFullscreen;
await applyFullscreen(next);
setIsFullscreen(next);
if (next) focusIframe();
}, [applyFullscreen, focusIframe, isFullscreen]);
useEffect(() => {
const applyWindowSettings = async () => {
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,
resizable,
x: pos.x,
y: pos.y,
};
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();
}
focusIframeSoon();
};
applyWindowSettings();
return () => {
if (prevWindowRef.current) {
const prev = prevWindowRef.current;
if (!isMac && fullscreenPrevRef.current != null) {
platformAdapter.setWindowFullscreen(false);
}
platformAdapter.setWindowSize(prev.width, prev.height);
platformAdapter.setWindowResizable(prev.resizable);
platformAdapter.setWindowPosition(prev.x, prev.y);
prevWindowRef.current = null;
fullscreenPrevRef.current = null;
}
};
}, [
focusIframeSoon,
hasExplicitWindowSize,
recomputeScale,
ui,
uiHeight,
uiWidth,
]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && isFullscreen) {
applyFullscreen(false);
setIsFullscreen(false);
}
};
window.addEventListener("keydown", handleKeyDown, { capture: true });
return () => {
window.removeEventListener("keydown", handleKeyDown, {
capture: true,
} as any);
};
}, [applyFullscreen, isFullscreen]);
return {
ui,
resizable,
scale,
iframeRef,
isFullscreen,
toggleFullscreen,
focusIframe,
};
}

View File

@@ -300,6 +300,32 @@
background-color: #475569;
}
.coco-container .auto-resize-textarea::placeholder {
color: #999;
opacity: 1;
-webkit-text-fill-color: #999;
}
.dark.coco-container .auto-resize-textarea::placeholder,
[data-theme="dark"] .coco-container .auto-resize-textarea::placeholder {
color: #6b7280;
opacity: 1;
-webkit-text-fill-color: #6b7280;
}
.coco-container .auto-resize-textarea:disabled::placeholder {
color: #999;
opacity: 1;
-webkit-text-fill-color: #999;
}
.dark.coco-container .auto-resize-textarea:disabled::placeholder,
[data-theme="dark"] .coco-container .auto-resize-textarea:disabled::placeholder {
color: #6b7280;
opacity: 1;
-webkit-text-fill-color: #6b7280;
}
/* Background styles */
.bg-100 {
background-size: 100% 100%;

View File

@@ -46,6 +46,7 @@ export interface TauriPlatformAdapter extends BasePlatformAdapter {
setWindowMaximized: (enable: boolean) => Promise<void>;
getWindowPosition: () => Promise<{ x: number; y: number }>;
setWindowPosition: (x: number, y: number) => Promise<void>;
centerWindow: () => Promise<void>;
getMonitorFromCursor: () => Promise<Monitor | null>;
centerOnCurrentMonitor: () => Promise<unknown>;
}
@@ -80,6 +81,9 @@ export const createTauriAdapter = (): TauriPlatformAdapter => {
async setWindowPosition(x, y) {
return windowWrapper.setLogicalPosition(x, y);
},
async centerWindow() {
return windowWrapper.center();
},
async getMonitorFromCursor() {
const appWindow = getCurrentWebviewWindow();

View File

@@ -17,9 +17,9 @@ export interface WebPlatformAdapter extends BasePlatformAdapter {
getWindowSize: () => Promise<{ width: number; height: number }>;
setWindowFullscreen: (enable: boolean) => Promise<void>;
getMonitorFromCursor: () => Promise<any>;
centerOnCurrentMonitor: () => Promise<void>;
getWindowPosition: () => Promise<{ x: number; y: number }>;
setWindowPosition: (x: number, y: number) => Promise<void>;
centerOnCurrentMonitor: () => Promise<void>;
}
// Create Web adapter functions
@@ -71,7 +71,6 @@ export const createWebAdapter = (): WebPlatformAdapter => {
},
};
},
async centerOnCurrentMonitor() {
// Not applicable in web mode
return;

View File

@@ -1,4 +1,5 @@
import * as commands from "@/commands";
import { WINDOW_CENTER_BASELINE_HEIGHT } from "@/constants";
import platformAdapter from "../platformAdapter";
// Window operations
@@ -15,6 +16,9 @@ export const windowWrapper = {
const window = await this.getCurrentWebviewWindow();
if (window) {
await window.setSize(new LogicalSize(width, height));
if (height < WINDOW_CENTER_BASELINE_HEIGHT) {
await window.center();
}
}
},
async getLogicalSize() {
@@ -47,6 +51,12 @@ export const windowWrapper = {
const win = getCurrentWindow();
return win.setFullscreen(enable);
},
async center() {
const window = await this.getCurrentWebviewWindow();
if (window) {
return window.center();
}
},
async setLogicalPosition(x: number, y: number) {
const { LogicalPosition } = await import("@tauri-apps/api/dpi");