feat: add compact mode for window (#947)

* feat: add compact mode for window

* docs: update changelog

* feat: add i18n

* refactor: update

* refactor: update
This commit is contained in:
ayangweb
2025-10-27 10:06:19 +08:00
committed by GitHub
parent 3029303e95
commit 4a627cb32e
13 changed files with 167 additions and 34 deletions

View File

@@ -22,6 +22,7 @@ feat(View Extension): page field now accepts HTTP(s) links #925
feat: return sub-exts when extension type exts themselves are matched #928
feat: open quick ai with modifier key + enter #939
feat: allow navigate back when cursor is at the beginning #940
feat: add compact mode for window #947
### 🐛 Bug fix

View File

@@ -310,6 +310,7 @@ export default function ChatInput({
<div className={`w-full relative`}>
<div
ref={containerRef}
id="search-bar"
className={`flex items-center dark:text-[#D8D8D8] rounded-md transition-all relative overflow-hidden`}
>
{lineCount === 1 && renderSearchIcon()}

View File

@@ -165,6 +165,7 @@ const InputControls = ({
return (
<div
id="filter-bar"
data-tauri-drag-region
className="flex justify-between items-center pt-2"
>

View File

@@ -9,7 +9,7 @@ import {
useMemo,
} from "react";
import clsx from "clsx";
import { useMount } from "ahooks";
import { useMount, useMutationObserver } from "ahooks";
import Search from "@/components/Search/Search";
import InputBox from "@/components/Search/InputBox";
@@ -28,10 +28,12 @@ import { useConnectStore } from "@/stores/connectStore";
import { useAppearanceStore } from "@/stores/appearanceStore";
import type { StartPage } from "@/types/chat";
import {
canNavigateBack,
hasUploadingAttachment,
visibleFilterBar,
visibleSearchBar,
} from "@/utils";
import { useTauriFocus } from "@/hooks/useTauriFocus";
interface SearchChatProps {
isTauri?: boolean;
@@ -82,6 +84,7 @@ function SearchChat({
);
const [state, dispatch] = useReducer(appReducer, customInitialState);
const {
isChatMode,
input,
@@ -91,6 +94,52 @@ function SearchChat({
isMCPActive,
isTyping,
} = state;
const inputRef = useRef<string>();
const isChatModeRef = useRef(false);
const setWindowSize = useCallback(() => {
const width = 680;
let height = 590;
const updateAppDialog = document.querySelector("#update-app-dialog");
if (!updateAppDialog && !canNavigateBack() && !inputRef.current) {
const { windowMode } = useAppearanceStore.getState();
if (windowMode === "compact") {
const searchBar = document.querySelector("#search-bar");
const filterBar = document.querySelector("#filter-bar");
if (searchBar && filterBar) {
height = searchBar.clientHeight + filterBar.clientHeight + 16;
} else {
height = 82;
}
height = Math.min(height, 88);
}
}
platformAdapter.setWindowSize(width, height);
}, []);
useMutationObserver(setWindowSize, document.body, {
subtree: true,
childList: true,
});
useEffect(() => {
inputRef.current = input;
isChatModeRef.current = isChatMode;
setWindowSize();
}, [input, isChatMode]);
useTauriFocus({
onFocus: setWindowSize,
});
useEffect(() => {
dispatch({
type: "SET_SEARCH_ACTIVE",
@@ -114,11 +163,6 @@ function SearchChat({
const setTheme = useThemeStore((state) => state.setTheme);
const setIsDark = useThemeStore((state) => state.setIsDark);
const isChatModeRef = useRef(false);
useEffect(() => {
isChatModeRef.current = isChatMode;
}, [isChatMode]);
useMount(async () => {
const isWin10 = await platformAdapter.isWindows10();

View File

@@ -1,9 +1,7 @@
import SettingsInput from "@/components/Settings/SettingsInput";
import SettingsItem from "@/components/Settings/SettingsItem";
import { useAppearanceStore } from "@/stores/appearanceStore";
import platformAdapter from "@/utils/platformAdapter";
import { AppWindowMac } from "lucide-react";
import { useEffect } from "react";
import { useTranslation } from "react-i18next";
const Appearance = () => {
@@ -11,14 +9,6 @@ const Appearance = () => {
const opacity = useAppearanceStore((state) => state.opacity);
const setOpacity = useAppearanceStore((state) => state.setOpacity);
useEffect(() => {
const unlisten = useAppearanceStore.subscribe((state) => {
platformAdapter.emitEvent("change-appearance-store", state);
});
return unlisten;
}, []);
return (
<>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">

View File

@@ -1,4 +1,4 @@
import { useState, useEffect } from "react";
import { useState, useEffect, cloneElement, ReactElement } from "react";
import {
Command,
Monitor,
@@ -9,6 +9,9 @@ import {
Tags,
// Trash2,
Globe,
PictureInPicture2,
PanelTop,
RectangleHorizontal,
} from "lucide-react";
import { useTranslation } from "react-i18next";
import { isTauri } from "@tauri-apps/api/core";
@@ -31,6 +34,8 @@ import {
unregister_shortcut,
} from "@/commands";
import platformAdapter from "@/utils/platformAdapter";
import clsx from "clsx";
import { useAppearanceStore, WindowMode } from "@/stores/appearanceStore";
export function ThemeOption({
icon: Icon,
@@ -76,6 +81,7 @@ export default function GeneralSettings() {
const [launchAtLogin, setLaunchAtLogin] = useState(true);
const { showTooltip, setShowTooltip, language, setLanguage } = useAppStore();
const { windowMode, setWindowMode } = useAppearanceStore();
const fetchAutoStartStatus = async () => {
if (isTauri()) {
@@ -176,6 +182,20 @@ export default function GeneralSettings() {
const currentLanguage = language || i18n.language;
const windowModes: Array<{
icon: ReactElement;
value: WindowMode;
}> = [
{
icon: <PanelTop />,
value: "default",
},
{
icon: <RectangleHorizontal />,
value: "compact",
},
];
return (
<div className="space-y-8">
<div>
@@ -239,6 +259,51 @@ export default function GeneralSettings() {
/>
</div>
<SettingsItem
icon={PictureInPicture2}
title={t("settings.windowMode.title")}
description={t("settings.windowMode.description")}
/>
<div className="grid grid-cols-3 gap-4">
{windowModes.map((item) => {
const { icon, value } = item;
const label = t(`settings.windowMode.${value}`);
let isSelected = value === windowMode;
return (
<button
onClick={() => {
setWindowMode(value);
}}
className={clsx(
"p-4 rounded-lg border-2 border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 flex flex-col items-center justify-center space-y-2 transition-all",
{
"!border-blue-500 bg-blue-50 dark:bg-blue-900/20":
isSelected,
}
)}
title={label}
>
{cloneElement(icon, {
className: clsx({
"text-blue-500": isSelected,
}),
})}
<span
className={clsx(`text-sm font-medium`, {
"text-blue-500": isSelected,
})}
>
{label}
</span>
</button>
);
})}
</div>
<SettingsItem
icon={Globe}
title={t("settings.language.title")}

View File

@@ -4,7 +4,7 @@ interface SettingsItemProps {
icon: LucideIcon;
title: string;
description: string;
children: React.ReactNode;
children?: React.ReactNode;
}
export default function SettingsItem({

View File

@@ -131,9 +131,8 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
const { skipVersions, updateInfo } = useUpdateStore.getState();
if(updateInfo?.version){
if (updateInfo?.version) {
setSkipVersions([...skipVersions, updateInfo.version]);
}
isCheckPage ? hide_check() : setVisible(false);
@@ -143,6 +142,7 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
<Dialog
open={isCheckPage ? true : visible}
as="div"
id="update-app-dialog"
className="relative z-10 focus:outline-none"
onClose={noop}
>
@@ -154,6 +154,7 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
}`}
>
<div
data-tauri-drag-region
className={clsx(
"flex min-h-full items-center justify-center",
!isCheckPage && "p-4"
@@ -161,11 +162,13 @@ const UpdateApp = ({ isCheckPage }: UpdateAppProps) => {
>
<DialogPanel
transition
className={`relative w-[340px] py-8 flex flex-col items-center ${
isCheckPage
? ""
: "rounded-lg bg-white dark:bg-[#333] border border-[#EDEDED] dark:border-black/20 shadow-md"
}`}
className={clsx(
"relative w-[340px] py-8 flex flex-col items-center",
{
"rounded-lg bg-white dark:bg-[#333] border border-[#EDEDED] dark:border-black/20 shadow-md":
!isCheckPage,
}
)}
>
{!isCheckPage && isOptional && (
<X

View File

@@ -117,8 +117,11 @@ export const useSyncStore = () => {
const setShowTooltip = useAppStore((state) => state.setShowTooltip);
const setEndpoint = useAppStore((state) => state.setEndpoint);
const setLanguage = useAppStore((state) => state.setLanguage);
const { setWindowMode } = useAppearanceStore();
const setServerListSilently = useConnectStore((state) => state.setServerListSilently);
const setServerListSilently = useConnectStore(
(state) => state.setServerListSilently
);
useEffect(() => {
if (!resetFixedWindow) {
@@ -182,11 +185,8 @@ export const useSyncStore = () => {
}),
platformAdapter.listenEvent("change-connect-store", ({ payload }) => {
const {
connectionTimeout,
querySourceTimeout,
allowSelfSignature,
} = payload;
const { connectionTimeout, querySourceTimeout, allowSelfSignature } =
payload;
if (isNumber(connectionTimeout)) {
setConnectionTimeout(connectionTimeout);
}
@@ -197,12 +197,13 @@ export const useSyncStore = () => {
}),
platformAdapter.listenEvent("change-appearance-store", ({ payload }) => {
const { opacity, snapshotUpdate } = payload;
const { opacity, snapshotUpdate, windowMode } = payload;
if (isNumber(opacity)) {
setOpacity(opacity);
}
setSnapshotUpdate(snapshotUpdate);
setWindowMode(windowMode);
}),
platformAdapter.listenEvent("change-extensions-store", ({ payload }) => {

View File

@@ -17,6 +17,12 @@
"dark": "Dark",
"auto": "Auto"
},
"windowMode": {
"title": "Window Mode",
"description": "Set how the window appears when opened.",
"default": "Default",
"compact": "Compact"
},
"language": {
"title": "Language",
"description": "Choose your preferred language",

View File

@@ -17,6 +17,12 @@
"dark": "深色",
"auto": "自动"
},
"windowMode": {
"title": "窗口模式",
"description": "设置窗口打开时的显示方式。",
"default": "默认",
"compact": "紧凑"
},
"language": {
"title": "语言",
"description": "选择您的首选语言",

View File

@@ -16,6 +16,7 @@ import { useConnectStore } from "@/stores/connectStore";
import platformAdapter from "@/utils/platformAdapter";
import { useAppStore } from "@/stores/appStore";
import { useExtensionsStore } from "@/stores/extensionsStore";
import { useAppearanceStore } from "@/stores/appearanceStore";
const tabIndexMap: { [key: string]: number } = {
general: 0,
@@ -58,6 +59,10 @@ function SettingsPage() {
platformAdapter.emitEvent("change-app-store", state);
});
const unsubscribeAppearanceStore = useAppearanceStore.subscribe((state) => {
platformAdapter.emitEvent("change-appearance-store", state);
});
const unlisten2 = platformAdapter.listenEvent(
"config-extension",
({ payload }) => {
@@ -70,6 +75,7 @@ function SettingsPage() {
return () => {
unsubscribeConnect();
unsubscribeAppStore();
unsubscribeAppearanceStore();
unlisten.then((fn) => fn());
unlisten2.then((fn) => fn());
};

View File

@@ -1,11 +1,15 @@
import { create } from "zustand";
import { persist, subscribeWithSelector } from "zustand/middleware";
export type WindowMode = "default" | "compact";
export type IAppearanceStore = {
opacity: number;
setOpacity: (opacity?: number) => void;
snapshotUpdate: boolean;
setSnapshotUpdate: (snapshotUpdate: boolean) => void;
windowMode: WindowMode;
setWindowMode: (windowMode: WindowMode) => void;
};
export const useAppearanceStore = create<IAppearanceStore>()(
@@ -20,12 +24,17 @@ export const useAppearanceStore = create<IAppearanceStore>()(
setSnapshotUpdate: (snapshotUpdate) => {
return set({ snapshotUpdate });
},
windowMode: "default",
setWindowMode(windowMode) {
return set({ windowMode });
},
}),
{
name: "startup-store",
partialize: (state) => ({
opacity: state.opacity,
snapshotUpdate: state.snapshotUpdate,
windowMode: state.windowMode,
}),
}
)