mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
feat: platform self-adaptation for file viewer application
This commit is contained in:
parent
2c56116153
commit
8a074de7c8
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -202,6 +202,11 @@
|
|||
"addViewer": "アプリを追加",
|
||||
"viewerGroupTitle": "アプリグループ #{{index}}",
|
||||
"viewerType": "タイプ",
|
||||
"viewerPlatform": "プラットフォーム",
|
||||
"viewerPlatformDes": "対応するプラットフォームを選択し、アプリをそのプラットフォームでのみ表示します。",
|
||||
"viewerPlatformPC": " パソコン",
|
||||
"viewerPlatformMobile": "モバイル",
|
||||
"viewerPlatformAll": "全対応",
|
||||
"displayName": "名称",
|
||||
"displayNameDes": "表示名(i18nextキー対応)",
|
||||
"viewerEnabled": "有効化",
|
||||
|
|
|
|||
|
|
@ -202,6 +202,11 @@
|
|||
"addViewer": "添加应用",
|
||||
"viewerGroupTitle": "应用分组 #{{index}}",
|
||||
"viewerType": "类型",
|
||||
"viewerPlatform": "平台",
|
||||
"viewerPlatformDes": "选择对应的平台,使应用仅在对应平台上展示。",
|
||||
"viewerPlatformPC": "PC端",
|
||||
"viewerPlatformMobile": "移动端",
|
||||
"viewerPlatformAll": "全平台",
|
||||
"displayName": "名称",
|
||||
"displayNameDes": "展示名称,支持 i18next 键值。",
|
||||
"viewerEnabled": "启用",
|
||||
|
|
|
|||
|
|
@ -198,6 +198,11 @@
|
|||
"addViewer": "新增應用",
|
||||
"viewerGroupTitle": "應用分組 #{{index}}",
|
||||
"viewerType": "型別",
|
||||
"viewerPlatform": "平台",
|
||||
"viewerPlatformDes": "選擇對應的平台,讓應用僅在對應平台上展示。",
|
||||
"viewerPlatformPC": "電腦端",
|
||||
"viewerPlatformMobile": "行動端",
|
||||
"viewerPlatformAll": "全平台",
|
||||
"displayName": "名稱",
|
||||
"displayNameDes": "展示名稱,支援 i18next 鍵值。",
|
||||
"viewerEnabled": "啟用",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 || "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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 || "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;
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
ListItemButton,
|
||||
ListItemText,
|
||||
Stack,
|
||||
useTheme,
|
||||
useMediaQuery,
|
||||
} from "@mui/material";
|
||||
import React, { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
|
@ -98,6 +100,9 @@ const OpenWith = () => {
|
|||
const [selectedViewer, setSelectedViewer] = React.useState<Viewer | null>(null);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
|
||||
const theme = useTheme();
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
|
||||
|
||||
const selectorState = useAppSelector((state) => state.globalState.viewerSelector);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -174,21 +179,26 @@ const OpenWith = () => {
|
|||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
{((expanded ? Object.values(ViewersByID) : selectorState?.viewers) ?? emptyViewer).map((viewer) => (
|
||||
<ListItem
|
||||
disablePadding
|
||||
key={viewer.id}
|
||||
onDoubleClick={() => openWith(false, viewer)}
|
||||
onClick={() => onViewerClick(viewer)}
|
||||
>
|
||||
<ListItemButton selected={viewer.id == selectedViewer?.id}>
|
||||
<ListItemAvatar sx={{ minWidth: "48px" }}>
|
||||
<ViewerIcon viewer={viewer} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={t(viewer.display_name)} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
{((expanded ? Object.values(ViewersByID) : selectorState?.viewers) ?? emptyViewer)
|
||||
.filter((viewer) => {
|
||||
const platform = viewer.platform || "all";
|
||||
return platform === "all" || platform === (isMobile ? "mobile" : "pc");
|
||||
})
|
||||
.map((viewer) => (
|
||||
<ListItem
|
||||
disablePadding
|
||||
key={viewer.id}
|
||||
onDoubleClick={() => openWith(false, viewer)}
|
||||
onClick={() => onViewerClick(viewer)}
|
||||
>
|
||||
<ListItemButton selected={viewer.id == selectedViewer?.id}>
|
||||
<ListItemAvatar sx={{ minWidth: "48px" }}>
|
||||
<ViewerIcon viewer={viewer} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={t(viewer.display_name)} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
))}
|
||||
{!expanded && (
|
||||
<ListItem onClick={() => setExpanded(true)} disablePadding>
|
||||
<ListItemButton>
|
||||
|
|
|
|||
Loading…
Reference in New Issue