diff --git a/src/App.tsx b/src/App.tsx index 95554fbda1..bf1c17a9f6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -27,6 +27,7 @@ import { AppContextProvider } from "./features/auth/AppContext"; import Router from "./Router"; import BeforeInstallPromptProvider from "./BeforeInstallPromptProvider"; import { UpdateContextProvider } from "./pages/settings/update/UpdateContext"; +import GlobalStyles from "./GlobalStyles"; setupIonicReact({ rippleEffect: false, @@ -39,6 +40,7 @@ export default function App() { return ( + diff --git a/src/GlobalStyles.tsx b/src/GlobalStyles.tsx new file mode 100644 index 0000000000..a5e9c6243a --- /dev/null +++ b/src/GlobalStyles.tsx @@ -0,0 +1,30 @@ +import { Global, css } from "@emotion/react"; +import { useAppSelector } from "./store"; + +export default function GlobalStyles() { + const { fontSizeMultiplier, useSystemFontSize } = useAppSelector( + (state) => state.appearance.font + ); + + const baseFontStyles = useSystemFontSize + ? css` + font: -apple-system-body; + ` + : css` + font-size: ${fontSizeMultiplier}rem; + `; + + return ( + + ); +} diff --git a/src/TabbedRoutes.tsx b/src/TabbedRoutes.tsx index 85e5f767b5..8648726e51 100644 --- a/src/TabbedRoutes.tsx +++ b/src/TabbedRoutes.tsx @@ -49,6 +49,7 @@ import UpdateAppPage from "./pages/settings/UpdateAppPage"; import useShouldInstall from "./features/pwa/useShouldInstall"; import { UpdateContext } from "./pages/settings/update/UpdateContext"; import { LEMMY_SERVERS } from "./helpers/lemmy"; +import AppearancePage from "./pages/settings/AppearancePage"; const Interceptor = styled.div` position: absolute; @@ -314,6 +315,9 @@ export default function TabbedRoutes() { + + + ` + font-size: 1.3em; + padding: 0 0.5rem; + font-weight: 500; + + ${({ small }) => + small && + css` + font-size: 0.8em; + `} +`; + +const HelperText = styled.div` + margin: 0 32px; + font-size: 0.9em; + color: var(--ion-color-medium); +`; + +export default function TextSize() { + const dispatch = useAppDispatch(); + const { fontSizeMultiplier, useSystemFontSize } = useAppSelector( + (state) => state.appearance.font + ); + + return ( + <> + + Text size + + + + Use System Text Size + + dispatch(setUseSystemFontSize(e.detail.checked)) + } + /> + + + { + dispatch(setFontSizeMultiplier(e.detail.value as number)); + }} + > + + A + + A + + + + Default is two ticks from the left. + + ); +} diff --git a/src/features/settings/appearance/appearanceSlice.tsx b/src/features/settings/appearance/appearanceSlice.tsx new file mode 100644 index 0000000000..6f785716e0 --- /dev/null +++ b/src/features/settings/appearance/appearanceSlice.tsx @@ -0,0 +1,55 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit"; +import { get, set } from "../storage"; +import { merge } from "lodash"; + +const STORAGE_KEYS = { + FONT: { + FONT_SIZE_MULTIPLIER: "appearance--font-size-multiplier", + USE_SYSTEM: "appearance--font-use-system", + }, +} as const; + +interface AppearanceState { + font: { + fontSizeMultiplier: number; + useSystemFontSize: boolean; + }; +} + +const initialState: AppearanceState = { + font: { + fontSizeMultiplier: 1, + useSystemFontSize: false, + }, +}; + +const stateFromStorage: AppearanceState = merge(initialState, { + font: { + fontSizeMultiplier: get(STORAGE_KEYS.FONT.FONT_SIZE_MULTIPLIER), + useSystemFontSize: get(STORAGE_KEYS.FONT.USE_SYSTEM), + }, +}); + +export const appearanceSlice = createSlice({ + name: "appearance", + initialState: stateFromStorage, + reducers: { + setFontSizeMultiplier(state, action: PayloadAction) { + state.font.fontSizeMultiplier = action.payload; + + set(STORAGE_KEYS.FONT.FONT_SIZE_MULTIPLIER, action.payload); + }, + setUseSystemFontSize(state, action: PayloadAction) { + state.font.useSystemFontSize = action.payload; + + set(STORAGE_KEYS.FONT.USE_SYSTEM, action.payload); + }, + + resetAppearance: () => initialState, + }, +}); + +export const { setFontSizeMultiplier, setUseSystemFontSize } = + appearanceSlice.actions; + +export default appearanceSlice.reducer; diff --git a/src/features/settings/storage.ts b/src/features/settings/storage.ts new file mode 100644 index 0000000000..e0ba7dd5fd --- /dev/null +++ b/src/features/settings/storage.ts @@ -0,0 +1,10 @@ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function get(key: string): any { + const data = localStorage.getItem(key); + if (!data) return; + return JSON.parse(data); +} + +export function set(key: string, value: unknown) { + localStorage.setItem(key, JSON.stringify(value)); +} diff --git a/src/features/user/Profile.tsx b/src/features/user/Profile.tsx index 9d7169ca05..440de741f0 100644 --- a/src/features/user/Profile.tsx +++ b/src/features/user/Profile.tsx @@ -22,7 +22,7 @@ export const InsetIonItem = styled(IonItem)` `; export const SettingLabel = styled(IonLabel)` - margin-left: 1rem; + margin-left: 16px; `; interface ProfileProps { diff --git a/src/index.css b/src/index.css index d3fc4199da..1281ff9603 100644 --- a/src/index.css +++ b/src/index.css @@ -48,9 +48,3 @@ ion-modal.small { ion-modal.small ion-header ion-toolbar:first-of-type { padding-top: 0px; } - -/* TODO: Native font scaling */ -/* (Need a configurable option first) */ -/* body ion-content ion-item { - font: -apple-system-body; -} */ diff --git a/src/pages/profile/ProfileFeedItemsPage.tsx b/src/pages/profile/ProfileFeedItemsPage.tsx index e729f66b17..36c53328e2 100644 --- a/src/pages/profile/ProfileFeedItemsPage.tsx +++ b/src/pages/profile/ProfileFeedItemsPage.tsx @@ -27,7 +27,7 @@ export const InsetIonItem = styled(IonItem)` `; export const SettingLabel = styled(IonLabel)` - margin-left: 1rem; + margin-left: 16px; `; interface ProfileFeedItemsPageProps { diff --git a/src/pages/settings/AppearancePage.tsx b/src/pages/settings/AppearancePage.tsx new file mode 100644 index 0000000000..ed1b233721 --- /dev/null +++ b/src/pages/settings/AppearancePage.tsx @@ -0,0 +1,29 @@ +import { + IonBackButton, + IonButtons, + IonHeader, + IonPage, + IonTitle, + IonToolbar, +} from "@ionic/react"; +import AppContent from "../../features/shared/AppContent"; +import TextSize from "../../features/settings/appearance/TextSize"; + +export default function AppearancePage() { + return ( + + + + + + + + Appearance + + + + + + + ); +} diff --git a/src/pages/settings/SettingsPage.tsx b/src/pages/settings/SettingsPage.tsx index 6a30cb7c46..1a6216d6f7 100644 --- a/src/pages/settings/SettingsPage.tsx +++ b/src/pages/settings/SettingsPage.tsx @@ -11,15 +11,36 @@ import AppContent from "../../features/shared/AppContent"; import { InsetIonItem, SettingLabel } from "../../features/user/Profile"; import { apps, + colorPalette, logoGithub, mailOutline, openOutline, - reload, + reloadCircle, shieldCheckmarkOutline, } from "ionicons/icons"; import { useContext, useEffect } from "react"; import { UpdateContext } from "./update/UpdateContext"; import useShouldInstall from "../../features/pwa/useShouldInstall"; +import styled from "@emotion/styled"; +import { css } from "@emotion/react"; + +const IconBg = styled.div<{ color: string }>` + width: 30px; + height: 30px; + + display: flex; + align-items: center; + justify-content: center; + + ion-icon { + width: 20px; + height: 20px; + } + + border-radius: 50%; + background-color: ${({ color }) => color}; + color: white; +`; export default function SettingsPage() { const { status: updateStatus, checkForUpdates } = useContext(UpdateContext); @@ -45,17 +66,33 @@ export default function SettingsPage() { - + + + Install app {shouldInstall && 1} - + + + Check for updates {updateStatus === "outdated" && ( 1 )} + + + + + + Appearance + diff --git a/src/store.ts b/src/store.ts index 8b3d1ceb0d..6f1e2b9d85 100644 --- a/src/store.ts +++ b/src/store.ts @@ -6,6 +6,7 @@ import commentSlice from "./features/comment/commentSlice"; import communitySlice from "./features/community/communitySlice"; import userSlice from "./features/user/userSlice"; import inboxSlice from "./features/inbox/inboxSlice"; +import appearanceSlice from "./features/settings/appearance/appearanceSlice"; const store = configureStore({ reducer: { @@ -15,6 +16,7 @@ const store = configureStore({ community: communitySlice, user: userSlice, inbox: inboxSlice, + appearance: appearanceSlice, }, }); export type RootState = ReturnType;