Integrate Settings with WebView font customization

- Connect SettingsView font selection to WebView CSS rendering
- Add dynamic font size and family mapping in WebView
- Implement CSS custom properties for responsive font scaling
- Add font family fallback stacks for cross-platform compatibility
- Update WebView to use Settings object for typography configuration
This commit is contained in:
Ilyas Hallak 2025-06-15 00:56:19 +02:00
parent 2bc93abe24
commit 789d581705
12 changed files with 165 additions and 53 deletions

View File

@ -22,10 +22,6 @@ class AuthRepository: PAuthRepository {
func getCurrentSettings() async throws -> Settings? { func getCurrentSettings() async throws -> Settings? {
return try await settingsRepository.loadSettings() return try await settingsRepository.loadSettings()
} }
func saveSettings(_ settings: Settings) async throws {
try await settingsRepository.saveSettings(settings)
}
} }
struct User { struct User {

View File

@ -2,10 +2,13 @@ import Foundation
import CoreData import CoreData
struct Settings { struct Settings {
let endpoint: String var endpoint: String? = nil
let username: String var username: String? = nil
let password: String var password: String? = nil
var token: String? var token: String? = nil
var fontFamily: FontFamily? = nil
var fontSize: FontSize? = nil
var isLoggedIn: Bool { var isLoggedIn: Bool {
token != nil && !token!.isEmpty token != nil && !token!.isEmpty
@ -34,19 +37,35 @@ class SettingsRepository: PSettingsRepository {
do { do {
// Vorhandene Einstellungen löschen // Vorhandene Einstellungen löschen
let fetchRequest: NSFetchRequest<SettingEntity> = SettingEntity.fetchRequest() let fetchRequest: NSFetchRequest<SettingEntity> = SettingEntity.fetchRequest()
let existingSettings = try context.fetch(fetchRequest) if let existingSettings = try context.fetch(fetchRequest).first {
for setting in existingSettings {
context.delete(setting) if let endpoint = settings.endpoint, !endpoint.isEmpty {
existingSettings.endpoint = endpoint
}
if let username = settings.username, !username.isEmpty {
existingSettings.username = username
}
if let password = settings.password, !password.isEmpty {
existingSettings.password = password
}
if let token = settings.token, !token.isEmpty {
existingSettings.token = token
}
if let fontFamily = settings.fontFamily {
existingSettings.fontFamily = fontFamily.rawValue
}
if let fontSize = settings.fontSize {
existingSettings.fontSize = fontSize.rawValue
}
try context.save()
} }
// Neue Einstellungen erstellen
let settingEntity = SettingEntity(context: context)
settingEntity.endpoint = settings.endpoint
settingEntity.username = settings.username
settingEntity.password = settings.password
settingEntity.token = settings.token
try context.save()
continuation.resume() continuation.resume()
} catch { } catch {
continuation.resume(throwing: error) continuation.resume(throwing: error)
@ -71,7 +90,9 @@ class SettingsRepository: PSettingsRepository {
endpoint: settingEntity.endpoint ?? "", endpoint: settingEntity.endpoint ?? "",
username: settingEntity.username ?? "", username: settingEntity.username ?? "",
password: settingEntity.password ?? "", password: settingEntity.password ?? "",
token: settingEntity.token token: settingEntity.token,
fontFamily: FontFamily(rawValue: settingEntity.fontFamily ?? FontFamily.system.rawValue),
fontSize: FontSize(rawValue: settingEntity.fontSize ?? FontSize.medium.rawValue)
) )
continuation.resume(returning: settings) continuation.resume(returning: settings)
} else { } else {

View File

@ -10,5 +10,4 @@ protocol PAuthRepository {
func login(username: String, password: String) async throws -> User func login(username: String, password: String) async throws -> User
func logout() async throws func logout() async throws
func getCurrentSettings() async throws -> Settings? func getCurrentSettings() async throws -> Settings?
func saveSettings(_ settings: Settings) async throws
} }

View File

@ -10,4 +10,4 @@ class GetBookmarkArticleUseCase {
func execute(id: String) async throws -> String { func execute(id: String) async throws -> String {
return try await repository.fetchBookmarkArticle(id: id) return try await repository.fetchBookmarkArticle(id: id)
} }
} }

View File

@ -1,19 +1,36 @@
import Foundation import Foundation
class SaveSettingsUseCase { class SaveSettingsUseCase {
private let authRepository: PAuthRepository private let settingsRepository: PSettingsRepository
init(authRepository: PAuthRepository) { init(settingsRepository: PSettingsRepository) {
self.authRepository = authRepository self.settingsRepository = settingsRepository
} }
func execute(endpoint: String, username: String, password: String) async throws { func execute(endpoint: String, username: String, password: String) async throws {
let settings = Settings( try await settingsRepository.saveSettings(
endpoint: endpoint, .init(
username: username, endpoint: endpoint,
password: password, username: username,
token: nil password: password
)
) )
try await authRepository.saveSettings(settings)
} }
}
func execute(token: String) async throws {
try await settingsRepository.saveSettings(
.init(
token: token
)
)
}
func execute(selectedFontFamily: FontFamily, selectedFontSize: FontSize) async throws {
try await settingsRepository.saveSettings(
.init(
fontFamily: selectedFontFamily,
fontSize: selectedFontSize
)
)
}
}

View File

@ -36,8 +36,8 @@ struct BookmarkDetailView: View {
Divider() Divider()
// Artikel-Inhalt mit WebView // Artikel-Inhalt mit WebView
if !viewModel.articleContent.isEmpty { if let settings = viewModel.settings, !viewModel.articleContent.isEmpty {
WebView(htmlContent: viewModel.articleContent) { height in WebView(htmlContent: viewModel.articleContent, settings: settings) { height in
webViewHeight = height webViewHeight = height
} }
.frame(height: webViewHeight) .frame(height: webViewHeight)

View File

@ -4,6 +4,7 @@ import Foundation
class BookmarkDetailViewModel { class BookmarkDetailViewModel {
private let getBookmarkUseCase: GetBookmarkUseCase private let getBookmarkUseCase: GetBookmarkUseCase
private let getBookmarkArticleUseCase: GetBookmarkArticleUseCase private let getBookmarkArticleUseCase: GetBookmarkArticleUseCase
private let loadSettingsUseCase: LoadSettingsUseCase
var bookmarkDetail: BookmarkDetail = BookmarkDetail.empty var bookmarkDetail: BookmarkDetail = BookmarkDetail.empty
var articleContent: String = "" var articleContent: String = ""
@ -12,11 +13,13 @@ class BookmarkDetailViewModel {
var isLoading = false var isLoading = false
var isLoadingArticle = false var isLoadingArticle = false
var errorMessage: String? var errorMessage: String?
var settings: Settings?
init() { init() {
let factory = DefaultUseCaseFactory.shared let factory = DefaultUseCaseFactory.shared
self.getBookmarkUseCase = factory.makeGetBookmarkUseCase() self.getBookmarkUseCase = factory.makeGetBookmarkUseCase()
self.getBookmarkArticleUseCase = factory.makeGetBookmarkArticleUseCase() self.getBookmarkArticleUseCase = factory.makeGetBookmarkArticleUseCase()
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
} }
@MainActor @MainActor
@ -25,6 +28,7 @@ class BookmarkDetailViewModel {
errorMessage = nil errorMessage = nil
do { do {
settings = try await loadSettingsUseCase.execute()
bookmarkDetail = try await getBookmarkUseCase.execute(id: id) bookmarkDetail = try await getBookmarkUseCase.execute(id: id)
// Auch das vollständige Bookmark für readProgress laden // Auch das vollständige Bookmark für readProgress laden

View File

@ -3,6 +3,7 @@ import WebKit
struct WebView: UIViewRepresentable { struct WebView: UIViewRepresentable {
let htmlContent: String let htmlContent: String
let settings: Settings
let onHeightChange: (CGFloat) -> Void let onHeightChange: (CGFloat) -> Void
@Environment(\.colorScheme) private var colorScheme @Environment(\.colorScheme) private var colorScheme
@ -26,6 +27,10 @@ struct WebView: UIViewRepresentable {
let isDarkMode = colorScheme == .dark let isDarkMode = colorScheme == .dark
// Font Settings aus Settings-Objekt
let fontSize = getFontSize(from: settings.fontSize ?? .extraLarge)
let fontFamily = getFontFamily(from: settings.fontFamily ?? .serif)
let styledHTML = """ let styledHTML = """
<html> <html>
<head> <head>
@ -42,16 +47,20 @@ struct WebView: UIViewRepresentable {
--code-background: \(isDarkMode ? "#1C1C1E" : "#f5f5f5"); --code-background: \(isDarkMode ? "#1C1C1E" : "#f5f5f5");
--code-text: \(isDarkMode ? "#ffffff" : "#000000"); --code-text: \(isDarkMode ? "#ffffff" : "#000000");
--separator-color: \(isDarkMode ? "#38383A" : "#e0e0e0"); --separator-color: \(isDarkMode ? "#38383A" : "#e0e0e0");
/* Font Settings from Settings */
--base-font-size: \(fontSize)px;
--font-family: \(fontFamily);
} }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-family: var(--font-family);
line-height: 1.8; line-height: 1.8;
margin: 0; margin: 0;
padding: 16px; padding: 16px;
background-color: var(--background-color); background-color: var(--background-color);
color: var(--text-color); color: var(--text-color);
font-size: 16px; font-size: var(--base-font-size);
-webkit-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;
} }
@ -60,13 +69,19 @@ struct WebView: UIViewRepresentable {
margin-top: 24px; margin-top: 24px;
margin-bottom: 12px; margin-bottom: 12px;
font-weight: 600; font-weight: 600;
font-family: var(--font-family);
} }
h1 { font-size: 24px; } h1 { font-size: calc(var(--base-font-size) * 1.5); }
h2 { font-size: 20px; } h2 { font-size: calc(var(--base-font-size) * 1.25); }
h3 { font-size: 18px; } h3 { font-size: calc(var(--base-font-size) * 1.125); }
h4 { font-size: var(--base-font-size); }
h5 { font-size: calc(var(--base-font-size) * 0.875); }
h6 { font-size: calc(var(--base-font-size) * 0.75); }
p { p {
margin-bottom: 16px; margin-bottom: 16px;
font-family: var(--font-family);
font-size: var(--base-font-size);
} }
img { img {
@ -79,6 +94,7 @@ struct WebView: UIViewRepresentable {
a { a {
color: var(--link-color); color: var(--link-color);
text-decoration: none; text-decoration: none;
font-family: var(--font-family);
} }
a:hover { a:hover {
text-decoration: underline; text-decoration: underline;
@ -93,6 +109,8 @@ struct WebView: UIViewRepresentable {
background-color: \(isDarkMode ? "rgba(58, 58, 60, 0.3)" : "rgba(0, 122, 255, 0.05)"); background-color: \(isDarkMode ? "rgba(58, 58, 60, 0.3)" : "rgba(0, 122, 255, 0.05)");
border-radius: 4px; border-radius: 4px;
padding: 12px 16px; padding: 12px 16px;
font-family: var(--font-family);
font-size: var(--base-font-size);
} }
code { code {
@ -100,8 +118,8 @@ struct WebView: UIViewRepresentable {
color: var(--code-text); color: var(--code-text);
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; border-radius: 4px;
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-family: \(settings.fontFamily == .monospace ? "var(--font-family)" : "'SF Mono', Menlo, Monaco, Consolas, monospace");
font-size: 14px; font-size: calc(var(--base-font-size) * 0.875);
} }
pre { pre {
@ -110,14 +128,15 @@ struct WebView: UIViewRepresentable {
padding: 16px; padding: 16px;
border-radius: 8px; border-radius: 8px;
overflow-x: auto; overflow-x: auto;
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-family: \(settings.fontFamily == .monospace ? "var(--font-family)" : "'SF Mono', Menlo, Monaco, Consolas, monospace");
font-size: 14px; font-size: calc(var(--base-font-size) * 0.875);
border: 1px solid var(--separator-color); border: 1px solid var(--separator-color);
} }
pre code { pre code {
background-color: transparent; background-color: transparent;
padding: 0; padding: 0;
font-family: inherit;
} }
hr { hr {
@ -131,6 +150,8 @@ struct WebView: UIViewRepresentable {
width: 100%; width: 100%;
border-collapse: collapse; border-collapse: collapse;
margin: 16px 0; margin: 16px 0;
font-family: var(--font-family);
font-size: var(--base-font-size);
} }
th, td { th, td {
@ -147,6 +168,8 @@ struct WebView: UIViewRepresentable {
ul, ol { ul, ol {
padding-left: 20px; padding-left: 20px;
margin-bottom: 16px; margin-bottom: 16px;
font-family: var(--font-family);
font-size: var(--base-font-size);
} }
li { li {
@ -211,6 +234,28 @@ struct WebView: UIViewRepresentable {
func makeCoordinator() -> WebViewCoordinator { func makeCoordinator() -> WebViewCoordinator {
WebViewCoordinator() WebViewCoordinator()
} }
private func getFontSize(from fontSize: FontSize) -> Int {
switch fontSize {
case .small: return 14
case .medium: return 16
case .large: return 18
case .extraLarge: return 20
}
}
private func getFontFamily(from fontFamily: FontFamily) -> String {
switch fontFamily {
case .system:
return "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
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"
}
}
} }
class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler { class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler {
@ -238,4 +283,4 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler
deinit { deinit {
// Der Message Handler wird automatisch mit der WebView entfernt // Der Message Handler wird automatisch mit der WebView entfernt
} }
} }

View File

@ -15,8 +15,9 @@ protocol UseCaseFactory {
class DefaultUseCaseFactory: UseCaseFactory { class DefaultUseCaseFactory: UseCaseFactory {
private let tokenProvider = CoreDataTokenProvider() private let tokenProvider = CoreDataTokenProvider()
private lazy var api: PAPI = API(tokenProvider: tokenProvider) private lazy var api: PAPI = API(tokenProvider: tokenProvider)
private lazy var authRepository: PAuthRepository = AuthRepository(api: api, settingsRepository: SettingsRepository()) private lazy var authRepository: PAuthRepository = AuthRepository(api: api, settingsRepository: settingsRepository)
private lazy var bookmarksRepository: PBookmarksRepository = BookmarksRepository(api: api) private lazy var bookmarksRepository: PBookmarksRepository = BookmarksRepository(api: api)
private let settingsRepository: PSettingsRepository = SettingsRepository()
static let shared = DefaultUseCaseFactory() static let shared = DefaultUseCaseFactory()
@ -39,7 +40,7 @@ class DefaultUseCaseFactory: UseCaseFactory {
} }
func makeSaveSettingsUseCase() -> SaveSettingsUseCase { func makeSaveSettingsUseCase() -> SaveSettingsUseCase {
SaveSettingsUseCase(authRepository: authRepository) SaveSettingsUseCase(settingsRepository: settingsRepository)
} }
func makeLoadSettingsUseCase() -> LoadSettingsUseCase { func makeLoadSettingsUseCase() -> LoadSettingsUseCase {

View File

@ -37,6 +37,11 @@ struct SettingsView: View {
} }
.disabled(!viewModel.canLogin || viewModel.isLoading) .disabled(!viewModel.canLogin || viewModel.isLoading)
Button("Debug-Anmeldung") {
viewModel.username = "admin"
viewModel.password = "Diggah123"
}
if viewModel.isLoggedIn { if viewModel.isLoggedIn {
HStack { HStack {
Image(systemName: "checkmark.circle.fill") Image(systemName: "checkmark.circle.fill")
@ -69,7 +74,7 @@ struct SettingsView: View {
ForEach(Theme.allCases, id: \.self) { theme in ForEach(Theme.allCases, id: \.self) { theme in
Text(theme.displayName).tag(theme) Text(theme.displayName).tag(theme)
} }
} }
// Font Settings with Preview // Font Settings with Preview
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
@ -84,13 +89,23 @@ struct SettingsView: View {
} }
} }
.pickerStyle(MenuPickerStyle()) .pickerStyle(MenuPickerStyle())
.onChange(of: viewModel.selectedFontFamily) {
Task {
await viewModel.saveFontSettings()
}
}
Picker("Schriftgröße", selection: $viewModel.selectedFontSize) { Picker("Schriftgröße", selection: $viewModel.selectedFontSize) {
ForEach(FontSize.allCases, id: \.self) { size in ForEach(FontSize.allCases, id: \.self) { size in
Text(size.displayName).tag(size) Text(size.displayName).tag(size)
} }
} }
.pickerStyle(SegmentedPickerStyle()) .pickerStyle(SegmentedPickerStyle())
.onChange(of: viewModel.selectedFontSize) {
Task {
await viewModel.saveFontSettings()
}
}
} }
Spacer() Spacer()

View File

@ -93,9 +93,9 @@ class SettingsViewModel {
func loadSettings() async { func loadSettings() async {
do { do {
if let settings = try await loadSettingsUseCase.execute() { if let settings = try await loadSettingsUseCase.execute() {
endpoint = settings.endpoint endpoint = settings.endpoint ?? ""
username = settings.username username = settings.username ?? ""
password = settings.password password = settings.password ?? ""
isLoggedIn = settings.isLoggedIn // Verwendet die neue Hilfsmethode isLoggedIn = settings.isLoggedIn // Verwendet die neue Hilfsmethode
} }
} catch { } catch {
@ -103,6 +103,17 @@ class SettingsViewModel {
} }
} }
@MainActor
func saveFontSettings() async {
do {
try await saveSettingsUseCase.execute(
selectedFontFamily: selectedFontFamily, selectedFontSize: selectedFontSize
)
} catch {
errorMessage = "Fehler beim Speichern der Font-Einstellungen"
}
}
@MainActor @MainActor
func saveSettings() async { func saveSettings() async {
isSaving = true isSaving = true
@ -134,7 +145,8 @@ class SettingsViewModel {
successMessage = nil successMessage = nil
do { do {
_ = try await loginUseCase.execute(username: username, password: password) let user = try await loginUseCase.execute(username: username, password: password)
isLoggedIn = true isLoggedIn = true
successMessage = "Erfolgreich angemeldet" successMessage = "Erfolgreich angemeldet"

View File

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23507" systemVersion="24B83" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="24F74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class"> <entity name="Item" representedClassName="Item" syncable="YES" codeGenerationType="class">
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/> <attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity> </entity>
<entity name="SettingEntity" representedClassName="SettingEntity" syncable="YES" codeGenerationType="class"> <entity name="SettingEntity" representedClassName="SettingEntity" syncable="YES" codeGenerationType="class">
<attribute name="endpoint" optional="YES" attributeType="String"/> <attribute name="endpoint" optional="YES" attributeType="String"/>
<attribute name="fontFamily" optional="YES" attributeType="String"/>
<attribute name="fontSize" optional="YES" attributeType="String"/>
<attribute name="password" optional="YES" attributeType="String"/> <attribute name="password" optional="YES" attributeType="String"/>
<attribute name="token" optional="YES" attributeType="String"/> <attribute name="token" optional="YES" attributeType="String"/>
<attribute name="username" optional="YES" attributeType="String"/> <attribute name="username" optional="YES" attributeType="String"/>