feat: Implement extended font system and offline sync improvements

- Add 10 new fonts (Literata, Merriweather, Source Serif, Lato, Montserrat, Source Sans)
- Support Apple system fonts and Google Fonts (OFL 1.1 licensed)
- Extend FontFamily enum with new fonts and categories
- Update FontSettingsViewModel and WebView with font support
- Force WebView reload when font settings change
- Refactor OfflineSyncManager with protocol and improved error handling
- Add test mocks and OfflineSyncManagerTests with 9 test cases
- Add OpenSourceLicensesView and FontDebugView
- Bump build number
- Update localization strings
This commit is contained in:
Ilyas Hallak 2025-12-10 21:25:39 +01:00
parent 75200e472c
commit a227c275f3
42 changed files with 1616 additions and 93 deletions

View File

@ -1,8 +1,8 @@
# Font System Erweiterung - Konzept & Implementierungsplan
**Datum:** 27. November 2025
**Datum:** 5. Dezember 2025
**Status:** Geplant
**Ziel:** Erweiterte Font-Auswahl mit 11 hochwertigen Schriftarten für bessere Lesbarkeit
**Ziel:** Erweiterte Font-Auswahl mit 10 hochwertigen Schriftarten für bessere Lesbarkeit
---
@ -14,9 +14,9 @@
- ❌ **Sans Serif:** Helvetica Neue (Standard)
- ❌ **Monospace:** Menlo (Apple)
### Neue Situation (11 Fonts)
- ✅ **5 Apple System Fonts** (bereits in iOS enthalten, 0 KB)
- ✅ **8 Google Fonts** (OFL 1.1 lizenziert, ~1-2 MB)
### Neue Situation (10 Fonts)
- ✅ **4 Apple System Fonts** (bereits in iOS enthalten, 0 KB)
- ✅ **6 Google Fonts** (OFL 1.1 lizenziert, ~1.5 MB)
---
@ -29,33 +29,22 @@
---
## 📚 Font-Übersicht (11 Fonts Total)
## 📚 Font-Übersicht (10 Fonts Total)
### Serif Fonts (5 Schriftarten)
### Serif Fonts (4 Schriftarten)
#### 1. **New York** (Apple System Font)
#### 1. **New York** (Apple System Font)
- **Quelle:** In iOS 13+ enthalten
- **Lizenz:** Apple proprietär (frei für iOS Apps)
- **Eigenschaften:**
- 6 Gewichte
- Variable optische Größen
- Unterstützt Latin, Greek, Cyrillic
- Wird in Apple Books und News verwendet
- **Verwendung:** Premium Serif für Apple-native Ästhetik
- **App-Größe:** 0 KB (bereits in iOS)
#### 2. **Lora** (Google Font)
- **Quelle:** [GitHub - cyrealtype/Lora-Cyrillic](https://github.com/cyrealtype/Lora-Cyrillic)
- **Google Fonts:** [fonts.google.com/specimen/Lora](https://fonts.google.com/specimen/Lora)
- **Lizenz:** SIL Open Font License 1.1
- **Designer:** Olga Karpushina, Alexei Vanyashin (Cyreal)
- **Eigenschaften:**
- Gut ausbalancierte Brushes
- Optimiert für Bildschirm-Lesbarkeit
- Variable Font verfügbar
- **Verwendung:** Elegante, lesbare Serif für Artikel
- **App-Größe:** ~200-300 KB
#### 3. **Literata** (Google Font) ⭐
#### 2. **Literata** (Google Font) ⭐
- **Quelle:** [GitHub - googlefonts/literata](https://github.com/googlefonts/literata)
- **Google Fonts:** [fonts.google.com/specimen/Literata](https://fonts.google.com/specimen/Literata)
- **Lizenz:** SIL Open Font License 1.1
@ -67,7 +56,7 @@
- **Verwendung:** **Readeck Web-UI Match** - Hauptschrift für Artikel
- **App-Größe:** ~250-350 KB
#### 4. **Merriweather** (Google Font)
#### 3. **Merriweather** (Google Font)
- **Quelle:** [GitHub - SorkinType/Merriweather](https://github.com/SorkinType/Merriweather)
- **Google Fonts:** [fonts.google.com/specimen/Merriweather](https://fonts.google.com/specimen/Merriweather)
- **Lizenz:** SIL Open Font License 1.1
@ -79,7 +68,7 @@
- **Verwendung:** **Readeck Web-UI Match** - Alternative Serif
- **App-Größe:** ~200-300 KB
#### 5. **Source Serif** (Adobe/Google Font)
#### 4. **Source Serif** (Adobe/Google Font)
- **Quelle:** [GitHub - adobe-fonts/source-serif](https://github.com/adobe-fonts/source-serif)
- **Google Fonts:** [fonts.google.com/specimen/Source+Serif+4](https://fonts.google.com/specimen/Source+Serif+4)
- **Lizenz:** SIL Open Font License 1.1
@ -96,7 +85,7 @@
### Sans Serif Fonts (5 Schriftarten)
#### 6. **SF Pro** (San Francisco - Apple System Font)
#### 5. **SF Pro** (San Francisco - Apple System Font)
- **Quelle:** In iOS enthalten
- **Lizenz:** Apple proprietär (frei für iOS Apps)
- **Eigenschaften:**
@ -108,6 +97,17 @@
- **Verwendung:** Standard UI Font
- **App-Größe:** 0 KB (bereits in iOS)
#### 6. **Avenir Next** (Apple System Font) ⭐
- **Quelle:** In iOS enthalten
- **Lizenz:** Apple proprietär (frei für iOS Apps)
- **Eigenschaften:**
- Moderne geometrische Sans
- 12 Gewichte
- Sehr beliebt (Apple Marketing)
- Optimiert für Lesbarkeit
- **Verwendung:** Premium Sans für moderne Ästhetik
- **App-Größe:** 0 KB (bereits in iOS)
#### 7. **Lato** (Google Font)
- **Quelle:** [GitHub - latofonts/lato-source](https://github.com/latofonts/lato-source)
- **Google Fonts:** [fonts.google.com/specimen/Lato](https://fonts.google.com/specimen/Lato)

View File

@ -103,9 +103,9 @@
UI/Components/UnifiedLabelChip.swift,
UI/Extension/FontSizeExtension.swift,
UI/Models/AppSettings.swift,
"UI/Utils 2/Logger.swift",
"UI/Utils 2/LogStore.swift",
UI/Utils/NotificationNames.swift,
Utils/Logger.swift,
Utils/LogStore.swift,
);
target = 5D2B7FAE2DFA27A400EBDB2B /* URLShare */;
};
@ -640,7 +640,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES;
@ -684,7 +684,7 @@
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 37;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
DEVELOPMENT_TEAM = 8J69P655GN;
ENABLE_HARDENED_RUNTIME = YES;

View File

@ -2,7 +2,13 @@ import Foundation
import CoreData
import SwiftUI
class OfflineSyncManager: ObservableObject, @unchecked Sendable {
protocol POfflineSyncManager {
func syncOfflineBookmarks() async
func getOfflineBookmarks() -> [ArticleURLEntity]
func deleteOfflineBookmark(_ entity: ArticleURLEntity)
}
open class OfflineSyncManager: ObservableObject, @unchecked Sendable {
static let shared = OfflineSyncManager()
@Published var isSyncing = false
@ -10,36 +16,21 @@ class OfflineSyncManager: ObservableObject, @unchecked Sendable {
private let coreDataManager = CoreDataManager.shared
private let api: PAPI
private let checkServerReachabilityUseCase: PCheckServerReachabilityUseCase
init(api: PAPI = API(),
checkServerReachabilityUseCase: PCheckServerReachabilityUseCase = DefaultUseCaseFactory.shared.makeCheckServerReachabilityUseCase()) {
init(api: PAPI = API()) {
self.api = api
self.checkServerReachabilityUseCase = checkServerReachabilityUseCase
}
// MARK: - Sync Methods
func syncOfflineBookmarks() async {
// First check if server is reachable
guard await checkServerReachabilityUseCase.execute() else {
await MainActor.run {
isSyncing = false
syncStatus = "Server not reachable. Cannot sync."
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.syncStatus = nil
}
return
}
await MainActor.run {
isSyncing = true
syncStatus = "Syncing bookmarks with server..."
}
let offlineBookmarks = getOfflineBookmarks()
guard !offlineBookmarks.isEmpty else {
await MainActor.run {
isSyncing = false
@ -50,48 +41,61 @@ class OfflineSyncManager: ObservableObject, @unchecked Sendable {
}
return
}
var successCount = 0
var failedCount = 0
for bookmark in offlineBookmarks {
guard let url = bookmark.url else {
failedCount += 1
continue
}
let tags = bookmark.tags?.components(separatedBy: ",").filter { !$0.isEmpty } ?? []
let title = bookmark.title ?? ""
do {
// Try to upload via API
let dto = CreateBookmarkRequestDto(url: url, title: title, labels: tags.isEmpty ? nil : tags)
_ = try await api.createBookmark(createRequest: dto)
// If successful, delete from offline storage
deleteOfflineBookmark(bookmark)
successCount += 1
await MainActor.run {
syncStatus = "Synced \(successCount) bookmarks..."
}
} catch {
print("Failed to sync bookmark: \(url) - \(error)")
failedCount += 1
// If first sync attempt fails, server is likely unreachable - abort
if successCount == 0 && failedCount == 1 {
await MainActor.run {
isSyncing = false
syncStatus = "Server not reachable. Cannot sync."
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.syncStatus = nil
}
return
}
}
}
await MainActor.run {
isSyncing = false
if failedCount == 0 {
syncStatus = "✅ Successfully synced \(successCount) bookmarks"
} else {
syncStatus = "⚠️ Synced \(successCount), failed \(failedCount) bookmarks"
if successCount > 0 {
if failedCount == 0 {
syncStatus = "✅ Successfully synced \(successCount) bookmarks"
} else {
syncStatus = "⚠️ Synced \(successCount), failed \(failedCount) bookmarks"
}
} else if failedCount > 0 {
syncStatus = "❌ Sync failed - check your connection"
}
}
// Clear status after a few seconds
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.syncStatus = nil
}
@ -100,8 +104,8 @@ class OfflineSyncManager: ObservableObject, @unchecked Sendable {
func getOfflineBookmarksCount() -> Int {
return getOfflineBookmarks().count
}
private func getOfflineBookmarks() -> [ArticleURLEntity] {
open func getOfflineBookmarks() -> [ArticleURLEntity] {
do {
let fetchRequest: NSFetchRequest<ArticleURLEntity> = ArticleURLEntity.fetchRequest()
return try coreDataManager.context.safeFetch(fetchRequest)
@ -110,12 +114,12 @@ class OfflineSyncManager: ObservableObject, @unchecked Sendable {
return []
}
}
private func deleteOfflineBookmark(_ entity: ArticleURLEntity) {
open func deleteOfflineBookmark(_ entity: ArticleURLEntity) {
do {
try coreDataManager.context.safePerform { [weak self] in
guard let self = self else { return }
self.coreDataManager.context.delete(entity)
self.coreDataManager.save()
}

View File

@ -5,19 +5,74 @@
// Created by Ilyas Hallak on 06.11.25.
//
enum FontFamily: String, CaseIterable {
case system = "system"
// Apple System Fonts
case system = "system" // SF Pro
case newYork = "newYork" // New York
case avenirNext = "avenirNext" // Avenir Next
case monospace = "monospace" // SF Mono
// Google Serif Fonts
case literata = "literata"
case merriweather = "merriweather"
case sourceSerif = "sourceSerif"
// Google Sans Serif Fonts
case lato = "lato"
case montserrat = "montserrat"
case sourceSans = "sourceSans"
// Legacy (for backwards compatibility)
case serif = "serif"
case sansSerif = "sansSerif"
case monospace = "monospace"
var displayName: String {
switch self {
case .system: return "System"
case .serif: return "Serif"
case .sansSerif: return "Sans Serif"
case .monospace: return "Monospace"
// Apple
case .system: return "SF Pro"
case .newYork: return "New York"
case .avenirNext: return "Avenir Next"
case .monospace: return "SF Mono"
// Serif
case .literata: return "Literata *"
case .merriweather: return "Merriweather *"
case .sourceSerif: return "Source Serif *"
// Sans Serif
case .lato: return "Lato"
case .montserrat: return "Montserrat"
case .sourceSans: return "Source Sans *"
// Legacy
case .serif: return "Serif (Legacy)"
case .sansSerif: return "Sans Serif (Legacy)"
}
}
var category: FontCategory {
switch self {
case .system, .avenirNext, .lato, .montserrat, .sourceSans, .sansSerif:
return .sansSerif
case .newYork, .literata, .merriweather, .sourceSerif, .serif:
return .serif
case .monospace:
return .monospace
}
}
var isReadeckWebMatch: Bool {
switch self {
case .literata, .merriweather, .sourceSerif, .sourceSans:
return true
default:
return false
}
}
}
enum FontCategory {
case serif
case sansSerif
case monospace
}

View File

@ -15,6 +15,8 @@
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSExceptionRequiresForwardSecrecy</key>
@ -31,5 +33,20 @@
<key>UIImageName</key>
<string>splash</string>
</dict>
<key>UIAppFonts</key>
<array>
<string>Literata-Regular.ttf</string>
<string>Literata-Bold.ttf</string>
<string>Merriweather-Regular.ttf</string>
<string>Merriweather-Bold.ttf</string>
<string>SourceSerif4-Regular.ttf</string>
<string>SourceSerif4-Bold.ttf</string>
<string>Lato-Regular.ttf</string>
<string>Lato-Bold.ttf</string>
<string>Montserrat-Regular.ttf</string>
<string>Montserrat-Bold.ttf</string>
<string>SourceSans3-Regular.ttf</string>
<string>SourceSans3-Bold.ttf</string>
</array>
</dict>
</plist>

View File

@ -188,5 +188,9 @@
"Enable offline reading and sync to cache articles for offline access" = "Aktiviere Offline-Lesen und synchronisiere, um Artikel für den Offline-Zugriff zu cachen";
"Use 'Sync Now' to download articles" = "Verwende 'Jetzt synchronisieren', um Artikel herunterzuladen";
"Simulate Offline Mode" = "Offline-Modus simulieren";
/* Font Settings */
"font.web.match.hint" = "* Entspricht den Readeck Web-Schriften";
"DEBUG: Toggle network status" = "DEBUG: Netzwerkstatus umschalten";

View File

@ -184,4 +184,7 @@
"Enable offline reading and sync to cache articles for offline access" = "Enable offline reading and sync to cache articles for offline access";
"Use 'Sync Now' to download articles" = "Use 'Sync Now' to download articles";
"Simulate Offline Mode" = "Simulate Offline Mode";
/* Font Settings */
"font.web.match.hint" = "* Matches Readeck Web fonts";
"DEBUG: Toggle network status" = "DEBUG: Toggle network status";

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,71 @@
# Open Source Fonts Used in Readeck
This app uses the following open-source fonts:
## Google Fonts (SIL Open Font License 1.1)
### Serif Fonts
- **Literata** by TypeTogether for Google
- License: SIL OFL 1.1
- Source: https://github.com/googlefonts/literata
- **Merriweather** by Sorkin Type
- License: SIL OFL 1.1
- Source: https://github.com/SorkinType/Merriweather
- **Source Serif** by Adobe (Frank Grießhammer)
- License: SIL OFL 1.1
- Source: https://github.com/adobe-fonts/source-serif
### Sans Serif Fonts
- **Lato** by Łukasz Dziedzic
- License: SIL OFL 1.1
- Source: https://github.com/latofonts/lato-source
- **Montserrat** by Julieta Ulanovsky
- License: SIL OFL 1.1
- Source: https://github.com/JulietaUla/Montserrat
- **Source Sans** by Adobe (Paul D. Hunt)
- License: SIL OFL 1.1
- Source: https://github.com/adobe-fonts/source-sans
## Apple System Fonts
- **New York** - Apple proprietary (free for iOS apps)
- **SF Pro** - Apple proprietary (free for iOS apps)
- **Avenir Next** - Apple proprietary (free for iOS apps)
- **SF Mono** - Apple proprietary (free for iOS apps)
---
## SIL Open Font License 1.1
Full license text: https://scripts.sil.org/OFL
### Summary
**Permitted:**
✅ Private use
✅ Commercial use
✅ Modification
✅ Distribution (embedded in App)
✅ Sale of App in AppStore
**Forbidden:**
❌ Sale of fonts as standalone product
**Requirements:**
- Copyright notice must be retained (already in font files)
- License text should be included (see individual license files)
### Individual License Files
- Literata: `Literata-OFL.txt`
- Merriweather: `Merriweather-OFL.txt`
- Source Serif: `SourceSerif-LICENSE.md`
- Lato: `Lato-LICENSE.txt`
- Montserrat: `Montserrat-OFL.txt`
- Source Sans: `SourceSans-LICENSE.md`

View File

@ -0,0 +1,94 @@
Copyright (c) 2010-2019, Łukasz Dziedzic (dziedzic@typoland.com),
with Reserved Font Name Lato.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,93 @@
Copyright 2017 The Literata Project Authors (https://github.com/googlefonts/literata)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,93 @@
Copyright 2016 The Merriweather Project Authors (https://github.com/EbenSorkin/Merriweather), with Reserved Font Name "Merriweather".
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,93 @@
Copyright 2024 The Montserrat.Git Project Authors (https://github.com/JulietaUla/Montserrat.git)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
https://openfontlicense.org
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,93 @@
Copyright 2010-2024 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@ -0,0 +1,93 @@
Copyright 2014 - 2023 Adobe (http://www.adobe.com/), with Reserved Font Name Source. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

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 not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -116,6 +116,7 @@ struct BookmarkDetailLegacyView: View {
.frame(height: webViewHeight)
.cornerRadius(14)
.padding(.horizontal, 4)
.id("\(settings.fontFamily?.rawValue ?? "system")-\(settings.fontSize?.rawValue ?? "medium")")
} else if viewModel.isLoadingArticle {
ProgressView("Loading article...")
.frame(maxWidth: .infinity, alignment: .center)
@ -392,6 +393,7 @@ struct BookmarkDetailLegacyView: View {
.cornerRadius(14)
.padding(.horizontal, 4)
.animation(.easeInOut, value: webViewHeight)
.id("\(settings.fontFamily?.rawValue ?? "system")-\(settings.fontSize?.rawValue ?? "medium")")
} else if viewModel.isLoadingArticle {
ProgressView("Loading article...")
.frame(maxWidth: .infinity, alignment: .center)

View File

@ -496,6 +496,7 @@ struct BookmarkDetailView2: View {
.frame(height: webViewHeight)
.cornerRadius(14)
.padding(.horizontal, 4)
.id("\(settings.fontFamily?.rawValue ?? "system")-\(settings.fontSize?.rawValue ?? "medium")")
}
} else if viewModel.isLoadingArticle {
ProgressView("Loading article...")

View File

@ -189,6 +189,68 @@ struct NativeWebView: View {
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="color-scheme" content="\(isDarkMode ? "dark" : "light")">
<style>
/* Load custom fonts from app bundle */
@font-face {
font-family: 'Literata';
src: local('Literata-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Literata';
src: local('Literata-Bold');
font-weight: bold;
}
@font-face {
font-family: 'Merriweather';
src: local('Merriweather-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Merriweather';
src: local('Merriweather-Bold');
font-weight: bold;
}
@font-face {
font-family: 'Source Serif 4';
src: local('SourceSerif4-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Source Serif 4';
src: local('SourceSerif4-Bold');
font-weight: bold;
}
@font-face {
font-family: 'Lato';
src: local('Lato-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Lato';
src: local('Lato-Bold');
font-weight: bold;
}
@font-face {
font-family: 'Montserrat';
src: local('Montserrat-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Montserrat';
src: local('Montserrat-Bold');
font-weight: bold;
}
@font-face {
font-family: 'Source Sans 3';
src: local('SourceSans3-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Source Sans 3';
src: local('SourceSans3-Bold');
font-weight: bold;
}
* {
max-width: 100%;
box-sizing: border-box;
@ -394,10 +456,25 @@ struct NativeWebView: View {
private func getFontFamily(from fontFamily: FontFamily) -> String {
switch fontFamily {
// Apple System Fonts
case .system: return "-apple-system, BlinkMacSystemFont, sans-serif"
case .newYork: return "'New York', 'Times New Roman', Georgia, serif"
case .avenirNext: return "'Avenir Next', Avenir, 'Helvetica Neue', sans-serif"
case .monospace: return "'SF Mono', Menlo, Monaco, monospace"
// Google Serif Fonts
case .literata: return "'Literata', Georgia, 'Times New Roman', serif"
case .merriweather: return "'Merriweather', Georgia, 'Times New Roman', serif"
case .sourceSerif: return "'Source Serif 4', 'Source Serif Pro', Georgia, serif"
// Google Sans Serif Fonts
case .lato: return "'Lato', 'Helvetica Neue', Arial, sans-serif"
case .montserrat: return "'Montserrat', 'Helvetica Neue', Arial, sans-serif"
case .sourceSans: return "'Source Sans 3', 'Source Sans Pro', 'Helvetica Neue', sans-serif"
// Legacy
case .serif: return "'Times New Roman', Times, serif"
case .sansSerif: return "'Helvetica Neue', Helvetica, Arial, sans-serif"
case .monospace: return "'SF Mono', Menlo, Monaco, monospace"
}
}

View File

@ -74,6 +74,68 @@ struct WebView: UIViewRepresentable {
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="color-scheme" content="\(isDarkMode ? "dark" : "light")">
<style>
/* Load custom fonts from app bundle */
@font-face {
font-family: 'Literata';
src: local('Literata-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Literata';
src: local('Literata-Bold');
font-weight: bold;
}
@font-face {
font-family: 'Merriweather';
src: local('Merriweather-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Merriweather';
src: local('Merriweather-Bold');
font-weight: bold;
}
@font-face {
font-family: 'Source Serif 4';
src: local('SourceSerif4-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Source Serif 4';
src: local('SourceSerif4-Bold');
font-weight: bold;
}
@font-face {
font-family: 'Lato';
src: local('Lato-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Lato';
src: local('Lato-Bold');
font-weight: bold;
}
@font-face {
font-family: 'Montserrat';
src: local('Montserrat-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Montserrat';
src: local('Montserrat-Bold');
font-weight: bold;
}
@font-face {
font-family: 'Source Sans 3';
src: local('SourceSans3-Regular');
font-weight: normal;
}
@font-face {
font-family: 'Source Sans 3';
src: local('SourceSans3-Bold');
font-weight: bold;
}
:root {
--background-color: \(isDarkMode ? "#000000" : "#ffffff");
--text-color: \(isDarkMode ? "#ffffff" : "#1a1a1a");
@ -356,14 +418,37 @@ struct WebView: UIViewRepresentable {
private func getFontFamily(from fontFamily: FontFamily) -> String {
switch fontFamily {
// Apple System Fonts
case .system:
return "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
case .newYork:
return "'New York', 'Times New Roman', Georgia, serif"
case .avenirNext:
return "'Avenir Next', Avenir, 'Helvetica Neue', sans-serif"
case .monospace:
return "'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', monospace"
// Google Serif Fonts
case .literata:
return "'Literata', Georgia, 'Times New Roman', serif"
case .merriweather:
return "'Merriweather', Georgia, 'Times New Roman', serif"
case .sourceSerif:
return "'Source Serif 4', 'Source Serif Pro', Georgia, serif"
// Google Sans Serif Fonts
case .lato:
return "'Lato', 'Helvetica Neue', Arial, sans-serif"
case .montserrat:
return "'Montserrat', 'Helvetica Neue', Arial, sans-serif"
case .sourceSans:
return "'Source Sans 3', 'Source Sans Pro', 'Helvetica Neue', sans-serif"
// Legacy
case .serif:
return "'Times New Roman', Times, 'Liberation Serif', serif"
case .sansSerif:
return "'Helvetica Neue', Helvetica, Arial, sans-serif"
case .monospace:
return "'SF Mono', Menlo, Monaco, Consolas, 'Liberation Mono', monospace"
}
}

View File

@ -0,0 +1,71 @@
//
// FontDebugView.swift
// readeck
//
// Created by Ilyas Hallak on 05.12.25.
//
import SwiftUI
import UIKit
#if DEBUG
struct FontDebugView: View {
@State private var availableFonts: [String: [String]] = [:]
var body: some View {
NavigationStack {
List {
Section {
Text("This view shows all available font families and their font names. Use this to verify that custom fonts are loaded correctly.")
.font(.caption)
.foregroundColor(.secondary)
} header: {
Text("Debug Info")
}
ForEach(availableFonts.keys.sorted(), id: \.self) { family in
Section {
ForEach(availableFonts[family] ?? [], id: \.self) { fontName in
Text(fontName)
.font(.caption)
.textSelection(.enabled)
}
} header: {
Text(family)
.textSelection(.enabled)
}
}
}
.navigationTitle("Available Fonts")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Refresh") {
loadFonts()
}
}
}
.onAppear {
loadFonts()
}
}
}
private func loadFonts() {
var fonts: [String: [String]] = [:]
for family in UIFont.familyNames.sorted() {
let names = UIFont.fontNames(forFamilyName: family)
if !names.isEmpty {
fonts[family] = names
}
}
availableFonts = fonts
}
}
#Preview {
FontDebugView()
}
#endif

View File

@ -61,6 +61,10 @@ struct FontSelectionView: View {
}
}
Text("font.web.match.hint".localized)
.font(.caption)
.foregroundColor(.secondary)
VStack(alignment: .leading, spacing: 8) {
Text("Font size")
.font(.subheadline)

View File

@ -28,6 +28,10 @@ struct FontSettingsView: View {
}
}
Text("font.web.match.hint".localized)
.font(.caption)
.foregroundColor(.secondary)
Picker("Font size", selection: $viewModel.selectedFontSize) {
ForEach(FontSize.allCases, id: \.self) { size in
Text(size.displayName).tag(size)

View File

@ -24,42 +24,116 @@ class FontSettingsViewModel {
// MARK: - Computed Font Properties for Preview
var previewTitleFont: Font {
let size = selectedFontSize.size
switch selectedFontFamily {
// Apple System Fonts
case .system:
return selectedFontSize.systemFont.weight(.semibold)
case .serif:
return Font.custom("Times New Roman", size: selectedFontSize.size).weight(.semibold)
case .sansSerif:
return Font.custom("Helvetica Neue", size: selectedFontSize.size).weight(.semibold)
return Font.system(size: size).weight(.semibold)
case .newYork:
return Font.system(size: size, design: .serif).weight(.semibold)
case .avenirNext:
return Font.custom("AvenirNext-DemiBold", size: size)
case .monospace:
return Font.custom("Menlo", size: selectedFontSize.size).weight(.semibold)
return Font.system(size: size, design: .monospaced).weight(.semibold)
// Google Serif Fonts
case .literata:
return Font.custom("Literata-Bold", size: size)
case .merriweather:
return Font.custom("Merriweather-Bold", size: size)
case .sourceSerif:
return Font.custom("SourceSerif4-Bold", size: size)
// Google Sans Serif Fonts
case .lato:
return Font.custom("Lato-Bold", size: size)
case .montserrat:
return Font.custom("Montserrat-Bold", size: size)
case .sourceSans:
return Font.custom("SourceSans3-Bold", size: size)
// Legacy
case .serif:
return Font.custom("Times New Roman", size: size).weight(.semibold)
case .sansSerif:
return Font.custom("Helvetica Neue", size: size).weight(.semibold)
}
}
var previewBodyFont: Font {
let size = selectedFontSize.size
switch selectedFontFamily {
// Apple System Fonts
case .system:
return selectedFontSize.systemFont
case .serif:
return Font.custom("Times New Roman", size: selectedFontSize.size)
case .sansSerif:
return Font.custom("Helvetica Neue", size: selectedFontSize.size)
return Font.system(size: size)
case .newYork:
return Font.system(size: size, design: .serif)
case .avenirNext:
return Font.custom("AvenirNext-Regular", size: size)
case .monospace:
return Font.custom("Menlo", size: selectedFontSize.size)
return Font.system(size: size, design: .monospaced)
// Google Serif Fonts
case .literata:
return Font.custom("Literata-Regular", size: size)
case .merriweather:
return Font.custom("Merriweather-Regular", size: size)
case .sourceSerif:
return Font.custom("SourceSerif4-Regular", size: size)
// Google Sans Serif Fonts
case .lato:
return Font.custom("Lato-Regular", size: size)
case .montserrat:
return Font.custom("Montserrat-Regular", size: size)
case .sourceSans:
return Font.custom("SourceSans3-Regular", size: size)
// Legacy
case .serif:
return Font.custom("Times New Roman", size: size)
case .sansSerif:
return Font.custom("Helvetica Neue", size: size)
}
}
var previewCaptionFont: Font {
let captionSize = selectedFontSize.size * 0.85
switch selectedFontFamily {
// Apple System Fonts
case .system:
return Font.system(size: captionSize)
case .newYork:
return Font.system(size: captionSize, design: .serif)
case .avenirNext:
return Font.custom("AvenirNext-Regular", size: captionSize)
case .monospace:
return Font.system(size: captionSize, design: .monospaced)
// Google Serif Fonts
case .literata:
return Font.custom("Literata-Regular", size: captionSize)
case .merriweather:
return Font.custom("Merriweather-Regular", size: captionSize)
case .sourceSerif:
return Font.custom("SourceSerif4-Regular", size: captionSize)
// Google Sans Serif Fonts
case .lato:
return Font.custom("Lato-Regular", size: captionSize)
case .montserrat:
return Font.custom("Montserrat-Regular", size: captionSize)
case .sourceSans:
return Font.custom("SourceSans3-Regular", size: captionSize)
// Legacy
case .serif:
return Font.custom("Times New Roman", size: captionSize)
case .sansSerif:
return Font.custom("Helvetica Neue", size: captionSize)
case .monospace:
return Font.custom("Menlo", size: captionSize)
}
}

View File

@ -4,6 +4,7 @@ struct LegalPrivacySettingsView: View {
@State private var showingPrivacyPolicy = false
@State private var showingLegalNotice = false
@State private var showReleaseNotes = false
@State private var showingLicenses = false
var body: some View {
Group {
@ -47,6 +48,18 @@ struct LegalPrivacySettingsView: View {
}
}
Button(action: {
showingLicenses = true
}) {
HStack {
Text("Open Source Licenses")
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
}
Button(action: {
if let url = URL(string: "https://github.com/ilyas-hallak/readeck-ios/issues") {
UIApplication.shared.open(url)
@ -87,6 +100,9 @@ struct LegalPrivacySettingsView: View {
.sheet(isPresented: $showReleaseNotes) {
ReleaseNotesView()
}
.sheet(isPresented: $showingLicenses) {
OpenSourceLicensesView()
}
}
}

View File

@ -0,0 +1,135 @@
//
// OpenSourceLicensesView.swift
// readeck
//
// Created by Ilyas Hallak on 05.12.25.
//
import SwiftUI
struct OpenSourceLicensesView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
List {
Section {
Text("This app uses the following open-source fonts under the SIL Open Font License 1.1.")
.font(.subheadline)
.foregroundColor(.secondary)
} header: {
Text("Open Source Fonts")
}
Section {
FontLicenseRow(
name: "Literata",
author: "TypeTogether for Google",
license: "SIL OFL 1.1"
)
FontLicenseRow(
name: "Merriweather",
author: "Sorkin Type",
license: "SIL OFL 1.1"
)
FontLicenseRow(
name: "Source Serif",
author: "Adobe (Frank Grießhammer)",
license: "SIL OFL 1.1"
)
FontLicenseRow(
name: "Lato",
author: "Łukasz Dziedzic",
license: "SIL OFL 1.1"
)
FontLicenseRow(
name: "Montserrat",
author: "Julieta Ulanovsky",
license: "SIL OFL 1.1"
)
FontLicenseRow(
name: "Source Sans",
author: "Adobe (Paul D. Hunt)",
license: "SIL OFL 1.1"
)
} header: {
Text("Font Licenses")
}
Section {
VStack(alignment: .leading, spacing: 8) {
Text("SIL Open Font License 1.1")
.font(.headline)
Text("The SIL Open Font License allows the fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves.")
.font(.caption)
.foregroundColor(.secondary)
Button(action: {
if let url = URL(string: "https://scripts.sil.org/OFL") {
UIApplication.shared.open(url)
}
}) {
HStack {
Text("View Full License")
.font(.caption)
Image(systemName: "arrow.up.right")
.font(.caption2)
}
}
.padding(.top, 4)
}
.padding(.vertical, 4)
} header: {
Text("License Information")
}
Section {
Text("Apple System Fonts (SF Pro, New York, Avenir Next, SF Mono) are proprietary to Apple Inc. and are free to use within iOS applications.")
.font(.caption)
.foregroundColor(.secondary)
} header: {
Text("Apple Fonts")
}
}
.navigationTitle("Open Source Licenses")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
dismiss()
}
}
}
}
}
}
struct FontLicenseRow: View {
let name: String
let author: String
let license: String
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(name)
.font(.headline)
Text(author)
.font(.subheadline)
.foregroundColor(.secondary)
Text(license)
.font(.caption)
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
}
}
#Preview {
OpenSourceLicensesView()
}

View File

@ -134,6 +134,15 @@ struct SettingsContainerView: View {
) {
LoggingConfigurationView()
}
SettingsRowNavigationLink(
icon: "textformat",
iconColor: .green,
title: "Font Debug",
subtitle: "View available fonts"
) {
FontDebugView()
}
} header: {
HStack {
Text("Debug Settings")

View File

@ -0,0 +1,167 @@
//
// TestMocks.swift
// readeckTests
//
// Created by Ilyas Hallak
//
import Foundation
import CoreData
@testable import readeck
// MARK: - Mock API
@MainActor
class TestMockAPI: PAPI {
var tokenProvider: TokenProvider = TestMockTokenProvider()
var createBookmarkCalls: [(CreateBookmarkRequestDto, Result<CreateBookmarkResponseDto, Error>)] = []
var createBookmarkResults: [Result<CreateBookmarkResponseDto, Error>] = []
private var callIndex = 0
func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto {
guard callIndex < createBookmarkResults.count else {
throw APIError.serverError(500)
}
let result = createBookmarkResults[callIndex]
callIndex += 1
createBookmarkCalls.append((createRequest, result))
switch result {
case .success(let response):
return response
case .failure(let error):
throw error
}
}
func reset() {
createBookmarkCalls.removeAll()
callIndex = 0
}
// MARK: - Unimplemented Methods
func login(endpoint: String, username: String, password: String) async throws -> UserDto {
fatalError("Not implemented for tests")
}
func getBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?, tag: String?) async throws -> BookmarksPageDto {
fatalError("Not implemented for tests")
}
func getBookmark(id: String) async throws -> BookmarkDetailDto {
fatalError("Not implemented for tests")
}
func getBookmarkArticle(id: String) async throws -> String {
fatalError("Not implemented for tests")
}
func updateBookmark(id: String, updateRequest: UpdateBookmarkRequestDto) async throws {
fatalError("Not implemented for tests")
}
func deleteBookmark(id: String) async throws {
fatalError("Not implemented for tests")
}
func searchBookmarks(search: String) async throws -> BookmarksPageDto {
fatalError("Not implemented for tests")
}
func getBookmarkLabels() async throws -> [BookmarkLabelDto] {
fatalError("Not implemented for tests")
}
func getBookmarkAnnotations(bookmarkId: String) async throws -> [AnnotationDto] {
fatalError("Not implemented for tests")
}
func createAnnotation(bookmarkId: String, color: String, startOffset: Int, endOffset: Int, startSelector: String, endSelector: String) async throws -> AnnotationDto {
fatalError("Not implemented for tests")
}
func deleteAnnotation(bookmarkId: String, annotationId: String) async throws {
fatalError("Not implemented for tests")
}
}
// MARK: - Mock Token Provider
class TestMockTokenProvider: TokenProvider {
func getToken() async -> String? { return "mock-token" }
func setToken(_ token: String) async {}
func clearToken() async {}
func getEndpoint() async -> String? { return "https://mock.example.com" }
func setEndpoint(_ endpoint: String) async {}
func clearEndpoint() async {}
}
// MARK: - Test CoreData Manager
class TestCoreDataManager {
let context: NSManagedObjectContext
init() {
let managedObjectModel = NSManagedObjectModel.mergedModel(from: [Bundle.main])!
let persistentStoreCoordinator = NSPersistentStoreCoordinator(managedObjectModel: managedObjectModel)
try! persistentStoreCoordinator.addPersistentStore(
ofType: NSInMemoryStoreType,
configurationName: nil,
at: nil,
options: nil
)
context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
context.persistentStoreCoordinator = persistentStoreCoordinator
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
}
func createTestBookmark(url: String, title: String, tags: String? = nil) -> ArticleURLEntity {
let entity = ArticleURLEntity(context: context)
entity.url = url
entity.title = title
entity.tags = tags
entity.id = UUID()
try! context.save()
return entity
}
func fetchAllBookmarks() -> [ArticleURLEntity] {
let fetchRequest: NSFetchRequest<ArticleURLEntity> = ArticleURLEntity.fetchRequest()
return (try? context.fetch(fetchRequest)) ?? []
}
func clearAll() {
let fetchRequest: NSFetchRequest<NSFetchRequestResult> = ArticleURLEntity.fetchRequest()
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
try? context.execute(deleteRequest)
try? context.save()
}
}
// MARK: - Testable OfflineSyncManager
@MainActor
class TestableOfflineSyncManager: OfflineSyncManager {
let mockCoreDataManager: TestCoreDataManager
init(api: PAPI, coreDataManager: TestCoreDataManager) {
self.mockCoreDataManager = coreDataManager
super.init(api: api)
}
override func getOfflineBookmarks() -> [ArticleURLEntity] {
return mockCoreDataManager.fetchAllBookmarks()
}
override func deleteOfflineBookmark(_ entity: ArticleURLEntity) {
mockCoreDataManager.context.delete(entity)
try? mockCoreDataManager.context.save()
}
}

View File

@ -0,0 +1,165 @@
//
// OfflineSyncManagerTests.swift
// readeckTests
//
// Created by Ilyas Hallak
//
import Testing
import Foundation
@testable import readeck
@Suite("OfflineSyncManager Tests")
@MainActor
struct OfflineSyncManagerTests {
// MARK: - Test: Empty Queue
@Test("Should handle empty bookmark queue")
func testEmptyQueue() async throws {
let (syncManager, mockAPI, _) = createTestEnvironment()
await syncManager.syncOfflineBookmarks()
#expect(syncManager.isSyncing == false)
#expect(syncManager.syncStatus == "No bookmarks to sync")
#expect(mockAPI.createBookmarkCalls.isEmpty)
}
// MARK: - Test: Successful Sync
@Test("Should successfully sync all bookmarks")
func testSuccessfulSync() async throws {
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
_ = mockCoreData.createTestBookmark(url: "https://example.com/1", title: "Article 1", tags: "tag1,tag2")
_ = mockCoreData.createTestBookmark(url: "https://example.com/2", title: "Article 2")
_ = mockCoreData.createTestBookmark(url: "https://example.com/3", title: "Article 3", tags: "tag3")
mockAPI.createBookmarkResults = Array(repeating: .success(mockSuccessResponse()), count: 3)
await syncManager.syncOfflineBookmarks()
try await Task.sleep(for: .milliseconds(100))
#expect(syncManager.isSyncing == false)
#expect(syncManager.syncStatus?.contains("Successfully synced 3 bookmarks") == true)
#expect(mockAPI.createBookmarkCalls.count == 3)
#expect(mockCoreData.fetchAllBookmarks().isEmpty)
}
// MARK: - Test: Server Unreachable
@Test("Should abort on first failure (server unreachable)")
func testServerUnreachable() async throws {
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
_ = mockCoreData.createTestBookmark(url: "https://example.com/1", title: "Article 1")
_ = mockCoreData.createTestBookmark(url: "https://example.com/2", title: "Article 2")
_ = mockCoreData.createTestBookmark(url: "https://example.com/3", title: "Article 3")
mockAPI.createBookmarkResults = [.failure(APIError.serverError(503))]
await syncManager.syncOfflineBookmarks()
try await Task.sleep(for: .milliseconds(100))
#expect(syncManager.isSyncing == false)
#expect(syncManager.syncStatus == "Server not reachable. Cannot sync.")
#expect(mockAPI.createBookmarkCalls.count == 1)
#expect(mockCoreData.fetchAllBookmarks().count == 3)
}
// MARK: - Test: Partial Success
@Test("Should handle partial sync success")
func testPartialSuccess() async throws {
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
for i in 1...4 {
_ = mockCoreData.createTestBookmark(url: "https://example.com/\(i)", title: "Article \(i)")
}
mockAPI.createBookmarkResults = [
.success(mockSuccessResponse()),
.failure(APIError.serverError(400)),
.success(mockSuccessResponse()),
.failure(APIError.serverError(400))
]
await syncManager.syncOfflineBookmarks()
try await Task.sleep(for: .milliseconds(100))
#expect(syncManager.isSyncing == false)
#expect(syncManager.syncStatus?.contains("Synced 2, failed 2") == true)
#expect(mockAPI.createBookmarkCalls.count == 4)
#expect(mockCoreData.fetchAllBookmarks().count == 2)
}
// MARK: - Test: Bookmark Without URL
@Test("Should skip bookmarks without URL")
func testBookmarkWithoutURL() async throws {
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
let invalidEntity = ArticleURLEntity(context: mockCoreData.context)
invalidEntity.url = nil
invalidEntity.title = "Invalid Bookmark"
try! mockCoreData.context.save()
_ = mockCoreData.createTestBookmark(url: "https://example.com/1", title: "Valid Article")
mockAPI.createBookmarkResults = [.success(mockSuccessResponse())]
await syncManager.syncOfflineBookmarks()
try await Task.sleep(for: .milliseconds(100))
#expect(syncManager.isSyncing == false)
#expect(mockAPI.createBookmarkCalls.count == 1)
}
// MARK: - Test: Tags Parsing
@Test("Should correctly parse and send tags")
func testTagsParsing() async throws {
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
_ = mockCoreData.createTestBookmark(url: "https://example.com/1", title: "Article", tags: "swift,ios,testing")
mockAPI.createBookmarkResults = [.success(mockSuccessResponse())]
await syncManager.syncOfflineBookmarks()
try await Task.sleep(for: .milliseconds(100))
#expect(mockAPI.createBookmarkCalls.count == 1)
#expect(mockAPI.createBookmarkCalls[0].0.labels == ["swift", "ios", "testing"])
}
// MARK: - Test: Empty Tags
@Test("Should handle bookmarks without tags")
func testEmptyTags() async throws {
let (syncManager, mockAPI, mockCoreData) = createTestEnvironment()
_ = mockCoreData.createTestBookmark(url: "https://example.com/1", title: "Article")
mockAPI.createBookmarkResults = [.success(mockSuccessResponse())]
await syncManager.syncOfflineBookmarks()
try await Task.sleep(for: .milliseconds(100))
#expect(mockAPI.createBookmarkCalls.count == 1)
#expect(mockAPI.createBookmarkCalls[0].0.labels == nil)
}
// MARK: - Test Helpers
private func createTestEnvironment() -> (TestableOfflineSyncManager, TestMockAPI, TestCoreDataManager) {
let mockAPI = TestMockAPI()
let mockCoreData = TestCoreDataManager()
let syncManager = TestableOfflineSyncManager(api: mockAPI, coreDataManager: mockCoreData)
return (syncManager, mockAPI, mockCoreData)
}
private func mockSuccessResponse() -> CreateBookmarkResponseDto {
CreateBookmarkResponseDto(message: "Bookmark created", status: 200)
}
}