From b3b4e54975fc3c47b65191bc4d4f1373c76f6020 Mon Sep 17 00:00:00 2001 From: infinite-persistence Date: Sun, 8 Aug 2021 16:13:35 +0800 Subject: [PATCH] Settings Page Side Navigation All components will have an ID that corresponds to the sidebar link. When clicked, we scroll to the position of the card by searching for the element with the ID. It behaves simiar to # anchor navigation. I like this model mainly because in Mobile, users don't need to keep opening the drawer to navigate -- they just need to scroll. This allows us to use the same design for Mobile and App. --- ui/component/common/card.jsx | 3 + ui/component/common/icon-custom.jsx | 17 ++ ui/component/page/view.jsx | 3 + ui/component/settingAccount/view.jsx | 2 + ui/component/settingAppearance/view.jsx | 2 + ui/component/settingContent/view.jsx | 2 + ui/component/settingSystem/view.jsx | 2 + ui/component/settingsSideNavigation/index.js | 3 + ui/component/settingsSideNavigation/view.jsx | 158 +++++++++++++++++++ ui/constants/icons.js | 2 + ui/constants/settings.js | 7 + ui/page/settings/view.jsx | 76 +++++---- ui/scss/component/_main.scss | 18 +++ 13 files changed, 265 insertions(+), 30 deletions(-) create mode 100644 ui/component/settingsSideNavigation/index.js create mode 100644 ui/component/settingsSideNavigation/view.jsx diff --git a/ui/component/common/card.jsx b/ui/component/common/card.jsx index 29c3f1a9c..1fb606cdb 100644 --- a/ui/component/common/card.jsx +++ b/ui/component/common/card.jsx @@ -10,6 +10,7 @@ type Props = { title?: string | Node, subtitle?: string | Node, titleActions?: string | Node, + id?: string, body?: string | Node, actions?: string | Node, icon?: string, @@ -30,6 +31,7 @@ export default function Card(props: Props) { title, subtitle, titleActions, + id, body, actions, icon, @@ -53,6 +55,7 @@ export default function Card(props: Props) { className={classnames(className, 'card', { 'card__multi-pane': Boolean(secondPane), })} + id={id} onClick={(e) => { if (onClick) { onClick(); diff --git a/ui/component/common/icon-custom.jsx b/ui/component/common/icon-custom.jsx index c9a181801..fcd9e0954 100644 --- a/ui/component/common/icon-custom.jsx +++ b/ui/component/common/icon-custom.jsx @@ -2318,6 +2318,23 @@ export const icons = { ), + [ICONS.APPEARANCE]: buildIcon( + + + + + + + + ), + [ICONS.CONTENT]: buildIcon( + + + + + + + ), [ICONS.STAR]: buildIcon( diff --git a/ui/component/page/view.jsx b/ui/component/page/view.jsx index 3951ad603..76867b415 100644 --- a/ui/component/page/view.jsx +++ b/ui/component/page/view.jsx @@ -23,6 +23,7 @@ type Props = { isUpgradeAvailable: boolean, authPage: boolean, filePage: boolean, + settingsPage?: boolean, noHeader: boolean, noFooter: boolean, noSideNavigation: boolean, @@ -45,6 +46,7 @@ function Page(props: Props) { children, className, filePage = false, + settingsPage, authPage = false, fullWidthPage = false, noHeader = false, @@ -114,6 +116,7 @@ function Page(props: Props) { 'main--full-width': fullWidthPage, 'main--auth-page': authPage, 'main--file-page': filePage, + 'main--settings-page': settingsPage, 'main--markdown': isMarkdown, 'main--theater-mode': isOnFilePage && videoTheaterMode && !livestream, 'main--livestream': livestream && !chatDisabled, diff --git a/ui/component/settingAccount/view.jsx b/ui/component/settingAccount/view.jsx index a1ea5a347..f8a94230e 100644 --- a/ui/component/settingAccount/view.jsx +++ b/ui/component/settingAccount/view.jsx @@ -1,6 +1,7 @@ // @flow import * as ICONS from 'constants/icons'; import * as PAGES from 'constants/pages'; +import { SETTINGS_GRP } from 'constants/settings'; import React from 'react'; import Button from 'component/button'; import Card from 'component/common/card'; @@ -38,6 +39,7 @@ export default function SettingAccount(props: Props) { return ( any, + icon: string, + extra?: Node, +}; + +const SIDE_LINKS: Array = [ + { + title: 'Appearance', + link: `/$/${PAGES.SETTINGS}#${SETTINGS_GRP.APPEARANCE}`, + section: SETTINGS_GRP.APPEARANCE, + icon: ICONS.APPEARANCE, + }, + { + title: 'Account', + link: `/$/${PAGES.SETTINGS}#${SETTINGS_GRP.ACCOUNT}`, + section: SETTINGS_GRP.ACCOUNT, + icon: ICONS.ACCOUNT, + }, + { + title: 'Content settings', + link: `/$/${PAGES.SETTINGS}#${SETTINGS_GRP.CONTENT}`, + section: SETTINGS_GRP.CONTENT, + icon: ICONS.CONTENT, + }, + { + title: 'System', + link: `/$/${PAGES.SETTINGS}#${SETTINGS_GRP.SYSTEM}`, + section: SETTINGS_GRP.SYSTEM, + icon: ICONS.SETTINGS, + }, +]; + +export default function SettingsSideNavigation() { + const sidebarOpen = true; + const isMediumScreen = useIsMediumScreen(); + const isAbsolute = isMediumScreen; + const microNavigation = !sidebarOpen || isMediumScreen; + const { location } = useHistory(); + + // This sidebar could be called from Settings or from a Settings Sub Page. + // - "#" navigation = don't record to history, just scroll. + // - "/" navigation = record sub-page navigation to history. + const scrollInstead = location.pathname === `/$/${PAGES.SETTINGS}`; + + function scrollToSection(section: string) { + const TOP_MARGIN_PX = 20; + const element = document.getElementById(section); + if (element) { + window.scrollTo(0, element.offsetTop - TOP_MARGIN_PX); + } + } + + if (isMediumScreen) { + // I think it's ok to hide it for now on medium/small screens given that + // we are using a scrolling Settings Page that displays everything. If we + // really need this, most likely we can display it as a Tab at the top + // of the page. + return null; + } + + return ( +
+ + + {isMediumScreen && sidebarOpen && ( + <> + + + )} +
+ ); +} diff --git a/ui/constants/icons.js b/ui/constants/icons.js index cfe7d9ff9..6734cde59 100644 --- a/ui/constants/icons.js +++ b/ui/constants/icons.js @@ -163,6 +163,8 @@ export const STACK = 'stack'; export const TIME = 'time'; export const GLOBE = 'globe'; export const RSS = 'rss'; +export const APPEARANCE = 'Appearance'; +export const CONTENT = 'Content'; export const STAR = 'star'; export const MUSIC = 'MusicCategory'; export const BADGE_MOD = 'BadgeMod'; diff --git a/ui/constants/settings.js b/ui/constants/settings.js index ca0e48c31..aadb632a0 100644 --- a/ui/constants/settings.js +++ b/ui/constants/settings.js @@ -26,3 +26,10 @@ export const ENABLE_SYNC = 'enable_sync'; export const TO_TRAY_WHEN_CLOSED = 'to_tray_when_closed'; export const ENABLE_PUBLISH_PREVIEW = 'enable-publish-preview'; export const DESKTOP_WINDOW_ZOOM = 'desktop_window_zoom'; + +export const SETTINGS_GRP = { + APPEARANCE: 'appearance', + ACCOUNT: 'account', + CONTENT: 'content', + SYSTEM: 'system', +}; diff --git a/ui/page/settings/view.jsx b/ui/page/settings/view.jsx index 4f2b98d59..0752896aa 100644 --- a/ui/page/settings/view.jsx +++ b/ui/page/settings/view.jsx @@ -8,6 +8,7 @@ import Page from 'component/page'; import SettingAccount from 'component/settingAccount'; import SettingAppearance from 'component/settingAppearance'; import SettingContent from 'component/settingContent'; +import SettingsSideNavigation from 'component/settingsSideNavigation'; import SettingSystem from 'component/settingSystem'; import SettingUnauthenticated from 'component/settingUnauthenticated'; import Yrbl from 'component/yrbl'; @@ -40,37 +41,52 @@ class SettingsPage extends React.PureComponent { const noDaemonSettings = !daemonSettings || Object.keys(daemonSettings).length === 0; return ( - - {!isAuthenticated && IS_WEB && ( - <> - -
- -
- } - /> - - - )} + + - {!IS_WEB && noDaemonSettings ? ( -
-
{__('Failed to load settings.')}
-
- ) : ( -
- - - - -
- )} +
+ {!isAuthenticated && IS_WEB && ( + <> + +
+ +
+ } + /> +
+ + )} + + {!IS_WEB && noDaemonSettings ? ( +
+
{__('Failed to load settings.')}
+
+ ) : ( +
+ + + + +
+ )} +
); } diff --git a/ui/scss/component/_main.scss b/ui/scss/component/_main.scss index e4814d29a..102237112 100644 --- a/ui/scss/component/_main.scss +++ b/ui/scss/component/_main.scss @@ -221,6 +221,24 @@ } } +.main--settings-page { + width: 100%; + max-width: 70rem; + margin-left: auto; + margin-right: auto; + margin-top: var(--spacing-m); + padding: 0 var(--spacing-m); + + .card__subtitle { + margin: 0 0 var(--spacing-s) 0; + font-size: var(--font-small); + } + + .button--inverse { + color: var(--color-primary); + } +} + .main--markdown { flex-direction: column; }