feat: Add bookmark creation functionality and WebView dark mode support
- Add CREATE API endpoint for new bookmarks with POST request - Implement CreateBookmarkRequestDto and CreateBookmarkResponseDto - Create AddBookmarkView with form validation and clipboard integration - Add AddBookmarkViewModel with URL validation and label parsing - Implement CreateBookmarkUseCase with convenience methods - Extend BookmarksRepository with createBookmark method returning server message - Add comprehensive error handling for bookmark creation scenarios - Integrate WebView dark mode support with CSS variables and system color scheme - Add dynamic theme switching based on iOS appearance settings - Enhance WebView styling with iOS-native colors and typography - Fix BookmarksView refresh after bookmark creation - Add floating action button and sheet presentation for adding bookmarks - Implement form validation with real-time feedback - Add clipboard URL detection and paste functionality
This commit is contained in:
parent
cd265730d3
commit
da7bb1613c
@ -13,6 +13,7 @@ protocol PAPI {
|
||||
func getBookmarks(state: BookmarkState?) async throws -> [BookmarkDto]
|
||||
func getBookmark(id: String) async throws -> BookmarkDetailDto
|
||||
func getBookmarkArticle(id: String) async throws -> String
|
||||
func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto
|
||||
func updateBookmark(id: String, updateRequest: UpdateBookmarkRequestDto) async throws
|
||||
func deleteBookmark(id: String) async throws
|
||||
}
|
||||
@ -165,6 +166,17 @@ class API: PAPI {
|
||||
)
|
||||
}
|
||||
|
||||
func createBookmark(createRequest: CreateBookmarkRequestDto) async throws -> CreateBookmarkResponseDto {
|
||||
let requestData = try JSONEncoder().encode(createRequest)
|
||||
|
||||
return try await makeJSONRequest(
|
||||
endpoint: "/api/bookmarks",
|
||||
method: .POST,
|
||||
body: requestData,
|
||||
responseType: CreateBookmarkResponseDto.self
|
||||
)
|
||||
}
|
||||
|
||||
func updateBookmark(id: String, updateRequest: UpdateBookmarkRequestDto) async throws {
|
||||
let requestData = try JSONEncoder().encode(updateRequest)
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ struct BookmarkDto: Codable {
|
||||
let siteName: String
|
||||
let site: String
|
||||
let readingTime: Int?
|
||||
let wordCount: Int
|
||||
let wordCount: Int?
|
||||
let hasArticle: Bool
|
||||
let isArchived: Bool
|
||||
let isDeleted: Bool
|
||||
@ -62,3 +62,5 @@ struct ImageResourceDto: Codable {
|
||||
let height: Int
|
||||
let width: Int
|
||||
}
|
||||
|
||||
|
||||
|
||||
13
readeck/Data/API/DTOs/CreateBookmarkRequestDto.swift
Normal file
13
readeck/Data/API/DTOs/CreateBookmarkRequestDto.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
struct CreateBookmarkRequestDto: Codable {
|
||||
let labels: [String]?
|
||||
let title: String?
|
||||
let url: String
|
||||
|
||||
init(url: String, title: String? = nil, labels: [String]? = nil) {
|
||||
self.url = url
|
||||
self.title = title
|
||||
self.labels = labels
|
||||
}
|
||||
}
|
||||
6
readeck/Data/DTOs/CreateBookmarkResponseDto.swift
Normal file
6
readeck/Data/DTOs/CreateBookmarkResponseDto.swift
Normal file
@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
struct CreateBookmarkResponseDto: Codable {
|
||||
let message: String
|
||||
let status: Int
|
||||
}
|
||||
@ -4,9 +4,9 @@ protocol PBookmarksRepository {
|
||||
func fetchBookmarks(state: BookmarkState?) async throws -> [Bookmark]
|
||||
func fetchBookmark(id: String) async throws -> BookmarkDetail
|
||||
func fetchBookmarkArticle(id: String) async throws -> String
|
||||
func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String
|
||||
func updateBookmark(id: String, updateRequest: BookmarkUpdateRequest) async throws
|
||||
func deleteBookmark(id: String) async throws
|
||||
func addBookmark(bookmark: Bookmark) async throws
|
||||
}
|
||||
|
||||
class BookmarksRepository: PBookmarksRepository {
|
||||
@ -46,8 +46,21 @@ class BookmarksRepository: PBookmarksRepository {
|
||||
return try await api.getBookmarkArticle(id: id)
|
||||
}
|
||||
|
||||
func addBookmark(bookmark: Bookmark) async throws {
|
||||
// Implement logic to add a bookmark if needed
|
||||
func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String {
|
||||
let dto = CreateBookmarkRequestDto(
|
||||
url: createRequest.url,
|
||||
title: createRequest.title,
|
||||
labels: createRequest.labels
|
||||
)
|
||||
|
||||
let response = try await api.createBookmark(createRequest: dto)
|
||||
|
||||
// Prüfe ob die Erstellung erfolgreich war
|
||||
guard response.status == 0 else {
|
||||
throw CreateBookmarkError.serverError(response.message)
|
||||
}
|
||||
|
||||
return response.message
|
||||
}
|
||||
|
||||
func deleteBookmark(id: String) async throws {
|
||||
@ -80,7 +93,7 @@ struct BookmarkDetail {
|
||||
let authors: [String]
|
||||
let created: String
|
||||
let updated: String
|
||||
let wordCount: Int
|
||||
let wordCount: Int?
|
||||
let readingTime: Int?
|
||||
let hasArticle: Bool
|
||||
let isMarked: Bool
|
||||
@ -88,3 +101,23 @@ struct BookmarkDetail {
|
||||
let thumbnailUrl: String
|
||||
let imageUrl: String
|
||||
}
|
||||
|
||||
enum CreateBookmarkError: Error, LocalizedError {
|
||||
case invalidURL
|
||||
case duplicateBookmark
|
||||
case networkError
|
||||
case serverError(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL:
|
||||
return "Die eingegebene URL ist ungültig"
|
||||
case .duplicateBookmark:
|
||||
return "Dieser Bookmark existiert bereits"
|
||||
case .networkError:
|
||||
return "Netzwerkfehler beim Erstellen des Bookmarks"
|
||||
case .serverError(let message):
|
||||
return message // Verwende die Server-Nachricht direkt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ protocol UseCaseFactory {
|
||||
func makeLoadSettingsUseCase() -> LoadSettingsUseCase
|
||||
func makeUpdateBookmarkUseCase() -> UpdateBookmarkUseCase
|
||||
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase
|
||||
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase
|
||||
}
|
||||
|
||||
class DefaultUseCaseFactory: UseCaseFactory {
|
||||
@ -57,4 +58,8 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
||||
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase {
|
||||
return DeleteBookmarkUseCase(repository: bookmarksRepository)
|
||||
}
|
||||
|
||||
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase {
|
||||
return CreateBookmarkUseCase(repository: bookmarksRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ struct Bookmark {
|
||||
let siteName: String
|
||||
let site: String
|
||||
let readingTime: Int?
|
||||
let wordCount: Int
|
||||
let wordCount: Int?
|
||||
let hasArticle: Bool
|
||||
let isArchived: Bool
|
||||
let isDeleted: Bool
|
||||
|
||||
28
readeck/Domain/Model/CreateBookmarkRequest.swift
Normal file
28
readeck/Domain/Model/CreateBookmarkRequest.swift
Normal file
@ -0,0 +1,28 @@
|
||||
import Foundation
|
||||
|
||||
struct CreateBookmarkRequest {
|
||||
let url: String
|
||||
let title: String?
|
||||
let labels: [String]?
|
||||
|
||||
init(url: String, title: String? = nil, labels: [String]? = nil) {
|
||||
self.url = url
|
||||
self.title = title
|
||||
self.labels = labels
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience Initializers
|
||||
extension CreateBookmarkRequest {
|
||||
static func fromURL(_ url: String) -> CreateBookmarkRequest {
|
||||
return CreateBookmarkRequest(url: url)
|
||||
}
|
||||
|
||||
static func fromURLWithTitle(_ url: String, title: String) -> CreateBookmarkRequest {
|
||||
return CreateBookmarkRequest(url: url, title: title)
|
||||
}
|
||||
|
||||
static func fromURLWithLabels(_ url: String, labels: [String]) -> CreateBookmarkRequest {
|
||||
return CreateBookmarkRequest(url: url, labels: labels)
|
||||
}
|
||||
}
|
||||
51
readeck/Domain/UseCase/CreateBookmarkUseCase.swift
Normal file
51
readeck/Domain/UseCase/CreateBookmarkUseCase.swift
Normal file
@ -0,0 +1,51 @@
|
||||
import Foundation
|
||||
|
||||
class CreateBookmarkUseCase {
|
||||
private let repository: PBookmarksRepository
|
||||
|
||||
init(repository: PBookmarksRepository) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func execute(createRequest: CreateBookmarkRequest) async throws -> String {
|
||||
// URL-Validierung
|
||||
guard URL(string: createRequest.url) != nil else {
|
||||
throw CreateBookmarkError.invalidURL
|
||||
}
|
||||
|
||||
return try await repository.createBookmark(createRequest: createRequest)
|
||||
}
|
||||
|
||||
// Convenience methods für häufige Use Cases
|
||||
func createFromURL(_ url: String) async throws -> String {
|
||||
let request = CreateBookmarkRequest.fromURL(url)
|
||||
return try await execute(createRequest: request)
|
||||
}
|
||||
|
||||
func createFromURLWithTitle(_ url: String, title: String) async throws -> String {
|
||||
let request = CreateBookmarkRequest.fromURLWithTitle(url, title: title)
|
||||
return try await execute(createRequest: request)
|
||||
}
|
||||
|
||||
func createFromURLWithLabels(_ url: String, labels: [String]) async throws -> String {
|
||||
let request = CreateBookmarkRequest.fromURLWithLabels(url, labels: labels)
|
||||
return try await execute(createRequest: request)
|
||||
}
|
||||
|
||||
func createFromClipboard() async throws -> String? {
|
||||
return nil
|
||||
// URL aus Zwischenablage holen (falls verfügbar)
|
||||
/*#if canImport(UIKit)
|
||||
import UIKit
|
||||
guard let clipboardString = UIPasteboard.general.string,
|
||||
URL(string: clipboardString) != nil else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let request = CreateBookmarkRequest.fromURL(clipboardString)
|
||||
return try await execute(createRequest: request)
|
||||
#else
|
||||
return nil
|
||||
#endif*/
|
||||
}
|
||||
}
|
||||
134
readeck/UI/AddBookmark/AddBookmarkView.swift
Normal file
134
readeck/UI/AddBookmark/AddBookmarkView.swift
Normal file
@ -0,0 +1,134 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AddBookmarkView: View {
|
||||
@State private var viewModel = AddBookmarkViewModel()
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section(header: Text("Bookmark Details")) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("URL *")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("https://example.com", text: $viewModel.url)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled()
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Titel (optional)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("Bookmark Titel", text: $viewModel.title)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Labels")) {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text("Labels (durch Komma getrennt)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
TextField("work, important, later", text: $viewModel.labelsText)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
}
|
||||
|
||||
if !viewModel.parsedLabels.isEmpty {
|
||||
LazyVGrid(columns: [
|
||||
GridItem(.adaptive(minimum: 80))
|
||||
], spacing: 8) {
|
||||
ForEach(viewModel.parsedLabels, id: \.self) { label in
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.foregroundColor(.blue)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Aus Zwischenablage einfügen") {
|
||||
viewModel.pasteFromClipboard()
|
||||
}
|
||||
.disabled(viewModel.clipboardURL == nil)
|
||||
|
||||
if let clipboardURL = viewModel.clipboardURL {
|
||||
Text("Zwischenablage: \(clipboardURL)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Bookmark hinzufügen")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Abbrechen") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Speichern") {
|
||||
Task {
|
||||
await viewModel.createBookmark()
|
||||
}
|
||||
}
|
||||
.disabled(!viewModel.isValid || viewModel.isLoading)
|
||||
}
|
||||
}
|
||||
.overlay {
|
||||
if viewModel.isLoading {
|
||||
ZStack {
|
||||
Color.black.opacity(0.3)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
ProgressView()
|
||||
.scaleEffect(1.2)
|
||||
|
||||
Text("Bookmark wird erstellt...")
|
||||
.font(.subheadline)
|
||||
}
|
||||
.padding(24)
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.shadow(radius: 10)
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
.alert("Erfolgreich", isPresented: $viewModel.showSuccessAlert) {
|
||||
Button("OK") {
|
||||
dismiss()
|
||||
}
|
||||
} message: {
|
||||
Text("Bookmark wurde erfolgreich hinzugefügt!")
|
||||
}
|
||||
.alert("Fehler", isPresented: $viewModel.showErrorAlert) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text(viewModel.errorMessage ?? "Unbekannter Fehler")
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.checkClipboard()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AddBookmarkView()
|
||||
}
|
||||
80
readeck/UI/AddBookmark/AddBookmarkViewModel.swift
Normal file
80
readeck/UI/AddBookmark/AddBookmarkViewModel.swift
Normal file
@ -0,0 +1,80 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
@Observable
|
||||
class AddBookmarkViewModel {
|
||||
private let createBookmarkUseCase = DefaultUseCaseFactory.shared.makeCreateBookmarkUseCase()
|
||||
|
||||
var url: String = ""
|
||||
var title: String = ""
|
||||
var labelsText: String = ""
|
||||
|
||||
var isLoading: Bool = false
|
||||
var errorMessage: String?
|
||||
var showErrorAlert: Bool = false
|
||||
var showSuccessAlert: Bool = false
|
||||
var clipboardURL: String?
|
||||
|
||||
var isValid: Bool {
|
||||
!url.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
||||
URL(string: url.trimmingCharacters(in: .whitespacesAndNewlines)) != nil
|
||||
}
|
||||
|
||||
var parsedLabels: [String] {
|
||||
labelsText
|
||||
.components(separatedBy: ",")
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func createBookmark() async {
|
||||
guard isValid else { return }
|
||||
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
|
||||
do {
|
||||
let cleanURL = url.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let cleanTitle = title.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let labels = parsedLabels
|
||||
|
||||
let request = CreateBookmarkRequest(
|
||||
url: cleanURL,
|
||||
title: cleanTitle.isEmpty ? nil : cleanTitle,
|
||||
labels: labels.isEmpty ? nil : labels
|
||||
)
|
||||
|
||||
let message = try await createBookmarkUseCase.execute(createRequest: request)
|
||||
|
||||
// Optional: Zeige die Server-Nachricht an
|
||||
print("Server response: \(message)")
|
||||
|
||||
showSuccessAlert = true
|
||||
|
||||
} catch let error as CreateBookmarkError {
|
||||
errorMessage = error.localizedDescription
|
||||
showErrorAlert = true
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Erstellen des Bookmarks"
|
||||
showErrorAlert = true
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
func checkClipboard() {
|
||||
guard let clipboardString = UIPasteboard.general.string,
|
||||
URL(string: clipboardString) != nil else {
|
||||
clipboardURL = nil
|
||||
return
|
||||
}
|
||||
|
||||
clipboardURL = clipboardString
|
||||
}
|
||||
|
||||
func pasteFromClipboard() {
|
||||
guard let clipboardURL = clipboardURL else { return }
|
||||
url = clipboardURL
|
||||
}
|
||||
}
|
||||
@ -28,17 +28,17 @@ struct BookmarkCardView: View {
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
// Status-Badges und Action-Button
|
||||
// Status-Icons und Action-Button
|
||||
HStack {
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: 6) {
|
||||
if bookmark.isMarked {
|
||||
Badge(text: "Markiert", color: .blue)
|
||||
IconBadge(systemName: "heart.fill", color: .red)
|
||||
}
|
||||
if bookmark.isArchived {
|
||||
Badge(text: "Archiviert", color: .gray)
|
||||
IconBadge(systemName: "archivebox.fill", color: .gray)
|
||||
}
|
||||
if bookmark.hasArticle {
|
||||
Badge(text: "Artikel", color: .green)
|
||||
IconBadge(systemName: "doc.text.fill", color: .green)
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,18 +146,17 @@ struct BookmarkCardView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct Badge: View {
|
||||
let text: String
|
||||
struct IconBadge: View {
|
||||
let systemName: String
|
||||
let color: Color
|
||||
|
||||
var body: some View {
|
||||
Text(text)
|
||||
Image(systemName: systemName)
|
||||
.font(.caption2)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.padding(6)
|
||||
.background(color.opacity(0.2))
|
||||
.foregroundColor(color)
|
||||
.clipShape(Capsule())
|
||||
.clipShape(Circle())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import SwiftUI
|
||||
|
||||
struct BookmarksView: View {
|
||||
@State private var viewModel = BookmarksViewModel()
|
||||
@State private var showingAddBookmark = false
|
||||
let state: BookmarkState
|
||||
|
||||
var body: some View {
|
||||
@ -55,6 +56,18 @@ struct BookmarksView: View {
|
||||
}
|
||||
}
|
||||
.navigationTitle(state.displayName)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
showingAddBookmark = true
|
||||
}) {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingAddBookmark) {
|
||||
AddBookmarkView()
|
||||
}
|
||||
.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||||
Button("OK", role: .cancel) {
|
||||
viewModel.errorMessage = nil
|
||||
@ -65,6 +78,14 @@ struct BookmarksView: View {
|
||||
.task {
|
||||
await viewModel.loadBookmarks(state: state)
|
||||
}
|
||||
.onChange(of: showingAddBookmark) { oldValue, newValue in
|
||||
// Refresh bookmarks when sheet is dismissed
|
||||
if oldValue && !newValue {
|
||||
Task {
|
||||
await viewModel.refreshBookmarks()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,11 +4,14 @@ import WebKit
|
||||
struct WebView: UIViewRepresentable {
|
||||
let htmlContent: String
|
||||
let onHeightChange: (CGFloat) -> Void
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let webView = WKWebView()
|
||||
webView.navigationDelegate = context.coordinator
|
||||
webView.scrollView.isScrollEnabled = false
|
||||
webView.isOpaque = false
|
||||
webView.backgroundColor = UIColor.clear
|
||||
|
||||
// Message Handler hier einmalig hinzufügen
|
||||
webView.configuration.userContentController.add(context.coordinator, name: "heightUpdate")
|
||||
@ -21,21 +24,39 @@ struct WebView: UIViewRepresentable {
|
||||
// Nur den HTML-Inhalt laden, keine Handler-Konfiguration
|
||||
context.coordinator.onHeightChange = onHeightChange
|
||||
|
||||
let isDarkMode = colorScheme == .dark
|
||||
|
||||
let styledHTML = """
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="color-scheme" content="\(isDarkMode ? "dark" : "light")">
|
||||
<style>
|
||||
:root {
|
||||
--background-color: \(isDarkMode ? "#000000" : "#ffffff");
|
||||
--text-color: \(isDarkMode ? "#ffffff" : "#1a1a1a");
|
||||
--heading-color: \(isDarkMode ? "#ffffff" : "#000000");
|
||||
--link-color: \(isDarkMode ? "#0A84FF" : "#007AFF");
|
||||
--quote-color: \(isDarkMode ? "#8E8E93" : "#666666");
|
||||
--quote-border: \(isDarkMode ? "#0A84FF" : "#007AFF");
|
||||
--code-background: \(isDarkMode ? "#1C1C1E" : "#f5f5f5");
|
||||
--code-text: \(isDarkMode ? "#ffffff" : "#000000");
|
||||
--separator-color: \(isDarkMode ? "#38383A" : "#e0e0e0");
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
line-height: 1.8;
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
color: #1a1a1a;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
font-size: 16px;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: #000;
|
||||
color: var(--heading-color);
|
||||
margin-top: 24px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 600;
|
||||
@ -43,43 +64,123 @@ struct WebView: UIViewRepresentable {
|
||||
h1 { font-size: 24px; }
|
||||
h2 { font-size: 20px; }
|
||||
h3 { font-size: 18px; }
|
||||
|
||||
p {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 8px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #007AFF;
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 4px solid #007AFF;
|
||||
border-left: 4px solid var(--quote-border);
|
||||
margin: 16px 0;
|
||||
padding-left: 16px;
|
||||
font-style: italic;
|
||||
color: #666;
|
||||
}
|
||||
code {
|
||||
background-color: #f5f5f5;
|
||||
padding: 2px 4px;
|
||||
color: var(--quote-color);
|
||||
background-color: \(isDarkMode ? "rgba(58, 58, 60, 0.3)" : "rgba(0, 122, 255, 0.05)");
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--code-background);
|
||||
color: var(--code-text);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f5f5f5;
|
||||
padding: 12px;
|
||||
background-color: var(--code-background);
|
||||
color: var(--code-text);
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
overflow-x: auto;
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 14px;
|
||||
border: 1px solid var(--separator-color);
|
||||
}
|
||||
|
||||
pre code {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background-color: var(--separator-color);
|
||||
margin: 24px 0;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid var(--separator-color);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: \(isDarkMode ? "rgba(58, 58, 60, 0.5)" : "rgba(0, 0, 0, 0.05)");
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
padding-left: 20px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Dark mode media query als Fallback */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background-color: #000000;
|
||||
--text-color: #ffffff;
|
||||
--heading-color: #ffffff;
|
||||
--link-color: #0A84FF;
|
||||
--quote-color: #8E8E93;
|
||||
--quote-border: #0A84FF;
|
||||
--code-background: #1C1C1E;
|
||||
--code-text: #ffffff;
|
||||
--separator-color: #38383A;
|
||||
}
|
||||
}
|
||||
|
||||
/* Light mode media query als Fallback */
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--background-color: #ffffff;
|
||||
--text-color: #1a1a1a;
|
||||
--heading-color: #000000;
|
||||
--link-color: #007AFF;
|
||||
--quote-color: #666666;
|
||||
--quote-border: #007AFF;
|
||||
--code-background: #f5f5f5;
|
||||
--code-text: #000000;
|
||||
--separator-color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@ -95,6 +196,11 @@ struct WebView: UIViewRepresentable {
|
||||
setTimeout(updateHeight, 100);
|
||||
setTimeout(updateHeight, 500);
|
||||
setTimeout(updateHeight, 1000);
|
||||
|
||||
// Höhe bei Bild-Ladevorgängen aktualisieren
|
||||
document.querySelectorAll('img').forEach(img => {
|
||||
img.addEventListener('load', updateHeight);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user