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? {
return try await settingsRepository.loadSettings()
}
func saveSettings(_ settings: Settings) async throws {
try await settingsRepository.saveSettings(settings)
}
}
struct User {

View File

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

View File

@ -10,5 +10,4 @@ protocol PAuthRepository {
func login(username: String, password: String) async throws -> User
func logout() async throws
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 {
return try await repository.fetchBookmarkArticle(id: id)
}
}
}

View File

@ -1,19 +1,36 @@
import Foundation
class SaveSettingsUseCase {
private let authRepository: PAuthRepository
private let settingsRepository: PSettingsRepository
init(authRepository: PAuthRepository) {
self.authRepository = authRepository
init(settingsRepository: PSettingsRepository) {
self.settingsRepository = settingsRepository
}
func execute(endpoint: String, username: String, password: String) async throws {
let settings = Settings(
endpoint: endpoint,
username: username,
password: password,
token: nil
try await settingsRepository.saveSettings(
.init(
endpoint: endpoint,
username: username,
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()
// Artikel-Inhalt mit WebView
if !viewModel.articleContent.isEmpty {
WebView(htmlContent: viewModel.articleContent) { height in
if let settings = viewModel.settings, !viewModel.articleContent.isEmpty {
WebView(htmlContent: viewModel.articleContent, settings: settings) { height in
webViewHeight = height
}
.frame(height: webViewHeight)

View File

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

View File

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

View File

@ -15,8 +15,9 @@ protocol UseCaseFactory {
class DefaultUseCaseFactory: UseCaseFactory {
private let tokenProvider = CoreDataTokenProvider()
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 let settingsRepository: PSettingsRepository = SettingsRepository()
static let shared = DefaultUseCaseFactory()
@ -39,7 +40,7 @@ class DefaultUseCaseFactory: UseCaseFactory {
}
func makeSaveSettingsUseCase() -> SaveSettingsUseCase {
SaveSettingsUseCase(authRepository: authRepository)
SaveSettingsUseCase(settingsRepository: settingsRepository)
}
func makeLoadSettingsUseCase() -> LoadSettingsUseCase {

View File

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

View File

@ -93,9 +93,9 @@ class SettingsViewModel {
func loadSettings() async {
do {
if let settings = try await loadSettingsUseCase.execute() {
endpoint = settings.endpoint
username = settings.username
password = settings.password
endpoint = settings.endpoint ?? ""
username = settings.username ?? ""
password = settings.password ?? ""
isLoggedIn = settings.isLoggedIn // Verwendet die neue Hilfsmethode
}
} 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
func saveSettings() async {
isSaving = true
@ -134,7 +145,8 @@ class SettingsViewModel {
successMessage = nil
do {
_ = try await loginUseCase.execute(username: username, password: password)
let user = try await loginUseCase.execute(username: username, password: password)
isLoggedIn = true
successMessage = "Erfolgreich angemeldet"

View File

@ -1,10 +1,12 @@
<?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">
<attribute name="timestamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
</entity>
<entity name="SettingEntity" representedClassName="SettingEntity" syncable="YES" codeGenerationType="class">
<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="token" optional="YES" attributeType="String"/>
<attribute name="username" optional="YES" attributeType="String"/>