diff --git a/public/locales/en-US/application.json b/public/locales/en-US/application.json index cf3a679..9264bd9 100644 --- a/public/locales/en-US/application.json +++ b/public/locales/en-US/application.json @@ -486,7 +486,7 @@ "shareLinkPasswordInfo": "Password: {{password}}", "createShareLink": "Create share link", "privateShare": "Protect with password", - "privateShareDes": "If selected, password is required to access the share link, others cannot see this share link on your homepage.", + "privateShareDes": "If selected, password is required to access the share link.", "useCustomPassword": "Custom share link password", "shareView": "Share view setting", "shareViewDes": "If selected, other users can see your view setting (layout, sorting, etc.) saved on the server server when accessing this shared folder.", @@ -787,6 +787,13 @@ "regTime": "Sign up date", "security": "Password and security", "profilePage": "Public profile", + "publicShareOnly": "Public share only", + "publicShareOnlyDes": "Only show shares without password on the profile page.", + "allShare": "All shares", + "allShareDes": "Show all shares on the profile page (including password-protected shares). Users still need to enter a password to access them.", + "hideShare": "Hide all shares", + "hideShareDes": "Hide all shares on the profile page.", + "userHideShare": "User has hidden the share list", "accountPassword": "Password", "2fa": "2FA authentication", "enabled": "Enabled", diff --git a/public/locales/ja-JP/application.json b/public/locales/ja-JP/application.json index ee8eee1..a4a83b8 100644 --- a/public/locales/ja-JP/application.json +++ b/public/locales/ja-JP/application.json @@ -486,7 +486,7 @@ "shareLinkPasswordInfo": "パスワード:{{password}}", "createShareLink": "共有リンク作成", "privateShare": "パスワードで保護", - "privateShareDes": "チェックを入れると、パスワードが必要です。他の人はあなたのプロフィールページでこの共有リンクを見ることができません。", + "privateShareDes": "チェックを入れると、パスワードが必要です。", "useCustomPassword": "カスタムパスワードを使用", "expireAfterDownload": "ダウンロード後に自動的に期限切れ", "sharePassword": "共有パスワード", @@ -791,6 +791,13 @@ "regTime": "登録日時", "security": "パスワードとセキュリティ", "profilePage": "マイページ", + "publicShareOnly": "パスワードなし共有のみ表示", + "publicShareOnlyDes": "マイページにパスワードなし共有のみ表示します。", + "allShare": "すべての共有", + "allShareDes": "マイページにすべての共有を表示します。パスワードが設定された共有には、パスワードを入力してアクセスする必要があります。", + "hideShare": "すべての共有を非表示", + "hideShareDes": "マイページにすべての共有を非表示にします。", + "userHideShare": "ユーザーが共有リストを非表示にしました", "accountPassword": "ログインパスワード", "2fa": "2段階認証", "enabled": "有効", diff --git a/public/locales/zh-CN/application.json b/public/locales/zh-CN/application.json index bd7c731..712041f 100644 --- a/public/locales/zh-CN/application.json +++ b/public/locales/zh-CN/application.json @@ -486,7 +486,7 @@ "shareLinkPasswordInfo": " 密码: {{password}}", "createShareLink": "创建分享链接", "privateShare": "使用密码保护链接", - "privateShareDes": "勾选后,需要使用密码访问分享链接,其他人无法在你的个人主页看到此分享链接。", + "privateShareDes": "勾选后,需要使用密码访问分享链接。", "useCustomPassword": "自定义分享密码", "expireAfterDownload": "下载后自动过期", "sharePassword": "分享密码", @@ -791,6 +791,13 @@ "regTime": "注册时间", "security": "密码和安全", "profilePage": "个人主页", + "publicShareOnly": "仅展示无密码分享链接", + "publicShareOnlyDes": "仅在个人主页展示没有设置密码的分享链接。", + "allShare": "所有分享", + "allShareDes": "在个人主页展示所有分享链接(包括有密码的分享)。对于有密码的分享,用户还需要输入密码才能访问。", + "hideShare": "隐藏所有分享链接", + "hideShareDes": "在个人主页隐藏所有分享链接。", + "userHideShare": "用户隐藏了分享链接列表", "accountPassword": "登录密码", "2fa": "二步验证", "enabled": "已开启", diff --git a/public/locales/zh-TW/application.json b/public/locales/zh-TW/application.json index 359ddc6..21b38a4 100644 --- a/public/locales/zh-TW/application.json +++ b/public/locales/zh-TW/application.json @@ -482,7 +482,7 @@ "shareLinkPasswordInfo": " 密碼: {{password}}", "createShareLink": "建立分享連結", "privateShare": "使用密碼保護連結", - "privateShareDes": "勾選後,需要使用密碼訪問分享連結,其他人無法在你的個人主頁看到此分享連結。", + "privateShareDes": "勾選後,需要使用密碼訪問分享連結。", "useCustomPassword": "使用自定義密碼", "expireAfterDownload": "下載後自動過期", "sharePassword": "分享密碼", @@ -787,6 +787,13 @@ "regTime": "注冊時間", "security": "密碼和安全", "profilePage": "個人主頁", + "publicShareOnly": "僅展示無密碼分享連結", + "publicShareOnlyDes": "僅在個人主頁展示沒有設置密碼的分享連結。", + "allShare": "所有分享", + "allShareDes": "在個人主頁展示所有分享連結(包括有密碼的分享)。對於有密碼的分享,用戶還需要輸入密碼才能訪問。", + "hideShare": "隱藏所有分享連結", + "hideShareDes": "在個人主頁隱藏所有分享連結。", + "userHideShare": "用戶隱藏了分享連結列表", "accountPassword": "登入密碼", "2fa": "二步驗證", "enabled": "已開啟", diff --git a/src/api/explorer.ts b/src/api/explorer.ts index 7cb9711..65a6fb6 100644 --- a/src/api/explorer.ts +++ b/src/api/explorer.ts @@ -83,6 +83,7 @@ export interface Share { downloaded: number; expired?: boolean; unlocked: boolean; + password_protected: boolean; source_type?: number; owner: User; source_uri?: string; diff --git a/src/api/user.ts b/src/api/user.ts index faa1c2b..ffcaabb 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -23,6 +23,7 @@ export interface User { pined?: PinedFile[]; language?: string; disable_view_sync?: boolean; + share_links_in_profile?: ShareLinksInProfileLevel; } export interface Group { id: string; @@ -102,6 +103,7 @@ export interface UserSettings { two_fa_enabled: boolean; passkeys?: Passkey[]; disable_view_sync: boolean; + share_links_in_profile: ShareLinksInProfileLevel; } export interface PatchUserSetting { @@ -116,6 +118,7 @@ export interface PatchUserSetting { two_fa_enabled?: boolean; two_fa_code?: string; disable_view_sync?: boolean; + share_links_in_profile?: ShareLinksInProfileLevel; } export interface PasskeyCredentialOption { @@ -191,3 +194,9 @@ export interface ResetPasswordService { password: string; secret: string; } + +export enum ShareLinksInProfileLevel { + public_share_only = "", + all_share = "all_share", + hide_share = "hide_share", +} diff --git a/src/component/Icons/EyeOff.tsx b/src/component/Icons/EyeOff.tsx new file mode 100644 index 0000000..3269008 --- /dev/null +++ b/src/component/Icons/EyeOff.tsx @@ -0,0 +1,9 @@ +import { SvgIcon, SvgIconProps } from "@mui/material"; + +export default function EyeOff(props: SvgIconProps) { + return ( + + + + ); +} diff --git a/src/component/Pages/Profile/Profile.tsx b/src/component/Pages/Profile/Profile.tsx index 7527bbd..bcf75bd 100644 --- a/src/component/Pages/Profile/Profile.tsx +++ b/src/component/Pages/Profile/Profile.tsx @@ -12,7 +12,7 @@ import { DenseSelect } from "../../Common/StyledComponents.tsx"; import { SquareMenuItem } from "../../FileManager/ContextMenu/ContextMenu.tsx"; import ShareCard from "../Shares/ShareCard.tsx"; import { useParams } from "react-router-dom"; -import { User } from "../../../api/user.ts"; +import { ShareLinksInProfileLevel, User } from "../../../api/user.ts"; import { loadUserInfo } from "../../../redux/thunks/session.ts"; import { UserProfile } from "../../Common/User/UserPopover.tsx"; import PageContainer from "../PageContainer.tsx"; @@ -110,31 +110,36 @@ const Profile = () => { - - - - {t("application:share.createdAtDesc")} - - - - - {t("application:share.createdAtAsc")} - - - - + user && + user.share_links_in_profile !== ShareLinksInProfileLevel.hide_share && ( + + + + + {t("application:share.createdAtDesc")} + + + + + {t("application:share.createdAtAsc")} + + + + + ) } skipChangingDocumentTitle - onRefresh={() => refresh()} + onRefresh={ + user && user.share_links_in_profile !== ShareLinksInProfileLevel.hide_share ? () => refresh() : undefined + } loading={loading} title={t("application:share.somebodyShare", { name: user?.nickname ?? "-", @@ -142,19 +147,28 @@ const Profile = () => { /> - {shares.map((share) => ( - - ))} - {nextPageToken != undefined && ( + {user && + user.share_links_in_profile !== ShareLinksInProfileLevel.hide_share && + shares.map((share) => )} + {nextPageToken != undefined && + user && + user?.share_links_in_profile !== ShareLinksInProfileLevel.hide_share && ( + <> + {[...Array(4)].map((_, i) => ( + + ))} + + )} + {user && user.share_links_in_profile === ShareLinksInProfileLevel.hide_share && ( <> - {[...Array(4)].map((_, i) => ( - - ))} + + + )} diff --git a/src/component/Pages/Setting/ProfileSetting.tsx b/src/component/Pages/Setting/ProfileSetting.tsx index 013a0c8..62d646f 100644 --- a/src/component/Pages/Setting/ProfileSetting.tsx +++ b/src/component/Pages/Setting/ProfileSetting.tsx @@ -1,14 +1,17 @@ import { LoadingButton } from "@mui/lab"; -import { Collapse, Grid2, Stack, Typography, useMediaQuery, useTheme } from "@mui/material"; +import { Collapse, Grid2, Stack, Typography, useMediaQuery, useTheme, styled } from "@mui/material"; +import { bindPopover, bindTrigger, usePopupState } from "material-ui-popup-state/hooks"; import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { sendUpdateUserSetting } from "../../../api/api.ts"; -import { UserSettings } from "../../../api/user.ts"; +import { UserSettings, ShareLinksInProfileLevel } from "../../../api/user.ts"; import { useAppDispatch } from "../../../redux/hooks.ts"; import SessionManager from "../../../session"; -import { DenseFilledTextField } from "../../Common/StyledComponents.tsx"; +import { DefaultButton, DenseFilledTextField } from "../../Common/StyledComponents.tsx"; import TimeBadge from "../../Common/TimeBadge.tsx"; +import CaretDown from "../../Icons/CaretDown.tsx"; import AvatarSetting from "./AvatarSetting.tsx"; +import ProfileSettingPopover, { useProfileSettingSummary } from "./ProfileSettingPopover.tsx"; import SettingForm from "./SettingForm.tsx"; export interface ProfileSettingProps { @@ -16,6 +19,18 @@ export interface ProfileSettingProps { setSetting: (setting: UserSettings) => void; } +const ProfileDropButton = styled(DefaultButton)(({ theme }) => ({ + color: theme.palette.text.secondary, + minWidth: 0, + minHeight: 0, + fontSize: theme.typography.body2.fontSize, + fontWeight: theme.typography.body2.fontWeight, + borderRadius: "4px", + padding: "0px 4px", + position: "relative", + left: "-4px", +})); + const ProfileSetting = ({ setting, setSetting }: ProfileSettingProps) => { const { t } = useTranslation(); const theme = useTheme(); @@ -26,6 +41,14 @@ const ProfileSetting = ({ setting, setSetting }: ProfileSettingProps) => { const user = SessionManager.currentLoginOrNull(); const [nick, setNick] = useState(user?.user.nickname); const [nickLoading, setNickLoading] = useState(false); + const [profileSettingLoading, setProfileSettingLoading] = useState(false); + + const profileSettingPopup = usePopupState({ + variant: "popover", + popupId: "profileSetting", + }); + + const profileSettingSummary = useProfileSettingSummary(setting.share_links_in_profile); const onClick = () => { // Validate input length @@ -49,6 +72,21 @@ const ProfileSetting = ({ setting, setSetting }: ProfileSettingProps) => { } }; + const onProfileSettingChange = (value: ShareLinksInProfileLevel) => { + setProfileSettingLoading(true); + dispatch(sendUpdateUserSetting({ share_links_in_profile: value })) + .then(() => { + setSetting({ + ...setting, + share_links_in_profile: value, + }); + profileSettingPopup.close(); + }) + .finally(() => { + setProfileSettingLoading(false); + }); + }; + return ( { - - - {user?.user.group?.name} - - + + + + {user?.user.group?.name} + + + + + } + disabled={profileSettingLoading} + > + {profileSettingSummary} + + + + diff --git a/src/component/Pages/Setting/ProfileSettingPopover.tsx b/src/component/Pages/Setting/ProfileSettingPopover.tsx new file mode 100644 index 0000000..7f31542 --- /dev/null +++ b/src/component/Pages/Setting/ProfileSettingPopover.tsx @@ -0,0 +1,127 @@ +import { + Divider, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + Popover, + PopoverProps, + SvgIconProps, +} from "@mui/material"; +import { useMemo } from "react"; +import { useTranslation } from "react-i18next"; +import { ShareLinksInProfileLevel } from "../../../api/user.ts"; +import Eye from "../../Icons/Eye.tsx"; +import EyeOff from "../../Icons/EyeOff.tsx"; +import Globe from "../../Icons/Globe.tsx"; + +export interface ProfileSettingPopoverProps extends PopoverProps { + currentValue: ShareLinksInProfileLevel; + onValueChange?: (value: ShareLinksInProfileLevel) => void; + readOnly?: boolean; +} + +const profileSettingOptions: { + value: ShareLinksInProfileLevel; + label: string; + description: string; + icon?: ((props: SvgIconProps) => JSX.Element) | typeof Eye; +}[] = [ + { + value: ShareLinksInProfileLevel.public_share_only, + label: "application:setting.publicShareOnly", + description: "application:setting.publicShareOnlyDes", + icon: Eye, + }, + { + value: ShareLinksInProfileLevel.all_share, + label: "application:setting.allShare", + description: "application:setting.allShareDes", + icon: Globe, + }, + { + value: ShareLinksInProfileLevel.hide_share, + label: "application:setting.hideShare", + description: "application:setting.hideShareDes", + icon: EyeOff, + }, +]; + +export const useProfileSettingSummary = (value: ShareLinksInProfileLevel) => { + const { t } = useTranslation(); + + const summary = useMemo(() => { + const option = profileSettingOptions.find((opt) => opt.value === (value ?? "")); + return option ? t(option.label) : t("application:setting.publicShareOnly"); + }, [value, t]); + + return summary; +}; + +const ProfileSettingPopover = ({ currentValue, onValueChange, readOnly, ...rest }: ProfileSettingPopoverProps) => { + const { t } = useTranslation(); + + const handleChange = (value: ShareLinksInProfileLevel) => () => { + if (onValueChange && !readOnly) { + onValueChange(value); + } + }; + + return ( + + + {profileSettingOptions.map((option, index) => ( + <> + + + {option.icon && ( + + (currentValue === option.value ? theme.palette.primary.main : "inherit"), + }} + /> + + )} + + + + {index < profileSettingOptions.length - 1 && } + + ))} + + + + ); +}; + +export default ProfileSettingPopover; diff --git a/src/component/Pages/Shares/ShareCard.tsx b/src/component/Pages/Shares/ShareCard.tsx index dc09b9c..0853e70 100644 --- a/src/component/Pages/Shares/ShareCard.tsx +++ b/src/component/Pages/Shares/ShareCard.tsx @@ -32,6 +32,7 @@ import Clipboard from "../../Icons/Clipboard.tsx"; import DeleteOutlined from "../../Icons/DeleteOutlined.tsx"; import Eye from "../../Icons/Eye.tsx"; import LinkEdit from "../../Icons/LinkEdit.tsx"; +import LockClosedOutlined from "../../Icons/LockClosedOutlined.tsx"; import Open from "../../Icons/Open.tsx"; import { SummaryButton } from "../Tasks/TaskCard.tsx"; @@ -209,7 +210,8 @@ const ShareCard = ({ share, onShareDeleted, onLoad, loading }: ShareCardProps) = alignItems: "center", }} > - + + {share && share?.password_protected && } {loading ? : share?.name}