feat: platform self-adaptation for file viewer application (#276)

* feat: platform self-adaptation for file viewer application

* pref: optimize the default values of viewerPlatform

* perf: moved the device filter logic to redux

* perf: removed unused dependencies
This commit is contained in:
Samler 2025-07-03 14:06:48 +08:00 committed by GitHub
parent 2c56116153
commit 37320610e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 151 additions and 48 deletions

View File

@ -202,6 +202,11 @@
"addViewer": "Add an application",
"viewerGroupTitle": "Application group #{{index}}",
"viewerType": "Type",
"viewerPlatform": "Platform",
"viewerPlatformDes": "Select the corresponding platform to display the application only on that platform.",
"viewerPlatformPC": "Desktop",
"viewerPlatformMobile": "Mobile",
"viewerPlatformAll": "All",
"displayName": "Display name",
"displayNameDes": "Display name to users, support i18next key.",
"viewerEnabled": "Enabled",

View File

@ -202,6 +202,11 @@
"addViewer": "アプリを追加",
"viewerGroupTitle": "アプリグループ #{{index}}",
"viewerType": "タイプ",
"viewerPlatform": "プラットフォーム",
"viewerPlatformDes": "対応するプラットフォームを選択し、アプリをそのプラットフォームでのみ表示します。",
"viewerPlatformPC": " パソコン",
"viewerPlatformMobile": "モバイル",
"viewerPlatformAll": "全対応",
"displayName": "名称",
"displayNameDes": "表示名i18nextキー対応",
"viewerEnabled": "有効化",

View File

@ -202,6 +202,11 @@
"addViewer": "添加应用",
"viewerGroupTitle": "应用分组 #{{index}}",
"viewerType": "类型",
"viewerPlatform": "平台",
"viewerPlatformDes": "选择对应的平台,使应用仅在对应平台上展示。",
"viewerPlatformPC": "PC端",
"viewerPlatformMobile": "移动端",
"viewerPlatformAll": "全平台",
"displayName": "名称",
"displayNameDes": "展示名称,支持 i18next 键值。",
"viewerEnabled": "启用",

View File

@ -198,6 +198,11 @@
"addViewer": "新增應用",
"viewerGroupTitle": "應用分組 #{{index}}",
"viewerType": "型別",
"viewerPlatform": "平台",
"viewerPlatformDes": "選擇對應的平台,讓應用僅在對應平台上展示。",
"viewerPlatformPC": "電腦端",
"viewerPlatformMobile": "行動端",
"viewerPlatformAll": "全平台",
"displayName": "名稱",
"displayNameDes": "展示名稱,支援 i18next 鍵值。",
"viewerEnabled": "啟用",

View File

@ -412,6 +412,12 @@ export const ViewerType = {
custom: "custom",
};
export enum ViewerPlatform {
pc = "pc",
mobile = "mobile",
all = "all",
}
export interface Viewer {
id: string;
type: string;
@ -430,6 +436,7 @@ export interface Viewer {
};
};
templates?: NewFileTemplate[];
platform?: ViewerPlatform;
}
export interface NewFileTemplate {

View File

@ -17,7 +17,7 @@ import Grid from "@mui/material/Grid2";
import { useSnackbar } from "notistack";
import React, { lazy, Suspense, useCallback, useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Viewer, ViewerType } from "../../../../../api/explorer.ts";
import { Viewer, ViewerPlatform, ViewerType } from "../../../../../api/explorer.ts";
import { builtInViewers } from "../../../../../redux/thunks/viewer.ts";
import { isTrueVal } from "../../../../../session/utils.ts";
import CircularProgress from "../../../../Common/CircularProgress.tsx";
@ -110,7 +110,17 @@ interface DraggableTemplateRowProps {
template: any;
}
function DraggableTemplateRow({ i, moveRow, onExtChange, onNameChange, onDelete, isFirst, isLast, extList, template }: DraggableTemplateRowProps) {
function DraggableTemplateRow({
i,
moveRow,
onExtChange,
onNameChange,
onDelete,
isFirst,
isLast,
extList,
template,
}: DraggableTemplateRowProps) {
const ref = React.useRef<HTMLTableRowElement>(null);
const [, drop] = useDrop({
accept: DND_TYPE,
@ -150,11 +160,7 @@ function DraggableTemplateRow({ i, moveRow, onExtChange, onNameChange, onDelete,
hover
>
<NoWrapTableCell>
<DenseSelect
value={template.ext}
required
onChange={onExtChange}
>
<DenseSelect value={template.ext} required onChange={onExtChange}>
{extList.map((ext) => (
<SquareMenuItem value={ext} key={ext}>
<ListItemText slotProps={{ primary: { variant: "body2" } }}>{ext}</ListItemText>
@ -163,12 +169,7 @@ function DraggableTemplateRow({ i, moveRow, onExtChange, onNameChange, onDelete,
</DenseSelect>
</NoWrapTableCell>
<NoWrapTableCell>
<DenseFilledTextField
fullWidth
required
value={template.display_name}
onChange={onNameChange}
/>
<DenseFilledTextField fullWidth required value={template.display_name} onChange={onNameChange} />
</NoWrapTableCell>
<NoWrapTableCell>
<IconButton size={"small"} onClick={onDelete}>
@ -353,6 +354,48 @@ const FileViewerEditDialog = ({ viewer, onChange, open, onClose }: FileViewerEdi
<NoMarginHelperText>{t("settings.maxSizeDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
<SettingForm noContainer lgWidth={6} title={t("settings.viewerPlatform")}>
<FormControl fullWidth>
<DenseSelect
value={viewerShadowed.platform || ViewerPlatform.all}
onChange={(e) =>
setViewerShadowed((v) => ({
...(v as Viewer),
platform: e.target.value as ViewerPlatform,
}))
}
>
<SquareMenuItem value="pc">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformPC")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value="mobile">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformMobile")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value="all">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformAll")}
</ListItemText>
</SquareMenuItem>
</DenseSelect>
<NoMarginHelperText>{t("settings.viewerPlatformDes")}</NoMarginHelperText>
</FormControl>
</SettingForm>
{viewer.type == ViewerType.custom && (
<SettingForm noContainer lgWidth={6}>
<FormControlLabel

View File

@ -124,14 +124,17 @@ const ViewerGroupRow = memo(({ group, index, onDelete, onGroupChange, dndType }:
React.useEffect(() => {
setViewers(group.viewers);
}, [group.viewers]);
const moveRow = useCallback((from: number, to: number) => {
if (from === to) return;
const updated = [...viewers];
const [moved] = updated.splice(from, 1);
updated.splice(to, 0, moved);
setViewers(updated);
onGroupChange({ viewers: updated });
}, [viewers, onGroupChange]);
const moveRow = useCallback(
(from: number, to: number) => {
if (from === to) return;
const updated = [...viewers];
const [moved] = updated.splice(from, 1);
updated.splice(to, 0, moved);
setViewers(updated);
onGroupChange({ viewers: updated });
},
[viewers, onGroupChange],
);
const handleMoveUp = (idx: number) => {
if (idx <= 0) return;
moveRow(idx, idx - 1);
@ -178,10 +181,11 @@ const ViewerGroupRow = memo(({ group, index, onDelete, onGroupChange, dndType }:
<NoWrapTableCell width={100}>{t("settings.viewerType")}</NoWrapTableCell>
<NoWrapTableCell width={200}>{t("settings.displayName")}</NoWrapTableCell>
<NoWrapTableCell width={250}>{t("settings.exts")}</NoWrapTableCell>
<NoWrapTableCell width={150}>{t("settings.viewerPlatform")}</NoWrapTableCell>
<NoWrapTableCell width={100}>{t("settings.newFileAction")}</NoWrapTableCell>
<NoWrapTableCell width={64}>{t("settings.viewerEnabled")}</NoWrapTableCell>
<NoWrapTableCell width={64}>{t("settings.actions")}</NoWrapTableCell>
<NoWrapTableCell width={64}></NoWrapTableCell>
<NoWrapTableCell width={100}>{t("settings.actions")}</NoWrapTableCell>
<NoWrapTableCell width={100}></NoWrapTableCell>
</TableRow>
</TableHead>
<TableBody>

View File

@ -1,9 +1,10 @@
import * as React from "react";
import { memo, useCallback, useState } from "react";
import { Viewer, ViewerType } from "../../../../../api/explorer.ts";
import { Viewer, ViewerPlatform, ViewerType } from "../../../../../api/explorer.ts";
import { useTranslation } from "react-i18next";
import { IconButton, TableRow } from "@mui/material";
import { DenseFilledTextField, NoWrapCell, StyledCheckbox } from "../../../../Common/StyledComponents.tsx";
import { IconButton, TableRow, ListItemText } from "@mui/material";
import { DenseFilledTextField, NoWrapCell, StyledCheckbox, DenseSelect } from "../../../../Common/StyledComponents.tsx";
import { SquareMenuItem } from "../../../../FileManager/ContextMenu/ContextMenu.tsx";
import { ViewerIcon } from "../../../../FileManager/Dialogs/OpenWith.tsx";
import Dismiss from "../../../../Icons/Dismiss.tsx";
import Edit from "../../../../Icons/Edit.tsx";
@ -23,19 +24,7 @@ export interface FileViewerRowProps {
const FileViewerRow = React.memo(
React.forwardRef<HTMLTableRowElement, FileViewerRowProps>(
(
{
viewer,
onChange,
onDelete,
onMoveUp,
onMoveDown,
isFirst,
isLast,
style,
},
ref
) => {
({ viewer, onChange, onDelete, onMoveUp, onMoveDown, isFirst, isLast, style }, ref) => {
const { t } = useTranslation("dashboard");
const [extCached, setExtCached] = useState("");
const [editOpen, setEditOpen] = useState(false);
@ -43,12 +32,7 @@ const FileViewerRow = React.memo(
setEditOpen(false);
}, [setEditOpen]);
return (
<TableRow
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
hover
ref={ref}
style={style}
>
<TableRow sx={{ "&:last-child td, &:last-child th": { border: 0 } }} hover ref={ref} style={style}>
<FileViewerEditDialog viewer={viewer} onChange={onChange} open={editOpen} onClose={onClose} />
<NoWrapCell>
<ViewerIcon viewer={viewer} />
@ -75,6 +59,45 @@ const FileViewerRow = React.memo(
onChange={(e) => setExtCached(e.target.value)}
/>
</NoWrapCell>
<NoWrapCell>
<DenseSelect
value={viewer.platform || ViewerPlatform.all}
onChange={(e) =>
onChange({
...viewer,
platform: e.target.value as ViewerPlatform,
})
}
>
<SquareMenuItem value="pc">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformPC")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value="mobile">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformMobile")}
</ListItemText>
</SquareMenuItem>
<SquareMenuItem value="all">
<ListItemText
slotProps={{
primary: { variant: "body2" },
}}
>
{t("settings.viewerPlatformAll")}
</ListItemText>
</SquareMenuItem>
</DenseSelect>
</NoWrapCell>
<NoWrapCell>
{viewer.templates?.length ? t("settings.nMapping", { num: viewer.templates?.length }) : t("share.none")}
</NoWrapCell>
@ -121,8 +144,8 @@ const FileViewerRow = React.memo(
</NoWrapCell>
</TableRow>
);
}
)
},
),
);
export default FileViewerRow;

View File

@ -2,7 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { SiteConfig } from "../api/site.ts";
import { ExpandedIconSettings, FileTypeIconSetting } from "../component/FileManager/Explorer/FileTypeIcon.tsx";
import { ExpandedViewerSetting } from "./thunks/viewer.ts";
import { Viewer } from "../api/explorer.ts";
import { Viewer, ViewerPlatform } from "../api/explorer.ts";
declare global {
interface Window {
@ -68,12 +68,18 @@ const preProcessors: {
Viewers = {};
ViewersByID = {};
const isMobile = window.matchMedia("(max-width: 768px)").matches;
config.file_viewers?.forEach((group) => {
group.viewers.forEach((viewer) => {
if (viewer.disabled) {
return;
}
const platform = viewer.platform || ViewerPlatform.all;
if (platform !== ViewerPlatform.all && platform !== (isMobile ? ViewerPlatform.mobile : ViewerPlatform.pc)) {
return;
}
ViewersByID[viewer.id] = viewer;
const simplified: Viewer = viewer;
viewer.exts.forEach((ext) => {