WIP: New themes and reader preferences for iOS

This commit is contained in:
Jackson Harper
2022-10-28 09:32:00 +08:00
parent 8712a3efef
commit 467a37e8cc
17 changed files with 236 additions and 93 deletions

View File

@ -83,8 +83,11 @@ final class AppStoreScreenshots: XCTestCase {
func testScreenshotSubscriptions() throws {
let app = XCUIApplication()
setupSnapshot(app)
app.navigationBars["Home"]/*@START_MENU_TOKEN@*/ .buttons["_profile"]/*[[".otherElements[\"_profile\"].buttons[\"_profile\"]",".buttons[\"_profile\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/ .tap()
app.collectionViews.buttons["Subscriptions"].tap()
// app.navigationBars.firstMatch.buttons["person.circle"].tap()
// app.collectionViews.buttons["Subscriptions"].tap()
//
// XCUIApplication().navigationBars["_TtGC7SwiftUI19UIHosting"]/*@START_MENU_TOKEN@*/.buttons["ToggleSidebar"]/*[[".buttons[\"Show Sidebar\"]",".buttons[\"ToggleSidebar\"]"],[[[-1,1],[-1,0]]],[0]]@END_MENU_TOKEN@*/.tap()
//
snapshot("Newsletters")

View File

@ -89,6 +89,7 @@ struct WebReader: PlatformViewRepresentable {
if readerSettingsChangedTransactionID != context.coordinator.previousReaderSettingsChangedUUID {
context.coordinator.previousReaderSettingsChangedUUID = readerSettingsChangedTransactionID
(webView as? OmnivoreWebView)?.updateTheme()
(webView as? OmnivoreWebView)?.updateFontFamily()
(webView as? OmnivoreWebView)?.updateFontSize()
(webView as? OmnivoreWebView)?.updateTextContrast()

View File

@ -240,7 +240,8 @@ struct WebReaderContainerView: View {
}
.frame(height: readerViewNavBarHeight * navBarVisibilityRatio)
.opacity(navBarVisibilityRatio)
.background(Color.systemBackground)
.background(Color.black)
.background(Theme.fromName(themeName: ThemeManager.currentThemeName)?.bgColor ?? .clear)
.alert("Are you sure?", isPresented: $showDeleteConfirmation) {
Button("Remove Link", role: .destructive) {
Snackbar.show(message: "Link removed")
@ -301,6 +302,7 @@ struct WebReaderContainerView: View {
annotation: $annotation,
showBottomBar: $showBottomBar
)
.background(Theme.fromName(themeName: ThemeManager.currentThemeName)?.bgColor ?? .clear)
.onTapGesture {
withAnimation {
navBarVisibilityRatio = 1

View File

@ -16,7 +16,7 @@ struct WebReaderContent {
init(
item: LinkedItem,
articleContent: ArticleContent,
isDark: Bool,
isDark _: Bool,
fontSize: Int,
lineHeight: Int,
maxWidthPercentage: Int,
@ -27,7 +27,7 @@ struct WebReaderContent {
self.lineHeight = lineHeight
self.maxWidthPercentage = maxWidthPercentage
self.item = item
self.themeKey = isDark ? "Gray" : "LightGray"
self.themeKey = ThemeManager.currentThemeName // isDark ? "Gray" : "Charcoal"
self.fontFamily = fontFamily
self.articleContent = articleContent
self.prefersHighContrastText = prefersHighContrastText

View File

@ -74,7 +74,7 @@ extension WebReaderCoordinator: WKNavigationDelegate {
func webView(_ webView: WKWebView, didFinish _: WKNavigation!) {
#if os(iOS)
webView.isOpaque = true
webView.backgroundColor = .systemBackground
webView.backgroundColor = .clear
#endif
}

View File

@ -120,7 +120,7 @@ struct SpeechSynthesizer {
func createPlayerItems(from: Int) -> [SpeechItem] {
var result: [SpeechItem] = []
for idx in from ..< document.utterances.count {
for idx in from ..< min(7, document.utterances.count) {
let utterance = document.utterances[idx]
let voiceStr = utterance.voice ?? document.defaultVoice
let segmentStr = String(format: "%04d", arguments: [idx])

View File

@ -19,4 +19,5 @@ public enum UserDefaultKey: String {
case textToSpeechPreloadEnabled
case recentSearchTerms
case audioPlayerExpanded
case themeName
}

View File

@ -44,6 +44,12 @@ public final class OmnivoreWebView: WKWebView {
fatalError("init(coder:) has not been implemented")
}
public func updateTheme() {
if let themeName = UserDefaults.standard.value(forKey: UserDefaultKey.themeName.rawValue) as? String {
dispatchEvent(.updateTheme(themeName: "Gray" /* themeName */ ))
}
}
public func updateFontFamily() {
if let fontFamily = UserDefaults.standard.value(forKey: UserDefaultKey.preferredWebFont.rawValue) as? String {
dispatchEvent(.updateFontFamily(family: fontFamily))
@ -293,6 +299,7 @@ public enum WebViewDispatchEvent {
case updateFontSize(size: Int)
case updateColorMode(isDark: Bool)
case updateFontFamily(family: String)
case updateTheme(themeName: String)
case saveAnnotation(annotation: String)
case annotate
case highlight
@ -320,6 +327,8 @@ public enum WebViewDispatchEvent {
return "updateColorMode"
case .updateFontFamily:
return "updateFontFamily"
case .updateTheme:
return "updateTheme"
case .saveAnnotation:
return "saveAnnotation"
case .annotate:
@ -347,6 +356,8 @@ public enum WebViewDispatchEvent {
return "event.lineHeight = '\(height)';"
case let .updateMaxWidthPercentage(maxWidthPercentage: maxWidthPercentage):
return "event.maxWidthPercentage = '\(maxWidthPercentage)';"
case let .updateTheme(themeName: themeName):
return "event.themeName = '\(themeName)';"
case let .updateFontSize(size: size):
return "event.fontSize = '\(size)';"
case let .updateColorMode(isDark: isDark):

View File

@ -69,81 +69,112 @@ public enum WebFont: String, CaseIterable {
.navigationTitle("Reader Font")
}
var themePicker: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 16) {
ForEach(Theme.allCases, id: \.self) { theme in
VStack {
ZStack {
Circle()
.foregroundColor(theme.bgColor)
.frame(minWidth: 32, minHeight: 32)
.padding(8)
}
Text(theme.rawValue).font(.appCaption)
}
.padding(8)
.background(Color(red: 248 / 255.0, green: 248 / 255.0, blue: 248 / 255.0))
.onTapGesture {
ThemeManager.currentThemeName = theme.rawValue
updateReaderPreferences()
}
.cornerRadius(8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(ThemeManager.currentThemeName == theme.rawValue ? Color.appCtaYellow : .clear, lineWidth: 2)
)
.padding(2)
}
}
}
}
public var body: some View {
NavigationView {
ScrollView(showsIndicators: false) {
VStack(alignment: .center) {
VStack {
LabelledStepper(
labelText: "Font Size:",
onIncrement: {
storedFontSize = min(storedFontSize + 2, 28)
updateReaderPreferences()
},
onDecrement: {
storedFontSize = max(storedFontSize - 2, 10)
updateReaderPreferences()
}
)
VStack(alignment: .center) {
themePicker
.padding(.bottom, 16)
LabelledStepper(
labelText: "Margin:",
onIncrement: {
storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40)
updateReaderPreferences()
},
onDecrement: {
storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100)
updateReaderPreferences()
}
)
LabelledStepper(
labelText: "Line Spacing:",
onIncrement: {
storedLineSpacing = min(storedLineSpacing + 25, 300)
updateReaderPreferences()
},
onDecrement: {
storedLineSpacing = max(storedLineSpacing - 25, 100)
updateReaderPreferences()
}
)
Toggle("High Contrast Text:", isOn: $prefersHighContrastText)
.frame(height: 40)
.padding(.trailing, 6)
.onChange(of: prefersHighContrastText) { _ in
updateReaderPreferences()
}
HStack {
NavigationLink(destination: fontList) {
Text("Change Reader Font")
}
Image(systemName: "chevron.right")
Spacer()
}
.frame(height: 40)
Spacer()
LabelledStepper(
labelText: "Font Size",
onIncrement: {
storedFontSize = min(storedFontSize + 2, 28)
updateReaderPreferences()
},
onDecrement: {
storedFontSize = max(storedFontSize - 2, 10)
updateReaderPreferences()
}
)
LabelledStepper(
labelText: "Margin",
onIncrement: {
storedMaxWidthPercentage = max(storedMaxWidthPercentage - 10, 40)
updateReaderPreferences()
},
onDecrement: {
storedMaxWidthPercentage = min(storedMaxWidthPercentage + 10, 100)
updateReaderPreferences()
}
)
LabelledStepper(
labelText: "Line Spacing",
onIncrement: {
storedLineSpacing = min(storedLineSpacing + 25, 300)
updateReaderPreferences()
},
onDecrement: {
storedLineSpacing = max(storedLineSpacing - 25, 100)
updateReaderPreferences()
}
)
HStack {
NavigationLink(destination: fontList) {
Text("Font")
}
Spacer()
Button(action: {}, label: { Text("Crimson Text").frame(width: 91) })
.buttonStyle(RoundedRectButtonStyle())
}
.frame(height: 40)
Toggle("High Contrast Text:", isOn: $prefersHighContrastText)
.frame(height: 40)
.padding(.trailing, 6)
.onChange(of: prefersHighContrastText) { _ in
updateReaderPreferences()
}
Spacer()
}
.padding()
.navigationTitle("Reader Preferences")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .barTrailing) {
Button(
action: dismissAction,
label: { Text("Done").foregroundColor(.appGrayTextContrast).padding() }
)
}
}
// .toolbar {
// ToolbarItem(placement: .barTrailing) {
// Button(
// action: dismissAction,
// label: { Text("Done").foregroundColor(.appGrayTextContrast).padding() }
// )
// }
// }
}
.navigationViewStyle(.stack)
.accentColor(.appGrayTextContrast)
// .navigationViewStyle(.stack)
// .accentColor(.appGrayTextContrast)
}
}

View File

@ -0,0 +1,54 @@
//
// File.swift
//
//
// Created by Jackson Harper on 10/27/22.
//
import Foundation
import SwiftUI
import Utils
public enum Theme: String, CaseIterable {
case system = "System"
case sepia = "Sepia"
case charcoal = "Charcoal"
case mint = "Mint"
case solarized = "Solarized"
case light = "Light"
case dark = "Dark"
public var bgColor: Color {
switch self {
case .system:
return Color.systemBackground
case .charcoal:
return Color(red: 48 / 255.0, green: 48 / 255.0, blue: 48 / 255.0)
case .sepia:
return Color(red: 249 / 255.0, green: 241 / 255.0, blue: 220 / 255.0)
case .mint:
return Color(red: 202 / 255.0, green: 230 / 255.0, blue: 208 / 255.0)
case .solarized:
return Color(red: 13 / 255.0, green: 39 / 255.0, blue: 50 / 255.0)
case .light:
return Color.white
case .dark:
return Color.black
}
}
public static func fromName(themeName: String) -> Theme? {
for theme in Theme.allCases {
if theme.rawValue == themeName {
return theme
}
}
return nil
}
}
public enum ThemeManager {
@AppStorage(UserDefaultKey.themeName.rawValue) public static var currentThemeName = "System"
}

View File

@ -19,7 +19,7 @@ import WebKit
webView.isOpaque = false
webView.backgroundColor = UIColor.clear
if let url = request.url {
let themeID = Color.isDarkMode ? "Gray" : "LightGray"
let themeID = Color.isDarkMode ? "Gray" /* "Sepia" */ : "Charcoal"
webView.injectCookie(cookieString: "theme=\(themeID); Max-Age=31536000;", url: url)
}
return webView
@ -51,7 +51,7 @@ import WebKit
if let url = request.url {
// Dark mode is still rendering a white background on mac for some reason.
// Forcing light mode for now until we figure out a fix
let themeID = "LightGray" // NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua ? "Gray" : "LightGray"
let themeID = "Charcoal" // NSApp.effectiveAppearance.name == NSAppearance.Name.darkAqua ? "Gray" : "LightGray"
webView.injectCookie(cookieString: "theme=\(themeID); Max-Age=31536000;", url: url)
}
return webView

View File

@ -11,7 +11,7 @@ import { ReportIssuesModal } from './ReportIssuesModal'
import { reportIssueMutation } from '../../../lib/networking/mutations/reportIssueMutation'
import { ArticleHeaderToolbar } from './ArticleHeaderToolbar'
import { userPersonalizationMutation } from '../../../lib/networking/mutations/userPersonalizationMutation'
import { updateThemeLocally } from '../../../lib/themeUpdater'
import { updateTheme, updateThemeLocally } from '../../../lib/themeUpdater'
import { ArticleMutations } from '../../../lib/articleActions'
import { LabelChip } from '../../elements/LabelChip'
import { Label } from '../../../lib/networking/fragments/labelFragment'
@ -121,6 +121,17 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
}
}
interface UpdateThemeEvent extends Event {
themeName?: string
}
const handleThemeChange = async (event: UpdateThemeEvent) => {
const newTheme = event.themeName
if (newTheme) {
updateTheme(newTheme)
}
}
interface UpdateColorModeEvent extends Event {
isDark?: string
}
@ -145,6 +156,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
'updateMaxWidthPercentage',
updateMaxWidthPercentage
)
document.addEventListener('updateTheme', handleThemeChange)
document.addEventListener('updateFontSize', handleFontSizeChange)
document.addEventListener('updateColorMode', updateColorMode)
document.addEventListener(
@ -160,6 +172,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
'updateMaxWidthPercentage',
updateMaxWidthPercentage
)
document.removeEventListener('updateTheme', handleThemeChange)
document.removeEventListener('updateFontSize', handleFontSizeChange)
document.removeEventListener('updateColorMode', updateColorMode)
document.removeEventListener(
@ -179,7 +192,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
readerFontColor: highContrastFont
? theme.colors.readerFontHighContrast.toString()
: theme.colors.readerFont.toString(),
readerFontColorTransparent: theme.colors.readerFontTransparent.toString(),
readerTableHeaderColor: theme.colors.readerTableHeader.toString(),
readerHeadersColor: theme.colors.readerHeader.toString(),
}
@ -192,8 +204,8 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
padding: '16px',
maxWidth: `${styles.maxWidthPercentage ?? 100}%`,
background: props.isAppleAppEmbed
? 'unset'
: theme.colors.grayBg.toString(),
? theme.colors.readerBg.toString()
: theme.colors.readerBg.toString(),
'--text-font-family': styles.fontFamily,
'--text-font-size': `${styles.fontSize}px`,
'--line-height': `${styles.lineHeight}%`,
@ -202,7 +214,6 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
'--figure-margin': '1.6rem auto',
'--hr-margin': '1em',
'--font-color': styles.readerFontColor,
'--font-color-transparent': styles.readerFontColorTransparent,
'--table-header-color': styles.readerTableHeaderColor,
'--headers-color': styles.readerHeadersColor,
'@sm': {
@ -227,6 +238,7 @@ export function ArticleContainer(props: ArticleContainerProps): JSX.Element {
fontFamily: styles.fontFamily,
width: '100%',
wordWrap: 'break-word',
color: '$readerFont',
}}
>
{props.article.title}

View File

@ -126,7 +126,7 @@ export function ReaderSettingsControl(props: ReaderSettingsProps): JSX.Element {
}}
>
<StyledText
color={theme.colors.readerFontTransparent.toString()}
color={theme.colors.readerFont.toString()}
css={{ pl: '12px', m: '0px', pt: '14px' }}
>
Margin:
@ -193,7 +193,7 @@ export function ReaderSettingsControl(props: ReaderSettingsProps): JSX.Element {
}}
>
<StyledText
color={theme.colors.readerFontTransparent.toString()}
color={theme.colors.readerFont.toString()}
css={{ pl: '12px', m: '0px', pt: '14px' }}
>
Line Spacing:

View File

@ -17,7 +17,6 @@ export function SkeletonArticleContainer(props: SkeletonArticleContainerProps):
lineHeight: props.lineHeight ?? 150,
fontFamily: props.fontFamily ?? 'inter',
readerFontColor: theme.colors.readerFont.toString(),
readerFontColorTransparent: theme.colors.readerFontTransparent.toString(),
readerTableHeaderColor: theme.colors.readerTableHeader.toString(),
readerHeadersColor: theme.colors.readerHeader.toString(),
}
@ -40,7 +39,6 @@ export function SkeletonArticleContainer(props: SkeletonArticleContainerProps):
'--figure-margin': '1.6rem auto',
'--hr-margin': '1em',
'--font-color': styles.readerFontColor,
'--font-color-transparent': styles.readerFontColorTransparent,
'--table-header-color': styles.readerTableHeaderColor,
'--headers-color': styles.readerHeadersColor,
'@sm': {

View File

@ -6,6 +6,8 @@ export enum ThemeId {
Light = 'LightGray',
Dark = 'Gray',
Darker = 'Dark',
Sepia = 'Sepia',
Charcoal = 'Charcoal'
}
export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
@ -144,11 +146,10 @@ export const { styled, css, theme, getCssText, globalCss, keyframes, config } =
omnivoreCtaYellow: 'rgb(255, 210, 52)',
// Reader Colors
readerBg: '#E5E5E5',
readerFont: '#3D3D3D',
readerFontHighContrast: 'black',
readerFontTransparent: 'rgba(61,61,61,0.65)',
readerHeader: '3D3D3D',
readerBg: '#F9F1DC', // #E5E5E5',
readerFont: '#554A34',
readerFontHighContrast: '#342100', // black',
readerHeader: '554A34',
readerTableHeader: '#FFFFFF',
// Avatar Fallback color
@ -211,7 +212,6 @@ const darkThemeSpec = {
readerBg: '#303030',
readerFont: '#b9b9b9',
readerFontHighContrast: 'white',
readerFontTransparent: 'rgba(185,185,185,0.65)',
readerHeader: '#b9b9b9',
readerTableHeader: '#FFFFFF',
tooltipIcons: '#5F5E58',
@ -236,12 +236,35 @@ const darkThemeSpec = {
},
}
// Avatar Fallback color
const sepiaThemeSpec = {
colors: {
// Reader Colors
readerBg: '#F9F1DC',
readerFont: '#554A34',
readerFontHighContrast: 'black',
readerHeader: '554A34',
readerTableHeader: '#FFFFFF',
}
}
const charcoalThemeSpec = {
colors: {
// Reader Colors
readerBg: '#303030',
readerFont: '#b9b9b9',
readerFontHighContrast: 'white',
readerHeader: '#b9b9b9',
readerTableHeader: '#FFFFFF',
}
}
// Dark and Darker theme now match each other.
// Use the darkThemeSpec object to make updates.
export const darkTheme = createTheme(ThemeId.Dark, darkThemeSpec)
export const darkerTheme = createTheme(ThemeId.Darker, darkThemeSpec)
export const sepiaTheme = createTheme(ThemeId.Sepia, {...darkThemeSpec, ...sepiaThemeSpec})
export const charcoalTheme = createTheme(ThemeId.Charcoal, {...darkThemeSpec, ...charcoalThemeSpec})
// Lighter theme now matches the default theme.
// This only exists for users that might still have a lighter theme set

View File

@ -3,6 +3,8 @@ import {
lighterTheme,
darkTheme,
darkerTheme,
sepiaTheme,
charcoalTheme,
} from '../components/tokens/stitches.config'
import { userPersonalizationMutation } from './networking/mutations/userPersonalizationMutation'
@ -26,7 +28,9 @@ export function updateThemeLocally(themeId: string): void {
lighterTheme,
ThemeId.Light,
darkTheme,
darkerTheme
darkerTheme,
sepiaTheme,
charcoalTheme
)
document.body.classList.add(themeId)
}
@ -41,6 +45,10 @@ export function currentThemeName(): string {
return 'Darker'
case ThemeId.Lighter:
return 'Lighter'
case ThemeId.Sepia:
return 'Sepia'
case ThemeId.Charcoal:
return 'Charcoal'
default:
return ''
}

View File

@ -1,6 +1,5 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable functional/no-class */
import { useEffect } from 'react'
import NextDocument, { Html, Head, Main, NextScript } from 'next/document'
import { getCssText, globalStyles } from '../components/tokens/stitches.config'
@ -35,7 +34,7 @@ export default class Document extends NextDocument {
var themeId = window.localStorage.getItem('theme')
if (themeId) {
document.body.classList.remove('theme-default', 'White', 'Gray', 'LightGray', 'Dark')
document.body.classList.remove('theme-default', 'White', 'Gray', 'LightGray', 'Dark', 'Sepia', 'Charcoal')
document.body.classList.add(themeId)
}
`