Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🍱 Add additional fonts #620

Merged
merged 4 commits into from
Mar 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions apps/expo/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@
"configureAndroidBackup": true,
"faceIDPermission": "Allow $(PRODUCT_NAME) to access your Face ID biometric data."
}
],
[
"expo-font",
{
"fonts": [
"assets/fonts/Atkinson-Hyperlegible-Bold.ttf",
"assets/fonts/Atkinson-Hyperlegible-BoldItalic.ttf",
"assets/fonts/Atkinson-Hyperlegible-Italic.ttf",
"assets/fonts/Atkinson-Hyperlegible-Regular.ttf",
"assets/fonts/Bitter-Italic-VariableFont_wght.ttf",
"assets/fonts/Bitter-VariableFont_wght.ttf",
"assets/fonts/CharisSIL-Bold.ttf",
"assets/fonts/CharisSIL-BoldItalic.ttf",
"assets/fonts/CharisSIL-Italic.ttf",
"assets/fonts/CharisSIL-Regular.ttf",
"assets/fonts/Literata-Italic[opsz,wght].ttf",
"assets/fonts/Literata[opsz,wght].ttf",
"assets/fonts/OpenDyslexic-Bold-Italic.otf",
"assets/fonts/OpenDyslexic-Bold.otf",
"assets/fonts/OpenDyslexic-Italic.otf",
"assets/fonts/OpenDyslexic-Regular.otf"
]
}
]
],
"owner": "stumpapp",
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file added apps/expo/assets/fonts/CharisSIL-Bold.ttf
Binary file not shown.
Binary file added apps/expo/assets/fonts/CharisSIL-BoldItalic.ttf
Binary file not shown.
Binary file added apps/expo/assets/fonts/CharisSIL-Italic.ttf
Binary file not shown.
Binary file added apps/expo/assets/fonts/CharisSIL-Regular.ttf
Binary file not shown.
Binary file not shown.
Binary file added apps/expo/assets/fonts/Literata[opsz,wght].ttf
Binary file not shown.
Binary file not shown.
Binary file added apps/expo/assets/fonts/OpenDyslexic-Bold.otf
Binary file not shown.
Binary file added apps/expo/assets/fonts/OpenDyslexic-Italic.otf
Binary file not shown.
Binary file added apps/expo/assets/fonts/OpenDyslexic-Regular.otf
Binary file not shown.
1 change: 1 addition & 0 deletions apps/expo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"dayjs": "^1.11.13",
"expo": "^52.0.25",
"expo-application": "~6.0.2",
"expo-font": "~11.10.3",
"expo-image": "~2.0.4",
"expo-keep-awake": "~14.0.3",
"expo-linking": "~7.0.5",
Expand Down
4 changes: 2 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^4.5.3",
"vite-plugin-tsconfig-paths": "^1.4.1",
"vite-plugin-pwa": "^0.21.1"
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-tsconfig-paths": "^1.4.1"
}
}
2 changes: 1 addition & 1 deletion apps/web/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default defineConfig({
VitePWA({
registerType: 'autoUpdate',
devOptions: {
enabled: true,
enabled: false,
},
workbox: {
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB
Expand Down
18 changes: 18 additions & 0 deletions core/src/db/entity/user/preferences.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,14 @@
#[derive(Default, Debug, Clone, Serialize, Deserialize, Type, ToSchema)]
#[serde(rename_all = "lowercase")]
pub enum SupportedFont {
AtkinsonHyperlegible,
Bitter,
Charis,
#[default]
Inter,
LibreBaskerville,
Literata,
Nunito,
OpenDyslexic,
// TODO(383): Support custom fonts
// Custom(String),
Expand All @@ -138,7 +144,13 @@
impl Display for SupportedFont {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SupportedFont::AtkinsonHyperlegible => write!(f, "atkinsonhyperlegible"),
SupportedFont::Bitter => write!(f, "bitter"),
SupportedFont::Charis => write!(f, "charis"),

Check warning on line 149 in core/src/db/entity/user/preferences.rs

View check run for this annotation

Codecov / codecov/patch

core/src/db/entity/user/preferences.rs#L147-L149

Added lines #L147 - L149 were not covered by tests
SupportedFont::Inter => write!(f, "inter"),
SupportedFont::LibreBaskerville => write!(f, "librebaskerville"),
SupportedFont::Literata => write!(f, "literata"),
SupportedFont::Nunito => write!(f, "nunito"),

Check warning on line 153 in core/src/db/entity/user/preferences.rs

View check run for this annotation

Codecov / codecov/patch

core/src/db/entity/user/preferences.rs#L151-L153

Added lines #L151 - L153 were not covered by tests
SupportedFont::OpenDyslexic => write!(f, "opendyslexic"),
}
}
Expand All @@ -147,6 +159,12 @@
impl From<String> for SupportedFont {
fn from(value: String) -> Self {
match value.to_lowercase().as_str() {
"atkinsonhyperlegible" => SupportedFont::AtkinsonHyperlegible,
"bitter" => SupportedFont::Bitter,
"charis" => SupportedFont::Charis,
"librebaskerville" => SupportedFont::LibreBaskerville,
"literata" => SupportedFont::Literata,
"nunito" => SupportedFont::Nunito,
"opendyslexic" => SupportedFont::OpenDyslexic,
// Note: for now we just always default to Inter. This will be acceptable
// until we have custom font support.
Expand Down
1 change: 1 addition & 0 deletions docs/pages/guides/configuration/theming.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ Stump has a limited number of fonts built-in:

- [Inter](https://rsms.me/inter/)
- [OpenDyslexic](https://opendyslexic.org/)
- [Atkinson Hyperlegible](https://brailleinstitute.org/freefont)

If you would like a new font, it will have to be added to the project manually. This can be done by appropriately defining the font in CSS, such as:

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"dev:expo": "concurrently -n server,expo -c green.bold,blue.bold \"cargo watch --ignore core/prisma -x run -p stump_server\" \"yarn expo dev\"",
"prisma-studio": "npx prisma studio --schema core/prisma/schema.prisma",
"format": "lerna run format --stream --parallel",
"nuke": "lerna run nuke"
"nuke": "concurrently -n root,monorepo -c green.bold,blue.bold \"yarn exec rimraf node_modules\" \"lerna run nuke\""
},
"devDependencies": {
"@babel/core": "^7.25.8",
Expand Down
42 changes: 20 additions & 22 deletions packages/browser/src/components/readers/epub/EpubJsReader.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,16 @@
import {
type EpubReaderPreferences,
queryClient,
useEpubLazy,
useEpubReader,
useQuery,
useSDK,
} from '@stump/client'
import { Bookmark, UpdateEpubProgress } from '@stump/sdk'
import { BookPreferences, queryClient, useEpubLazy, useQuery, useSDK } from '@stump/client'
import { Bookmark, Media, UpdateEpubProgress } from '@stump/sdk'
import { Book, Rendition } from 'epubjs'
import uniqby from 'lodash/uniqBy'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import toast from 'react-hot-toast'
import AutoSizer from 'react-virtualized-auto-sizer'

import { useTheme } from '@/hooks'
import { useBookPreferences } from '@/scenes/book/reader/useBookPreferences'

import EpubReaderContainer from './EpubReaderContainer'
import { stumpDark } from './themes'
import { applyTheme, stumpDark } from './themes'

// NOTE: http://epubjs.org/documentation/0.3/ for epubjs documentation overview

Expand Down Expand Up @@ -72,6 +66,8 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) {
const { sdk } = useSDK()
const { theme } = useTheme()

const { epub, isLoading } = useEpubLazy(id)

const ref = useRef<HTMLDivElement>(null)

const [book, setBook] = useState<Book | null>(null)
Expand All @@ -80,9 +76,7 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) {

const [currentLocation, setCurrentLocation] = useState<EpubLocationState>()

const { epubPreferences } = useEpubReader((state) => ({
epubPreferences: state.preferences,
}))
const { bookPreferences } = useBookPreferences({ book: epub?.media_entity || ({} as Media) })

const { data: bookmarks } = useQuery([sdk.epub.keys.getBookmarks, id], () =>
sdk.epub.getBookmarks(id),
Expand Down Expand Up @@ -124,8 +118,6 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) {
return { chapter: position, chapterName: name, sectionIndex: sectionIndex }
}, [book, currentLocation])

const { epub, isLoading } = useEpubLazy(id)

/**
* A function for focusing the iframe in the epub reader. This will be used to ensure
* the iframe is focused whenever the reader is loaded and/or the location changes.
Expand Down Expand Up @@ -185,14 +177,18 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) {
* @param rendition: The epubjs rendition instance
* @param preferences The epub reader preferences
*/
const applyEpubPreferences = (rendition: Rendition, preferences: EpubReaderPreferences) => {
const applyEpubPreferences = (rendition: Rendition, preferences: BookPreferences) => {
if (theme === 'dark') {
rendition.themes.register('stump-dark', applyTheme(stumpDark, preferences))
rendition.themes.select('stump-dark')
} else {
rendition.themes.register('stump-light', applyTheme({}, preferences))
rendition.themes.select('stump-light')
}
rendition.direction(preferences.readingDirection)
rendition.themes.fontSize(`${preferences.fontSize}px`)
if (preferences.fontSize) {
rendition.themes.fontSize(`${preferences.fontSize}px`)
}
}

/**
Expand All @@ -218,7 +214,9 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) {

//? TODO: I guess here I would need to wait for and load in custom theme blobs...
//* Color manipulation reference: https://github.com/futurepress/epub.js/issues/1019
rendition_.themes.register('stump-dark', stumpDark)
rendition_.themes.register('stump-dark', applyTheme(stumpDark, bookPreferences))
rendition_.themes.register('stump-light', applyTheme({}, bookPreferences))

rendition_.on('relocated', handleLocationChange)

// This callback is used to change the page when a keydown event is received.
Expand All @@ -236,7 +234,7 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) {
// When the epub page isn't in focus, the window fires them instead
window.addEventListener('keydown', keydown_callback)

applyEpubPreferences(rendition_, epubPreferences)
applyEpubPreferences(rendition_, bookPreferences)
setRendition(rendition_)

const targetCfi = epub?.media_entity.active_reading_session?.epubcfi ?? initialCfi
Expand Down Expand Up @@ -297,9 +295,9 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) {
*/
useEffect(() => {
if (rendition) {
applyEpubPreferences(rendition, epubPreferences)
applyEpubPreferences(rendition, bookPreferences)
}
}, [rendition, epubPreferences, theme])
}, [rendition, bookPreferences, theme])

/**
* Invalidate the book query when a reader is unmounted so that the book overview
Expand Down Expand Up @@ -603,7 +601,7 @@ export default function EpubJsReader({ id, initialCfi }: EpubJsReaderProps) {
// 'epubcfi(/6/12!/4[3Q280-a9efbf2f573d4345819e3829f80e5dbc]/2[prologue]/4[prologue-text]/8/1:56)',
// ).then((res) => console.log('cfiWithinAnother', res))

if (isLoading || !epub) {
if (isLoading || !epub?.media_entity) {
return null
}

Expand Down
15 changes: 11 additions & 4 deletions packages/browser/src/components/readers/epub/EpubReaderHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Spacer, Text } from '@stump/components'
import { cn, Spacer, Text } from '@stump/components'
import { ArrowLeft } from 'lucide-react'
import { Link } from 'react-router-dom'

import { useBookPreferences } from '@/scenes/book/reader/useBookPreferences'
import { formatBookName } from '@/utils/format'

import paths from '../../../paths'
Expand All @@ -16,12 +17,13 @@ import {
} from './controls'
import { LocationManager } from './locations'

// TODO(UX): gather feedback on the header design. I am worried the actionable items are too small on
// mobile devices.
export default function EpubReaderHeader() {
const {
readerMeta: { bookEntity },
} = useEpubReaderContext()
const {
bookPreferences: { fontFamily },
} = useBookPreferences({ book: bookEntity })

const bookName = formatBookName(bookEntity)

Expand All @@ -39,7 +41,12 @@ export default function EpubReaderHeader() {

<Spacer />

<Text size="sm" className="line-clamp-1">
<Text
size="sm"
className={cn('line-clamp-1', {
[`font-${fontFamily}`]: !!fontFamily,
})}
>
{bookName}
</Text>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import { useEpubReader } from '@stump/client'
import { cx } from '@stump/components'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useCallback } from 'react'
import { useSwipeable } from 'react-swipeable'

import { useEpubReaderControls } from '../context'
import { useBookPreferences } from '@/scenes/book/reader/useBookPreferences'

import { useEpubReaderContext, useEpubReaderControls } from '../context'
import ControlButton from './ControlButton'

type Props = {
children: React.ReactNode
}

export default function EpubNavigationControls({ children }: Props) {
const {
readerMeta: { bookEntity: book },
} = useEpubReaderContext()
const { visible, onPaginateBackward, onPaginateForward, setVisible } = useEpubReaderControls()

const { readingDirection } = useEpubReader((state) => ({
readingDirection: state.preferences.readingDirection,
}))
const {
bookPreferences: { readingDirection },
} = useBookPreferences({ book })

const invertControls = readingDirection === 'rtl'

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Label, NativeSelect } from '@stump/components'
import { useLocaleContext } from '@stump/i18n'
import { isSupportedFont } from '@stump/sdk'
import { useCallback } from 'react'

import { useBookPreferences } from '@/scenes/book/reader/useBookPreferences'
import { SUPPORTED_FONT_OPTIONS } from '@/scenes/settings/app/appearance/FontSelect'

import { useEpubReaderContext } from '../context'

export default function FontFamily() {
const { t } = useLocaleContext()
const {
readerMeta: { bookEntity },
} = useEpubReaderContext()
const {
bookPreferences: { fontFamily },
setBookPreferences,
} = useBookPreferences({ book: bookEntity })

const changeFont = useCallback(
(font: string) => {
if (!font) {
setBookPreferences({ fontFamily: undefined })
} else if (isSupportedFont(font)) {
// Note: useApplyTheme will apply the font to the body element after the preferences are updated
setBookPreferences({ fontFamily: font })
}
},
[setBookPreferences],
)

return (
<div className="py-1.5">
<Label htmlFor="font-family">{t(getKey('fontFamily.label'))}</Label>
<NativeSelect
id="font-family"
size="sm"
options={[{ value: '', label: 'Default' }].concat(SUPPORTED_FONT_OPTIONS)}
value={fontFamily ?? ''}
onChange={(e) => changeFont(e.target.value)}
className="mt-1.5"
/>
</div>
)
}

const LOCAL_BASE = 'settingsScene.app/reader.sections.textBasedBooks.sections'
const getKey = (key: string) => `${LOCAL_BASE}.${key}`
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
import { useEpubReader } from '@stump/client'
import { cx, Label, Text, TEXT_VARIANTS } from '@stump/components'
import { Minus, Plus } from 'lucide-react'
import { useCallback, useEffect, useRef } from 'react'

import { usePressAndHold } from '@/hooks/usePressAndHold'
import { useBookPreferences } from '@/scenes/book/reader/useBookPreferences'

import { useEpubReaderContext } from '../context'

export default function FontSizeControl() {
const { fontSize, setFontSize } = useEpubReader((state) => ({
fontSize: state.preferences.fontSize,
setFontSize: state.setFontSize,
}))
const {
readerMeta: { bookEntity },
} = useEpubReaderContext()
const {
bookPreferences: { fontSize = 13 },
setBookPreferences,
} = useBookPreferences({ book: bookEntity })
const fontSizeRef = useRef(fontSize)
useEffect(() => {
fontSizeRef.current = fontSize
Expand All @@ -20,10 +25,10 @@ export default function FontSizeControl() {
if (newSize < 1) {
return
} else {
setFontSize(newSize)
setBookPreferences({ fontSize: newSize })
}
},
[setFontSize],
[setBookPreferences],
)

const { bindButton } = usePressAndHold()
Expand Down
Loading
Loading