mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-29 00:24:46 +01:00
feat: new config ui.hide_scrollbar (#1031)
* feat: new config ui.hide_scrollbar * feat: front end supports hide_scrollbar --------- Co-authored-by: rain9 <15911122312@163.com>
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -164,6 +164,9 @@ pub(crate) struct ViewExtensionUISettings {
|
||||
/// Show the footer
|
||||
#[serde_inline_default(false)]
|
||||
footer: bool,
|
||||
/// If true, scrollbars will be hidden
|
||||
#[serde_inline_default(true)]
|
||||
hide_scrollbar: bool,
|
||||
/// The recommended width of the window for this extension
|
||||
width: Option<u32>,
|
||||
/// The recommended heigh of the window for this extension
|
||||
|
||||
@@ -4,13 +4,11 @@ import { useTranslation } from "react-i18next";
|
||||
import { Maximize2, Minimize2, Focus } from "lucide-react";
|
||||
|
||||
import { useSearchStore } from "@/stores/searchStore";
|
||||
import {
|
||||
ExtensionFileSystemPermission,
|
||||
FileSystemAccess,
|
||||
} from "../Settings/Extensions";
|
||||
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();
|
||||
@@ -168,9 +166,10 @@ const ViewExtension: React.FC = () => {
|
||||
}, [reversedApis, permission]); // Add apiPermissions as dependency
|
||||
|
||||
const fileUrl = viewExtensionOpened[2];
|
||||
|
||||
|
||||
const {
|
||||
resizable,
|
||||
hideScrollbar,
|
||||
scale,
|
||||
iframeRef,
|
||||
isFullscreen,
|
||||
@@ -207,95 +206,15 @@ const ViewExtension: React.FC = () => {
|
||||
<Focus className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
className="w-full h-full flex items-center justify-center overflow-hidden"
|
||||
onMouseDownCapture={focusIframe}
|
||||
onPointerDown={focusIframe}
|
||||
onClickCapture={focusIframe}
|
||||
>
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={fileUrl}
|
||||
className="border-0 w-full h-full"
|
||||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "center center",
|
||||
outline: "none",
|
||||
}}
|
||||
allow="fullscreen; pointer-lock; gamepad"
|
||||
allowFullScreen
|
||||
tabIndex={-1}
|
||||
onLoad={(event) => {
|
||||
event.currentTarget.focus();
|
||||
try {
|
||||
iframeRef.current?.contentWindow?.focus();
|
||||
} catch {}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<ViewExtensionIframe
|
||||
fileUrl={fileUrl}
|
||||
scale={scale}
|
||||
iframeRef={iframeRef}
|
||||
hideScrollbar={hideScrollbar}
|
||||
focusIframe={focusIframe}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ViewExtension;
|
||||
|
||||
// Permission check function - TypeScript translation of Rust function
|
||||
const apiPermissionCheck = (
|
||||
category: string,
|
||||
api: string,
|
||||
allowedApis: string[] | null
|
||||
): boolean => {
|
||||
if (!allowedApis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const qualifiedApi = `${category}:${api}`;
|
||||
return allowedApis.some((a) => a === qualifiedApi);
|
||||
};
|
||||
|
||||
const extractFsAccessPattern = (
|
||||
command: string,
|
||||
requestPayload: any
|
||||
): [string, FileSystemAccess] => {
|
||||
switch (command) {
|
||||
case "read_dir": {
|
||||
const { path } = requestPayload;
|
||||
|
||||
return [path, ["read"]];
|
||||
}
|
||||
default: {
|
||||
throw new Error(`unknown command ${command}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fsPermissionCheck = async (
|
||||
command: string,
|
||||
requestPayload: any,
|
||||
fsPermission: ExtensionFileSystemPermission[] | null
|
||||
): Promise<boolean> => {
|
||||
if (!fsPermission) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [path, access] = extractFsAccessPattern(command, requestPayload);
|
||||
const clean_path = await platformAdapter.invokeBackend("path_absolute", {
|
||||
path: path,
|
||||
});
|
||||
|
||||
// Walk through fsPermission array to find matching paths
|
||||
for (const permission of fsPermission) {
|
||||
if (permission.path === clean_path) {
|
||||
// Check if all required access permissions are included in the permission's access array
|
||||
const hasAllRequiredAccess = access.every((requiredAccess) =>
|
||||
permission.access.includes(requiredAccess)
|
||||
);
|
||||
|
||||
if (hasAllRequiredAccess) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
88
src/components/Search/ViewExtensionIframe.tsx
Normal file
88
src/components/Search/ViewExtensionIframe.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
const HIDE_SCROLLBAR_STYLE_ID = "coco-view-extension-hide-scrollbar";
|
||||
|
||||
const applyHideScrollbarToIframe = (
|
||||
iframe: HTMLIFrameElement | null,
|
||||
enabled: boolean
|
||||
) => {
|
||||
if (!iframe) return;
|
||||
|
||||
try {
|
||||
const doc = iframe.contentDocument;
|
||||
if (!doc) return;
|
||||
|
||||
const existing = doc.getElementById(HIDE_SCROLLBAR_STYLE_ID);
|
||||
if (!enabled) {
|
||||
existing?.remove();
|
||||
return;
|
||||
}
|
||||
|
||||
if (existing) return;
|
||||
|
||||
const style = doc.createElement("style");
|
||||
style.id = HIDE_SCROLLBAR_STYLE_ID;
|
||||
style.textContent = `
|
||||
* {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
*::-webkit-scrollbar {
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
}
|
||||
`;
|
||||
|
||||
const parent = doc.head ?? doc.documentElement;
|
||||
parent?.appendChild(style);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
type ViewExtensionIframeProps = {
|
||||
fileUrl: string;
|
||||
scale: number;
|
||||
iframeRef: React.MutableRefObject<HTMLIFrameElement | null>;
|
||||
hideScrollbar: boolean;
|
||||
focusIframe: () => void;
|
||||
};
|
||||
|
||||
export default function ViewExtensionIframe(props: ViewExtensionIframeProps) {
|
||||
const { fileUrl, scale, iframeRef, hideScrollbar, focusIframe } = props;
|
||||
|
||||
useEffect(() => {
|
||||
applyHideScrollbarToIframe(iframeRef.current, hideScrollbar);
|
||||
}, [hideScrollbar, iframeRef]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full h-full flex items-center justify-center overflow-hidden"
|
||||
onMouseDownCapture={focusIframe}
|
||||
onPointerDown={focusIframe}
|
||||
onClickCapture={focusIframe}
|
||||
>
|
||||
<iframe
|
||||
ref={(node) => {
|
||||
iframeRef.current = node;
|
||||
}}
|
||||
src={fileUrl}
|
||||
className="border-0 w-full h-full"
|
||||
scrolling={hideScrollbar ? "no" : "auto"}
|
||||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
transformOrigin: "center center",
|
||||
outline: "none",
|
||||
}}
|
||||
allow="fullscreen; pointer-lock; gamepad"
|
||||
allowFullScreen
|
||||
tabIndex={-1}
|
||||
onLoad={(event) => {
|
||||
event.currentTarget.focus();
|
||||
try {
|
||||
iframeRef.current?.contentWindow?.focus();
|
||||
} catch {}
|
||||
applyHideScrollbarToIframe(event.currentTarget, hideScrollbar);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
72
src/components/Search/viewExtensionPermissions.ts
Normal file
72
src/components/Search/viewExtensionPermissions.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import type { ExtensionFileSystemPermission } from "../Settings/Extensions";
|
||||
import type { FileSystemAccess } from "../Settings/Extensions";
|
||||
import platformAdapter from "@/utils/platformAdapter";
|
||||
|
||||
export const apiPermissionCheck = (
|
||||
category: string,
|
||||
api: string,
|
||||
allowedApis: string[] | null
|
||||
): boolean => {
|
||||
if (!allowedApis) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const qualifiedApi = `${category}:${api}`;
|
||||
return allowedApis.some((a) => a === qualifiedApi);
|
||||
};
|
||||
|
||||
type ReadDirPayload = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
const isReadDirPayload = (payload: unknown): payload is ReadDirPayload => {
|
||||
if (typeof payload !== "object" || payload == null) return false;
|
||||
return typeof (payload as Record<string, unknown>).path === "string";
|
||||
};
|
||||
|
||||
export const extractFsAccessPattern = (
|
||||
command: string,
|
||||
requestPayload: unknown
|
||||
): [string, FileSystemAccess] => {
|
||||
switch (command) {
|
||||
case "read_dir": {
|
||||
if (!isReadDirPayload(requestPayload)) {
|
||||
throw new Error("invalid payload for read_dir");
|
||||
}
|
||||
|
||||
return [requestPayload.path, ["read"]];
|
||||
}
|
||||
default: {
|
||||
throw new Error(`unknown command ${command}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const fsPermissionCheck = async (
|
||||
command: string,
|
||||
requestPayload: unknown,
|
||||
fsPermission: ExtensionFileSystemPermission[] | null
|
||||
): Promise<boolean> => {
|
||||
if (!fsPermission) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [path, access] = extractFsAccessPattern(command, requestPayload);
|
||||
const cleanPath = await platformAdapter.invokeBackend("path_absolute", {
|
||||
path,
|
||||
});
|
||||
|
||||
for (const permission of fsPermission) {
|
||||
if (permission.path === cleanPath) {
|
||||
const hasAllRequiredAccess = access.every((requiredAccess) =>
|
||||
permission.access.includes(requiredAccess)
|
||||
);
|
||||
|
||||
if (hasAllRequiredAccess) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
@@ -75,6 +75,7 @@ export interface ViewExtensionUISettings {
|
||||
search_bar: boolean;
|
||||
filter_bar: boolean;
|
||||
footer: boolean;
|
||||
hide_scrollbar: boolean;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
resizable: boolean;
|
||||
|
||||
@@ -28,6 +28,7 @@ export function useViewExtensionWindow() {
|
||||
return viewExtensionOpened[4] as ViewExtensionUISettingsOrNull;
|
||||
}, [viewExtensionOpened]);
|
||||
const resizable = ui?.resizable;
|
||||
const hideScrollbar = ui?.hide_scrollbar ?? true;
|
||||
|
||||
const uiWidth = ui && typeof ui.width === "number" ? ui.width : null;
|
||||
const uiHeight = ui && typeof ui.height === "number" ? ui.height : null;
|
||||
@@ -233,6 +234,7 @@ export function useViewExtensionWindow() {
|
||||
return {
|
||||
ui,
|
||||
resizable,
|
||||
hideScrollbar,
|
||||
scale,
|
||||
iframeRef,
|
||||
isFullscreen,
|
||||
@@ -240,4 +242,3 @@ export function useViewExtensionWindow() {
|
||||
focusIframe,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user