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:
SteveLauC
2025-12-26 15:55:04 +08:00
committed by GitHub
parent 585545c43c
commit e019206fcd
7 changed files with 178 additions and 94 deletions

View File

@@ -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

@@ -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

View File

@@ -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;
};

View 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>
);
}

View 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;
};

View File

@@ -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;

View File

@@ -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,
};
}