mirror of
https://github.com/infinilabs/coco-app.git
synced 2026-02-24 04:01:27 +01:00
feat: persist store across windows, right-align controls, and fullscreen new window
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": ["main", "chat", "settings", "check", "selection"],
|
||||
"windows": ["main", "chat", "settings", "check", "selection", "view_extension"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:event:allow-emit",
|
||||
|
||||
@@ -14,3 +14,4 @@ pub mod traits;
|
||||
pub static MAIN_WINDOW_LABEL: &str = "main";
|
||||
pub static SETTINGS_WINDOW_LABEL: &str = "settings";
|
||||
pub static CHECK_WINDOW_LABEL: &str = "check";
|
||||
pub static VIEW_EXTENSION_WINDOW_LABEL: &str = "view_extension";
|
||||
|
||||
@@ -12,7 +12,9 @@ mod shortcut;
|
||||
pub mod util;
|
||||
|
||||
use crate::common::register::SearchSourceRegistry;
|
||||
use crate::common::{CHECK_WINDOW_LABEL, MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL};
|
||||
use crate::common::{
|
||||
CHECK_WINDOW_LABEL, MAIN_WINDOW_LABEL, SETTINGS_WINDOW_LABEL, VIEW_EXTENSION_WINDOW_LABEL,
|
||||
};
|
||||
use crate::server::servers::{
|
||||
load_or_insert_default_server, load_servers_token, start_bg_heartbeat_worker,
|
||||
};
|
||||
@@ -23,7 +25,8 @@ use lazy_static::lazy_static;
|
||||
use std::sync::Mutex;
|
||||
use std::sync::OnceLock;
|
||||
use tauri::{
|
||||
AppHandle, Emitter, LogicalPosition, Manager, PhysicalPosition, WebviewWindow, WindowEvent,
|
||||
AppHandle, Emitter, LogicalPosition, Manager, PhysicalPosition, WebviewUrl, WebviewWindow,
|
||||
WebviewWindowBuilder, WindowEvent,
|
||||
};
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
|
||||
@@ -98,6 +101,7 @@ pub fn run() {
|
||||
show_coco,
|
||||
hide_coco,
|
||||
show_settings,
|
||||
show_view_extension,
|
||||
show_check,
|
||||
hide_check,
|
||||
server::servers::add_coco_server,
|
||||
@@ -394,6 +398,36 @@ async fn show_settings(app_handle: AppHandle) {
|
||||
window.set_focus().unwrap();
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn show_view_extension(app_handle: AppHandle) {
|
||||
log::debug!("view extension menu item was clicked");
|
||||
let label = VIEW_EXTENSION_WINDOW_LABEL;
|
||||
|
||||
if let Some(window) = app_handle.get_webview_window(label) {
|
||||
window.show().unwrap();
|
||||
window.unminimize().unwrap();
|
||||
window.set_focus().unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
// If window doesn't exist (e.g. was closed), create it
|
||||
let url = WebviewUrl::App("/ui/view-extension".into());
|
||||
let build_result = WebviewWindowBuilder::new(&app_handle, label, url)
|
||||
.title("View Extension")
|
||||
.inner_size(1000.0, 800.0)
|
||||
.min_inner_size(800.0, 600.0)
|
||||
.resizable(true)
|
||||
.visible(true)
|
||||
.build();
|
||||
|
||||
match build_result {
|
||||
Ok(win) => {
|
||||
let _ = win.set_focus();
|
||||
}
|
||||
Err(e) => log::error!("Failed to create view extension window: {}", e),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn show_check(app_handle: AppHandle) {
|
||||
log::debug!("check menu item was clicked");
|
||||
|
||||
@@ -79,6 +79,24 @@
|
||||
"radius": 7
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "view_extension",
|
||||
"title": "View Extension",
|
||||
"url": "/ui/view-extension",
|
||||
"width": 1000,
|
||||
"minWidth": 800,
|
||||
"height": 800,
|
||||
"minHeight": 600,
|
||||
"center": true,
|
||||
"decorations": true,
|
||||
"transparent": false,
|
||||
"maximizable": true,
|
||||
"skipTaskbar": false,
|
||||
"dragDropEnabled": false,
|
||||
"hiddenTitle": true,
|
||||
"visible": false,
|
||||
"resizable": true
|
||||
},
|
||||
{
|
||||
"label": "selection",
|
||||
"title": "Selection",
|
||||
|
||||
@@ -28,6 +28,10 @@ export function show_settings(): Promise<void> {
|
||||
return invoke('show_settings');
|
||||
}
|
||||
|
||||
export function show_view_extension(): Promise<void> {
|
||||
return invoke('show_view_extension');
|
||||
}
|
||||
|
||||
export function show_check(): Promise<void> {
|
||||
return invoke('show_check');
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import clsx from "clsx";
|
||||
import DropdownList from "./DropdownList";
|
||||
import { SearchResults } from "@/components/Search/SearchResults";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useExtensionStore } from "@/stores/extensionStore";
|
||||
import ContextMenu from "./ContextMenu";
|
||||
import { NoResults } from "@/components/Common/UI/NoResults";
|
||||
import Footer from "@/components/Common/UI/Footer";
|
||||
@@ -52,9 +53,9 @@ const SearchResultsPanel = memo<{
|
||||
const {
|
||||
setSelectedAssistant,
|
||||
selectedSearchContent,
|
||||
visibleExtensionStore,
|
||||
viewExtensionOpened,
|
||||
visibleExtensionStore
|
||||
} = useSearchStore();
|
||||
const viewExtensionOpened = useExtensionStore((state) => state.viewExtensionOpened);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSearchContent?.type === "AI Assistant") {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { ChevronLeft, Search } from "lucide-react";
|
||||
import { FC } from "react";
|
||||
import clsx from "clsx";
|
||||
@@ -7,6 +6,8 @@ import FontIcon from "@/components/Common/Icons/FontIcon";
|
||||
import lightDefaultIcon from "@/assets/images/source_default.png";
|
||||
import darkDefaultIcon from "@/assets/images/source_default_dark.png";
|
||||
import { useThemeStore } from "@/stores/themeStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useExtensionStore } from "@/stores/extensionStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { navigateBack, visibleSearchBar } from "@/utils";
|
||||
import VisibleKey from "../Common/VisibleKey";
|
||||
@@ -74,9 +75,10 @@ export default function SearchIcons({
|
||||
goAskAi,
|
||||
visibleExtensionStore,
|
||||
visibleExtensionDetail,
|
||||
selectedExtension,
|
||||
viewExtensionOpened,
|
||||
selectedExtension
|
||||
} = useSearchStore();
|
||||
|
||||
const viewExtensionOpened = useExtensionStore((state) => state.viewExtensionOpened);
|
||||
|
||||
if (isChatMode) {
|
||||
return null;
|
||||
|
||||
@@ -1,30 +1,35 @@
|
||||
import React from "react";
|
||||
import { useState, useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Maximize2, Minimize2, Focus } from "lucide-react";
|
||||
import { Focus, ExternalLink } from "lucide-react";
|
||||
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useExtensionStore } from "@/stores/extensionStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useShortcutsStore } from "@/stores/shortcutsStore";
|
||||
import { useViewExtensionWindow } from "@/hooks/useViewExtensionWindow";
|
||||
import ViewExtensionIframe from "./ViewExtensionIframe";
|
||||
import { apiPermissionCheck, fsPermissionCheck } from "./viewExtensionPermissions";
|
||||
|
||||
const ViewExtension: React.FC = () => {
|
||||
const { viewExtensionOpened } = useSearchStore();
|
||||
type ControlsProps = {
|
||||
showFullscreen?: boolean;
|
||||
showDetach?: boolean;
|
||||
showFocus?: boolean;
|
||||
forceResizable?: boolean;
|
||||
};
|
||||
|
||||
const ViewExtensionContent: React.FC<ControlsProps> = ({
|
||||
showFullscreen = true,
|
||||
showDetach = true,
|
||||
showFocus = true,
|
||||
forceResizable = false,
|
||||
}) => {
|
||||
const viewExtensionOpened = useExtensionStore((state) => state.viewExtensionOpened);
|
||||
|
||||
// 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();
|
||||
|
||||
if (viewExtensionOpened == null) {
|
||||
// When this view gets loaded, this state should not be NULL.
|
||||
throw new Error(
|
||||
"ViewExtension Error: viewExtensionOpened is null. This should not happen."
|
||||
);
|
||||
}
|
||||
|
||||
// invoke `apis()` and set the state
|
||||
useEffect(() => {
|
||||
setModifierKeyPressed(false);
|
||||
@@ -44,7 +49,7 @@ const ViewExtension: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
// White list of the permission entries
|
||||
const permission = viewExtensionOpened[3];
|
||||
const permission = viewExtensionOpened![3];
|
||||
|
||||
// apis is in format {"category": ["api1", "api2"]}, to make the permission check
|
||||
// easier, reverse the map key values: {"api1": "category", "api2": "category"}
|
||||
@@ -165,47 +170,39 @@ const ViewExtension: React.FC = () => {
|
||||
};
|
||||
}, [reversedApis, permission]); // Add apiPermissions as dependency
|
||||
|
||||
const fileUrl = viewExtensionOpened[2];
|
||||
const fileUrl = viewExtensionOpened![2];
|
||||
|
||||
const {
|
||||
resizable,
|
||||
detachable,
|
||||
hideScrollbar,
|
||||
scale,
|
||||
iframeRef,
|
||||
isFullscreen,
|
||||
toggleFullscreen,
|
||||
focusIframe,
|
||||
} = useViewExtensionWindow();
|
||||
} = useViewExtensionWindow({ forceResizable });
|
||||
|
||||
return (
|
||||
<div className="relative w-full h-full">
|
||||
{resizable && (
|
||||
<button
|
||||
aria-label={
|
||||
isFullscreen
|
||||
? t("viewExtension.fullscreen.exit")
|
||||
: 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}
|
||||
>
|
||||
{isFullscreen ? (
|
||||
<Minimize2 className="size-4" />
|
||||
) : (
|
||||
<Maximize2 className="size-4" />
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{/* Focus helper 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={focusIframe}
|
||||
>
|
||||
<Focus className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
<div className="absolute top-2 right-2 z-10 flex items-center gap-2">
|
||||
{resizable && showFocus && (
|
||||
<button
|
||||
aria-label={t("viewExtension.focus")}
|
||||
className="rounded-md bg-black/40 text-white p-2 hover:bg-black/60 focus:outline-none"
|
||||
onClick={focusIframe}
|
||||
>
|
||||
<Focus className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
{((detachable && showDetach) || (resizable && showFullscreen)) && (
|
||||
<button
|
||||
aria-label={t("viewExtension.detach")}
|
||||
className="rounded-md bg-black/40 text-white p-2 hover:bg-black/60 focus:outline-none"
|
||||
onClick={() => platformAdapter.invokeBackend("show_view_extension")}
|
||||
>
|
||||
<ExternalLink className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ViewExtensionIframe
|
||||
fileUrl={fileUrl}
|
||||
scale={scale}
|
||||
@@ -217,4 +214,18 @@ const ViewExtension: React.FC = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const ViewExtension: React.FC<ControlsProps> = (props) => {
|
||||
const viewExtensionOpened = useExtensionStore((state) => state.viewExtensionOpened);
|
||||
|
||||
if (viewExtensionOpened == null) {
|
||||
return (
|
||||
<div className="flex h-full w-full items-center justify-center text-muted-foreground">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <ViewExtensionContent {...props} />;
|
||||
};
|
||||
|
||||
export default ViewExtension;
|
||||
|
||||
@@ -49,8 +49,24 @@ type ViewExtensionIframeProps = {
|
||||
export default function ViewExtensionIframe(props: ViewExtensionIframeProps) {
|
||||
const { fileUrl, scale, iframeRef, hideScrollbar, focusIframe } = props;
|
||||
|
||||
const isSameOrigin = () => {
|
||||
try {
|
||||
const target = new URL(fileUrl);
|
||||
const current = new URL(window.location.href);
|
||||
return (
|
||||
target.protocol === current.protocol &&
|
||||
target.hostname === current.hostname &&
|
||||
target.port === current.port
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
applyHideScrollbarToIframe(iframeRef.current, hideScrollbar);
|
||||
if (isSameOrigin()) {
|
||||
applyHideScrollbarToIframe(iframeRef.current, hideScrollbar);
|
||||
}
|
||||
}, [hideScrollbar, iframeRef]);
|
||||
|
||||
return (
|
||||
@@ -77,10 +93,12 @@ export default function ViewExtensionIframe(props: ViewExtensionIframeProps) {
|
||||
tabIndex={-1}
|
||||
onLoad={(event) => {
|
||||
event.currentTarget.focus();
|
||||
try {
|
||||
iframeRef.current?.contentWindow?.focus();
|
||||
} catch {}
|
||||
applyHideScrollbarToIframe(event.currentTarget, hideScrollbar);
|
||||
if (isSameOrigin()) {
|
||||
try {
|
||||
iframeRef.current?.contentWindow?.focus();
|
||||
} catch {}
|
||||
applyHideScrollbarToIframe(event.currentTarget, hideScrollbar);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
} from "@/constants";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useExtensionStore } from "@/stores/extensionStore";
|
||||
|
||||
interface SearchChatProps {
|
||||
isTauri?: boolean;
|
||||
@@ -117,9 +118,10 @@ function SearchChat({
|
||||
windowPositionRef.current = await window.outerPosition();
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
const setWindowSize = useCallback(() => {
|
||||
const { viewExtensionOpened } = useSearchStore.getState();
|
||||
const { viewExtensionOpened } = useExtensionStore.getState();
|
||||
|
||||
if (collapseWindowTimer.current) {
|
||||
clearTimeout(collapseWindowTimer.current);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export const MAIN_WINDOW_LABEL = "main";
|
||||
|
||||
export const SETTINGS_WINDOW_LABEL = "settings";
|
||||
|
||||
export const VIEW_EXTENSION_WINDOW_LABEL = "view_extension";
|
||||
|
||||
export const CHECK_WINDOW_LABEL = "check";
|
||||
|
||||
export const CHAT_WINDOW_LABEL = "chat";
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useExtensionStore } from "@/stores/extensionStore";
|
||||
|
||||
export const useCanNavigateBack = () => {
|
||||
const {
|
||||
goAskAi,
|
||||
visibleExtensionStore,
|
||||
visibleExtensionDetail,
|
||||
viewExtensionOpened,
|
||||
sourceData,
|
||||
} = useSearchStore();
|
||||
|
||||
const { viewExtensionOpened } = useExtensionStore.getState();
|
||||
|
||||
const canNavigateBack = useMemo(() => {
|
||||
return (
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { useKeyPress } from "ahooks";
|
||||
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useKeyPress } from "ahooks";
|
||||
import { useExtensionStore } from "@/stores/extensionStore";
|
||||
import { HISTORY_PANEL_ID } from "@/constants";
|
||||
import { closeHistoryPanel } from "@/utils";
|
||||
|
||||
const useEscape = () => {
|
||||
const { setVisibleContextMenu } = useSearchStore();
|
||||
const viewExtensionOpened = useExtensionStore((state) => state.viewExtensionOpened);
|
||||
|
||||
useKeyPress("esc", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const { visibleContextMenu, viewExtensionOpened } =
|
||||
const { visibleContextMenu } =
|
||||
useSearchStore.getState();
|
||||
|
||||
if (
|
||||
|
||||
@@ -4,7 +4,7 @@ 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";
|
||||
import { useExtensionStore } from "@/stores/extensionStore";
|
||||
|
||||
type WindowSnapshot = {
|
||||
width: number;
|
||||
@@ -14,9 +14,9 @@ type WindowSnapshot = {
|
||||
y: number;
|
||||
};
|
||||
|
||||
export function useViewExtensionWindow() {
|
||||
export function useViewExtensionWindow(opts?: { forceResizable?: boolean }) {
|
||||
const isTauri = useAppStore((state) => state.isTauri);
|
||||
const viewExtensionOpened = useSearchStore((state) => state.viewExtensionOpened);
|
||||
const viewExtensionOpened = useExtensionStore((state) => state.viewExtensionOpened);
|
||||
|
||||
if (viewExtensionOpened == null) {
|
||||
throw new Error(
|
||||
@@ -27,8 +27,9 @@ export function useViewExtensionWindow() {
|
||||
const ui: ViewExtensionUISettingsOrNull = useMemo(() => {
|
||||
return viewExtensionOpened[4] as ViewExtensionUISettingsOrNull;
|
||||
}, [viewExtensionOpened]);
|
||||
const resizable = ui?.resizable;
|
||||
const resizable = opts?.forceResizable ? true : ui?.resizable;
|
||||
const hideScrollbar = ui?.hide_scrollbar ?? true;
|
||||
const detachable = ui?.detachable ?? false;
|
||||
|
||||
const uiWidth = ui && typeof ui.width === "number" ? ui.width : null;
|
||||
const uiHeight = ui && typeof ui.height === "number" ? ui.height : null;
|
||||
@@ -181,7 +182,11 @@ export function useViewExtensionWindow() {
|
||||
|
||||
if (hasExplicitWindowSize) {
|
||||
const nextResizable =
|
||||
ui && typeof ui.resizable === "boolean" ? ui.resizable : true;
|
||||
opts?.forceResizable
|
||||
? true
|
||||
: ui && typeof ui.resizable === "boolean"
|
||||
? ui.resizable
|
||||
: true;
|
||||
await platformAdapter.setWindowSize(uiWidth, uiHeight);
|
||||
await platformAdapter.setWindowResizable(nextResizable);
|
||||
await platformAdapter.centerOnCurrentMonitor();
|
||||
@@ -234,6 +239,7 @@ export function useViewExtensionWindow() {
|
||||
return {
|
||||
ui,
|
||||
resizable,
|
||||
detachable,
|
||||
hideScrollbar,
|
||||
scale,
|
||||
iframeRef,
|
||||
|
||||
@@ -647,7 +647,8 @@
|
||||
"enter": "Enter Full Screen",
|
||||
"exit": "Exit Full Screen"
|
||||
},
|
||||
"focus": "Focus"
|
||||
"focus": "Focus",
|
||||
"detach": "Detach"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"button": {
|
||||
|
||||
@@ -646,7 +646,8 @@
|
||||
"enter": "进入全屏",
|
||||
"exit": "退出全屏"
|
||||
},
|
||||
"focus": "聚焦"
|
||||
"focus": "聚焦",
|
||||
"detach": "分离"
|
||||
},
|
||||
"deleteDialog": {
|
||||
"button": {
|
||||
|
||||
@@ -6,12 +6,13 @@ import { useSyncStore } from "@/hooks/useSyncStore";
|
||||
import UpdateApp from "@/components/UpdateApp";
|
||||
import Synthesize from "@/components/Assistant/Synthesize";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useExtensionStore } from "@/stores/extensionStore";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
function MainApp() {
|
||||
const { setIsTauri } = useAppStore();
|
||||
const { setViewExtensionOpened } = useSearchStore();
|
||||
|
||||
const setViewExtensionOpened = useExtensionStore((state) => state.setViewExtensionOpened);
|
||||
|
||||
useEffect(() => {
|
||||
setIsTauri(true);
|
||||
|
||||
13
src/pages/view-extension/index.tsx
Normal file
13
src/pages/view-extension/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
|
||||
import ViewExtension from "@/components/Search/ViewExtension";
|
||||
|
||||
const ViewExtensionPage: React.FC = () => {
|
||||
return (
|
||||
<div className="w-screen h-screen bg-background text-foreground overflow-hidden">
|
||||
<ViewExtension showFullscreen={false} showDetach={false} showFocus forceResizable />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewExtensionPage;
|
||||
@@ -9,6 +9,7 @@ const SettingsPage = lazy(() => import("@/pages/settings/index"));
|
||||
const StandaloneChat = lazy(() => import("@/pages/chat/index"));
|
||||
const CheckPage = lazy(() => import("@/pages/check/index"));
|
||||
const SelectionWindow = lazy(() => import("@/pages/selection/index"));
|
||||
const ViewExtensionPage = lazy(() => import("@/pages/view-extension/index"));
|
||||
|
||||
const routerOptions = {
|
||||
basename: "/",
|
||||
@@ -30,6 +31,7 @@ export const router = createBrowserRouter(
|
||||
{ path: "/ui/chat", element: (<Suspense fallback={<></>}><StandaloneChat /></Suspense>) },
|
||||
{ path: "/ui/check", element: (<Suspense fallback={<></>}><CheckPage /></Suspense>) },
|
||||
{ path: "/ui/selection", element: (<Suspense fallback={<></>}><SelectionWindow /></Suspense>) },
|
||||
{ path: "/ui/view-extension", element: (<Suspense fallback={<></>}><ViewExtensionPage /></Suspense>) },
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useIconfontScript } from "@/hooks/useScript";
|
||||
import { Extension } from "@/components/Settings/Extensions";
|
||||
import { useExtensionsStore } from "@/stores/extensionsStore";
|
||||
import { useSelectionStore, startSelectionStorePersistence } from "@/stores/selectionStore";
|
||||
import { startExtensionStorePersistence } from "@/stores/extensionStore";
|
||||
import { useServers } from "@/hooks/useServers";
|
||||
import { useDeepLinkManager } from "@/hooks/useDeepLinkManager";
|
||||
// import { useSelectionWindow } from "@/hooks/useSelectionWindow";
|
||||
@@ -34,6 +35,9 @@ export default function LayoutOutlet() {
|
||||
useMount(() => {
|
||||
startSelectionStorePersistence();
|
||||
});
|
||||
useMount(() => {
|
||||
startExtensionStorePersistence();
|
||||
});
|
||||
|
||||
// init servers isTauri
|
||||
useServers();
|
||||
|
||||
47
src/stores/extensionStore.ts
Normal file
47
src/stores/extensionStore.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { create } from "zustand";
|
||||
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
import {
|
||||
ExtensionPermission,
|
||||
ViewExtensionUISettings,
|
||||
} from "@/components/Settings/Extensions";
|
||||
|
||||
export type ViewExtensionOpened = [
|
||||
// Extension name
|
||||
string,
|
||||
// An absolute path to the extension icon or a font code.
|
||||
string,
|
||||
// HTML file URL
|
||||
string,
|
||||
ExtensionPermission | null,
|
||||
ViewExtensionUISettings | null
|
||||
];
|
||||
|
||||
type ExtensionStore = {
|
||||
// When we open a View extension, we set this to a non-null value.
|
||||
viewExtensionOpened?: ViewExtensionOpened;
|
||||
setViewExtensionOpened: (showViewExtension?: ViewExtensionOpened) => void;
|
||||
}
|
||||
|
||||
// A Zustand store, like any other.
|
||||
export const useExtensionStore = create<ExtensionStore>((set) => ({
|
||||
setViewExtensionOpened: (viewExtensionOpened) => {
|
||||
return set({ viewExtensionOpened });
|
||||
},
|
||||
}));
|
||||
|
||||
/**
|
||||
* Initialize Extension store persistence on Tauri only.
|
||||
* In Web mode, this is a no-op to avoid loading Tauri-specific plugins.
|
||||
*
|
||||
* Returns a promise that resolves when persistence has been started on Tauri.
|
||||
*/
|
||||
export async function startExtensionStorePersistence(): Promise<void> {
|
||||
if (!platformAdapter.isTauri()) return;
|
||||
|
||||
const { createTauriStore } = await import("@tauri-store/zustand");
|
||||
createTauriStore("extension-store", useExtensionStore, {
|
||||
saveOnChange: true,
|
||||
autoStart: true,
|
||||
});
|
||||
}
|
||||
@@ -1,23 +1,10 @@
|
||||
import { SearchExtensionItem } from "@/components/Search/ExtensionStore";
|
||||
import {
|
||||
ExtensionPermission,
|
||||
ViewExtensionUISettings,
|
||||
} from "@/components/Settings/Extensions";
|
||||
import { AggregationBucket, Aggregations } from "@/types/search";
|
||||
import { DateRange } from "react-day-picker";
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
export type ViewExtensionOpened = [
|
||||
// Extension name
|
||||
string,
|
||||
// An absolute path to the extension icon or a font code.
|
||||
string,
|
||||
// HTML file URL
|
||||
string,
|
||||
ExtensionPermission | null,
|
||||
ViewExtensionUISettings | null
|
||||
];
|
||||
|
||||
|
||||
export interface AggregateFilter {
|
||||
[key: string]: AggregationBucket[];
|
||||
@@ -67,10 +54,6 @@ export type ISearchStore = {
|
||||
visibleExtensionDetail: boolean;
|
||||
setVisibleExtensionDetail: (visibleExtensionDetail: boolean) => void;
|
||||
|
||||
// When we open a View extension, we set this to a non-null value.
|
||||
viewExtensionOpened?: ViewExtensionOpened;
|
||||
setViewExtensionOpened: (showViewExtension?: ViewExtensionOpened) => void;
|
||||
|
||||
enabledFuzzyMatch: boolean;
|
||||
setEnabledFuzzyMatch: (enabledFuzzyMatch: boolean) => void;
|
||||
|
||||
@@ -161,9 +144,6 @@ export const useSearchStore = create<ISearchStore>()(
|
||||
setVisibleExtensionDetail: (visibleExtensionDetail) => {
|
||||
return set({ visibleExtensionDetail });
|
||||
},
|
||||
setViewExtensionOpened: (viewExtensionOpened) => {
|
||||
return set({ viewExtensionOpened });
|
||||
},
|
||||
enabledFuzzyMatch: false,
|
||||
setEnabledFuzzyMatch: (enabledFuzzyMatch) => {
|
||||
return set({ enabledFuzzyMatch });
|
||||
|
||||
@@ -7,7 +7,7 @@ import { AppTheme } from "@/types/index";
|
||||
import { SearchDocument } from "./search";
|
||||
import { IAppStore } from "@/stores/appStore";
|
||||
import type { Server } from "@/types/server";
|
||||
import { ViewExtensionOpened } from "@/stores/searchStore";
|
||||
import { ViewExtensionOpened } from "@/stores/extensionStore";
|
||||
|
||||
export interface EventPayloads {
|
||||
"theme-changed": string;
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from "lodash-es";
|
||||
import { filesize as filesizeLib } from "filesize";
|
||||
import i18next from "i18next";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import platformAdapter from "./platformAdapter";
|
||||
import { useAppStore } from "@/stores/appStore";
|
||||
@@ -17,8 +18,8 @@ import { DEFAULT_COCO_SERVER_ID, HISTORY_PANEL_ID } from "@/constants";
|
||||
import { useChatStore } from "@/stores/chatStore";
|
||||
import { getCurrentWindowService } from "@/commands/windowService";
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import { useExtensionStore } from "@/stores/extensionStore";
|
||||
import { MultiSourceQueryResponse } from "@/types/search";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
export async function copyToClipboard(text: string, noTip = false) {
|
||||
const addError = useAppStore.getState().addError;
|
||||
@@ -229,9 +230,9 @@ export const canNavigateBack = () => {
|
||||
goAskAi,
|
||||
visibleExtensionStore,
|
||||
visibleExtensionDetail,
|
||||
viewExtensionOpened,
|
||||
sourceData,
|
||||
} = useSearchStore.getState();
|
||||
const { viewExtensionOpened } = useExtensionStore.getState();
|
||||
|
||||
return (
|
||||
goAskAi ||
|
||||
@@ -247,14 +248,14 @@ export const navigateBack = () => {
|
||||
goAskAi,
|
||||
visibleExtensionStore,
|
||||
visibleExtensionDetail,
|
||||
viewExtensionOpened,
|
||||
setGoAskAi,
|
||||
setVisibleExtensionDetail,
|
||||
setVisibleExtensionStore,
|
||||
setSourceData,
|
||||
setViewExtensionOpened,
|
||||
} = useSearchStore.getState();
|
||||
|
||||
const { viewExtensionOpened, setViewExtensionOpened } = useExtensionStore.getState();
|
||||
|
||||
if (goAskAi) {
|
||||
return setGoAskAi(false);
|
||||
}
|
||||
@@ -304,8 +305,9 @@ export const dispatchEvent = (
|
||||
};
|
||||
|
||||
export const visibleSearchBar = () => {
|
||||
const { viewExtensionOpened, visibleExtensionDetail } =
|
||||
useSearchStore.getState();
|
||||
const { visibleExtensionDetail } = useSearchStore.getState();
|
||||
|
||||
const { viewExtensionOpened } = useExtensionStore.getState();
|
||||
|
||||
if (visibleExtensionDetail) return false;
|
||||
|
||||
@@ -318,12 +320,13 @@ export const visibleSearchBar = () => {
|
||||
|
||||
export const visibleFilterBar = () => {
|
||||
const {
|
||||
viewExtensionOpened,
|
||||
visibleExtensionStore,
|
||||
visibleExtensionDetail,
|
||||
goAskAi,
|
||||
} = useSearchStore.getState();
|
||||
|
||||
const { viewExtensionOpened } = useExtensionStore.getState();
|
||||
|
||||
if (visibleExtensionStore || visibleExtensionDetail || goAskAi) return false;
|
||||
|
||||
if (isNil(viewExtensionOpened)) return true;
|
||||
@@ -334,7 +337,7 @@ export const visibleFilterBar = () => {
|
||||
};
|
||||
|
||||
export const visibleFooterBar = () => {
|
||||
const { viewExtensionOpened } = useSearchStore.getState();
|
||||
const { viewExtensionOpened } = useExtensionStore.getState();
|
||||
|
||||
if (isNil(viewExtensionOpened)) return true;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user