diff --git a/URLShare/Info.plist b/URLShare/Info.plist
index 36d4d3e..ea3b19a 100644
--- a/URLShare/Info.plist
+++ b/URLShare/Info.plist
@@ -5,17 +5,17 @@
NSExtension
NSExtensionAttributes
-
- NSExtensionActivationRule
-
- NSExtensionActivationSupportsWebURLWithMaxCount
- 1
-
-
+
+ NSExtensionActivationRule
+
+ NSExtensionActivationSupportsWebURLWithMaxCount
+ 1
+
+
NSExtensionPointIdentifier
com.apple.share-services
- NSExtensionPrincipalClass
- $(PRODUCT_MODULE_NAME).ShareViewController
+ NSExtensionPrincipalClass
+ $(PRODUCT_MODULE_NAME).ShareViewController
diff --git a/readeck.xcodeproj/project.pbxproj b/readeck.xcodeproj/project.pbxproj
index 71e7733..0878df2 100644
--- a/readeck.xcodeproj/project.pbxproj
+++ b/readeck.xcodeproj/project.pbxproj
@@ -415,6 +415,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = URLShare;
+ INFOPLIST_KEY_LSApplicationCategoryType = "";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
LD_RUNPATH_SEARCH_PATHS = (
@@ -423,7 +424,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = de.ilyashallak.readeck.URLShare;
+ PRODUCT_BUNDLE_IDENTIFIER = de.ilyashallak.readeck2.URLShare;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
@@ -442,6 +443,7 @@
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = URLShare/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = URLShare;
+ INFOPLIST_KEY_LSApplicationCategoryType = "";
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 18.1;
LD_RUNPATH_SEARCH_PATHS = (
@@ -450,7 +452,7 @@
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = de.ilyashallak.readeck.URLShare;
+ PRODUCT_BUNDLE_IDENTIFIER = de.ilyashallak.readeck2.URLShare;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
@@ -581,6 +583,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
@@ -605,8 +608,9 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = de.ilyashallak.readeck;
+ PRODUCT_BUNDLE_IDENTIFIER = de.ilyashallak.readeck2;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -622,6 +626,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\"";
@@ -646,8 +651,9 @@
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 1.0;
- PRODUCT_BUNDLE_IDENTIFIER = de.ilyashallak.readeck;
+ PRODUCT_BUNDLE_IDENTIFIER = de.ilyashallak.readeck2;
PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx xros xrsimulator";
SWIFT_EMIT_LOC_STRINGS = YES;
diff --git a/readeck/Assets.xcassets/green.colorset/Contents.json b/readeck/Assets.xcassets/green.colorset/Contents.json
new file mode 100644
index 0000000..f5a0250
--- /dev/null
+++ b/readeck/Assets.xcassets/green.colorset/Contents.json
@@ -0,0 +1,38 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x5B",
+ "green" : "0x4D",
+ "red" : "0x00"
+ }
+ },
+ "idiom" : "universal"
+ },
+ {
+ "appearances" : [
+ {
+ "appearance" : "luminosity",
+ "value" : "dark"
+ }
+ ],
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0x5B",
+ "green" : "0x4D",
+ "red" : "0x00"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/readeck/Assets.xcassets/placeholder.imageset/Bildschirmfoto 2025-07-03 um 15.09.44.png b/readeck/Assets.xcassets/placeholder.imageset/Bildschirmfoto 2025-07-03 um 15.09.44.png
new file mode 100644
index 0000000..5666b2e
Binary files /dev/null and b/readeck/Assets.xcassets/placeholder.imageset/Bildschirmfoto 2025-07-03 um 15.09.44.png differ
diff --git a/readeck/Assets.xcassets/placeholder.imageset/Contents.json b/readeck/Assets.xcassets/placeholder.imageset/Contents.json
new file mode 100644
index 0000000..ed3dfcc
--- /dev/null
+++ b/readeck/Assets.xcassets/placeholder.imageset/Contents.json
@@ -0,0 +1,21 @@
+{
+ "images" : [
+ {
+ "filename" : "Bildschirmfoto 2025-07-03 um 15.09.44.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/readeck/Assets.xcassets/readeck.imageset/Contents.json b/readeck/Assets.xcassets/readeck.imageset/Contents.json
new file mode 100644
index 0000000..59c7294
--- /dev/null
+++ b/readeck/Assets.xcassets/readeck.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "readeck2@1x.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "readeck2@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "readeck2@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/readeck/Assets.xcassets/readeck.imageset/readeck2@1x.png b/readeck/Assets.xcassets/readeck.imageset/readeck2@1x.png
new file mode 100644
index 0000000..46d8037
Binary files /dev/null and b/readeck/Assets.xcassets/readeck.imageset/readeck2@1x.png differ
diff --git a/readeck/Assets.xcassets/readeck.imageset/readeck2@2x.png b/readeck/Assets.xcassets/readeck.imageset/readeck2@2x.png
new file mode 100644
index 0000000..2dd65d6
Binary files /dev/null and b/readeck/Assets.xcassets/readeck.imageset/readeck2@2x.png differ
diff --git a/readeck/Assets.xcassets/readeck.imageset/readeck2@3x.png b/readeck/Assets.xcassets/readeck.imageset/readeck2@3x.png
new file mode 100644
index 0000000..32d17ea
Binary files /dev/null and b/readeck/Assets.xcassets/readeck.imageset/readeck2@3x.png differ
diff --git a/readeck/Data/API/API.swift b/readeck/Data/API/API.swift
index ba720c3..ba959fc 100644
--- a/readeck/Data/API/API.swift
+++ b/readeck/Data/API/API.swift
@@ -9,13 +9,14 @@ import Foundation
protocol PAPI {
var tokenProvider: TokenProvider { get }
- func login(username: String, password: String) async throws -> UserDto
+ func login(endpoint: String, username: String, password: String) async throws -> UserDto
func getBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?) async throws -> BookmarksPageDto
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
+ func searchBookmarks(search: String) async throws -> BookmarksPageDto
}
class API: PAPI {
@@ -28,10 +29,12 @@ class API: PAPI {
private var baseURL: String {
get async {
- if let cached = cachedBaseURL {
+ if let cached = cachedBaseURL, cached.isEmpty == false {
return cached
}
- let url = await tokenProvider.getEndpoint()
+ guard let url = await tokenProvider.getEndpoint() else {
+ return ""
+ }
cachedBaseURL = url
return url
}
@@ -154,20 +157,25 @@ class API: PAPI {
return string
}
- func login(username: String, password: String) async throws -> UserDto {
+ func login(endpoint: String, username: String, password: String) async throws -> UserDto {
let loginRequest = LoginRequestDto(application: "api doc", username: username, password: password)
let requestData = try JSONEncoder().encode(loginRequest)
-
- let userDto = try await makeJSONRequest(
- endpoint: "/api/auth",
- method: .POST,
- body: requestData,
- responseType: UserDto.self
- )
-
- // Token automatisch speichern nach erfolgreichem Login
- await tokenProvider.setToken(userDto.token)
-
+ guard let url = URL(string: endpoint + "/api/auth") else {
+ throw APIError.invalidURL
+ }
+ var request = URLRequest(url: url)
+ request.httpMethod = "POST"
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ request.httpBody = requestData
+ let (data, response) = try await URLSession.shared.data(for: request)
+ guard let httpResponse = response as? HTTPURLResponse else {
+ throw APIError.invalidResponse
+ }
+ guard 200...299 ~= httpResponse.statusCode else {
+ throw APIError.serverError(httpResponse.statusCode)
+ }
+ let userDto = try JSONDecoder().decode(UserDto.self, from: data)
+ // Token NICHT automatisch speichern, da Settings noch nicht existieren
return userDto
}
@@ -322,6 +330,26 @@ class API: PAPI {
throw APIError.serverError(httpResponse.statusCode)
}
}
+
+ func searchBookmarks(search: String) async throws -> BookmarksPageDto {
+ let endpoint = "/api/bookmarks?search=\(search.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")"
+ let (bookmarks, response) = try await makeJSONRequestWithHeaders(
+ endpoint: endpoint,
+ responseType: [BookmarkDto].self
+ )
+ let currentPage = response.value(forHTTPHeaderField: "Current-Page").flatMap { Int($0) }
+ let totalCount = response.value(forHTTPHeaderField: "Total-Count").flatMap { Int($0) }
+ let totalPages = response.value(forHTTPHeaderField: "Total-Pages").flatMap { Int($0) }
+ let linksHeader = response.value(forHTTPHeaderField: "Link")
+ let links = linksHeader?.components(separatedBy: ",")
+ return BookmarksPageDto(
+ bookmarks: bookmarks,
+ currentPage: currentPage,
+ totalCount: totalCount,
+ totalPages: totalPages,
+ links: links
+ )
+ }
}
enum HTTPMethod: String {
diff --git a/readeck/Data/Repository/AuthRepository.swift b/readeck/Data/Repository/AuthRepository.swift
index 09a2097..0dd24b9 100644
--- a/readeck/Data/Repository/AuthRepository.swift
+++ b/readeck/Data/Repository/AuthRepository.swift
@@ -9,8 +9,8 @@ class AuthRepository: PAuthRepository {
self.settingsRepository = settingsRepository
}
- func login(username: String, password: String) async throws -> User {
- let userDto = try await api.login(username: username, password: password)
+ func login(endpoint: String, username: String, password: String) async throws -> User {
+ let userDto = try await api.login(endpoint: endpoint, username: username, password: password)
// Token wird automatisch von der API gespeichert
return User(id: userDto.id, token: userDto.token)
}
diff --git a/readeck/Data/Repository/BookmarksRepository.swift b/readeck/Data/Repository/BookmarksRepository.swift
index 8b448e5..1e5768c 100644
--- a/readeck/Data/Repository/BookmarksRepository.swift
+++ b/readeck/Data/Repository/BookmarksRepository.swift
@@ -7,6 +7,7 @@ protocol PBookmarksRepository {
func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String
func updateBookmark(id: String, updateRequest: BookmarkUpdateRequest) async throws
func deleteBookmark(id: String) async throws
+ func searchBookmarks(search: String) async throws -> BookmarksPage
}
class BookmarksRepository: PBookmarksRepository {
@@ -82,6 +83,11 @@ class BookmarksRepository: PBookmarksRepository {
try await api.updateBookmark(id: id, updateRequest: dto)
}
+
+ func searchBookmarks(search: String) async throws -> BookmarksPage {
+ let bookmarkDtos = try await api.searchBookmarks(search: search)
+ return bookmarkDtos.toDomain()
+ }
}
struct BookmarkDetail {
diff --git a/readeck/Data/Repository/SettingsRepository.swift b/readeck/Data/Repository/SettingsRepository.swift
index 79685fe..92c60ce 100644
--- a/readeck/Data/Repository/SettingsRepository.swift
+++ b/readeck/Data/Repository/SettingsRepository.swift
@@ -28,6 +28,7 @@ protocol PSettingsRepository {
func saveUsername(_ username: String) async throws
func savePassword(_ password: String) async throws
func saveHasFinishedSetup(_ hasFinishedSetup: Bool) async throws
+ func saveServerSettings(endpoint: String, username: String, password: String, token: String) async throws
var hasFinishedSetup: Bool { get }
}
@@ -170,6 +171,40 @@ class SettingsRepository: PSettingsRepository {
}
}
+ func saveServerSettings(endpoint: String, username: String, password: String, token: String) async throws {
+ let context = coreDataManager.context
+ return try await withCheckedThrowingContinuation { continuation in
+ context.perform {
+ do {
+ let fetchRequest: NSFetchRequest = SettingEntity.fetchRequest()
+ fetchRequest.fetchLimit = 1
+ let settingEntities = try context.fetch(fetchRequest)
+ let settingEntity: SettingEntity
+ if let existing = settingEntities.first {
+ settingEntity = existing
+ } else {
+ settingEntity = SettingEntity(context: context)
+ }
+ settingEntity.endpoint = endpoint
+ settingEntity.username = username
+ settingEntity.password = password
+ settingEntity.token = token
+ try context.save()
+ // Wenn ein Token gespeichert wird, Setup als abgeschlossen markieren
+ if !token.isEmpty {
+ self.hasFinishedSetup = true
+ DispatchQueue.main.async {
+ NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
+ }
+ }
+ continuation.resume()
+ } catch {
+ continuation.resume(throwing: error)
+ }
+ }
+ }
+ }
+
func saveUsername(_ username: String) async throws {
let context = coreDataManager.context
diff --git a/readeck/Data/TokenProvider.swift b/readeck/Data/TokenProvider.swift
index 6135d3c..960217b 100644
--- a/readeck/Data/TokenProvider.swift
+++ b/readeck/Data/TokenProvider.swift
@@ -2,7 +2,7 @@ import Foundation
protocol TokenProvider {
func getToken() async -> String?
- func getEndpoint() async -> String
+ func getEndpoint() async -> String?
func setToken(_ token: String) async
func clearToken() async
}
@@ -29,10 +29,10 @@ class CoreDataTokenProvider: TokenProvider {
return cachedSettings?.token
}
- func getEndpoint() async -> String {
+ func getEndpoint() async -> String? {
await loadSettingsIfNeeded()
// Basis-URL ohne /api Suffix, da es in der API-Klasse hinzugefügt wird
- return cachedSettings?.endpoint ?? "https://keep.mnk.any64.de"
+ return cachedSettings?.endpoint
}
func setToken(_ token: String) async {
@@ -56,4 +56,4 @@ class CoreDataTokenProvider: TokenProvider {
print("Failed to clear settings: \(error)")
}
}
-}
\ No newline at end of file
+}
diff --git a/readeck/Domain/Model/Bookmark.swift b/readeck/Domain/Model/Bookmark.swift
index f008481..cd7b34d 100644
--- a/readeck/Domain/Model/Bookmark.swift
+++ b/readeck/Domain/Model/Bookmark.swift
@@ -55,3 +55,12 @@ struct ImageResource {
let height: Int
let width: Int
}
+
+extension Bookmark: Hashable, Identifiable {
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(id)
+ }
+ static func == (lhs: Bookmark, rhs: Bookmark) -> Bool {
+ lhs.id == rhs.id
+ }
+}
diff --git a/readeck/Domain/Protocols/PAuthRepository.swift b/readeck/Domain/Protocols/PAuthRepository.swift
index feff181..7aa5b16 100644
--- a/readeck/Domain/Protocols/PAuthRepository.swift
+++ b/readeck/Domain/Protocols/PAuthRepository.swift
@@ -7,7 +7,7 @@
protocol PAuthRepository {
- func login(username: String, password: String) async throws -> User
+ func login(endpoint: String, username: String, password: String) async throws -> User
func logout() async throws
func getCurrentSettings() async throws -> Settings?
}
diff --git a/readeck/Domain/UseCase/LoginUseCase.swift b/readeck/Domain/UseCase/LoginUseCase.swift
index e181bcf..a6e176f 100644
--- a/readeck/Domain/UseCase/LoginUseCase.swift
+++ b/readeck/Domain/UseCase/LoginUseCase.swift
@@ -1,4 +1,3 @@
-
class LoginUseCase {
private let repository: PAuthRepository
@@ -6,7 +5,7 @@ class LoginUseCase {
self.repository = repository
}
- func execute(username: String, password: String) async throws -> User {
- return try await repository.login(username: username, password: password)
+ func execute(endpoint: String, username: String, password: String) async throws -> User {
+ return try await repository.login(endpoint: endpoint, username: username, password: password)
}
}
diff --git a/readeck/Domain/UseCase/SaveServerSettingsUseCase.swift b/readeck/Domain/UseCase/SaveServerSettingsUseCase.swift
new file mode 100644
index 0000000..7790f6b
--- /dev/null
+++ b/readeck/Domain/UseCase/SaveServerSettingsUseCase.swift
@@ -0,0 +1,13 @@
+import Foundation
+
+class SaveServerSettingsUseCase {
+ private let repository: PSettingsRepository
+
+ init(repository: PSettingsRepository) {
+ self.repository = repository
+ }
+
+ func execute(endpoint: String, username: String, password: String, token: String) async throws {
+ try await repository.saveServerSettings(endpoint: endpoint, username: username, password: password, token: token)
+ }
+}
\ No newline at end of file
diff --git a/readeck/Domain/UseCase/SearchBookmarksUseCase.swift b/readeck/Domain/UseCase/SearchBookmarksUseCase.swift
new file mode 100644
index 0000000..613d2a9
--- /dev/null
+++ b/readeck/Domain/UseCase/SearchBookmarksUseCase.swift
@@ -0,0 +1,13 @@
+import Foundation
+
+class SearchBookmarksUseCase {
+ private let repository: PBookmarksRepository
+
+ init(repository: PBookmarksRepository) {
+ self.repository = repository
+ }
+
+ func execute(search: String) async throws -> BookmarksPage {
+ return try await repository.searchBookmarks(search: search)
+ }
+}
\ No newline at end of file
diff --git a/readeck/Info.plist b/readeck/Info.plist
index 0f559b5..cb46d58 100644
--- a/readeck/Info.plist
+++ b/readeck/Info.plist
@@ -13,5 +13,12 @@
+ UILaunchScreen
+
+ UIColorName
+ green
+ UIImageName
+ readeck
+
diff --git a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift
index 3c70ee8..c46f109 100644
--- a/readeck/UI/BookmarkDetail/BookmarkDetailView.swift
+++ b/readeck/UI/BookmarkDetail/BookmarkDetailView.swift
@@ -7,51 +7,25 @@ struct BookmarkDetailView: View {
@State private var webViewHeight: CGFloat = 300
@State private var showingFontSettings = false
+ private let headerHeight: CGFloat = 260
+
var body: some View {
- ScrollView {
- VStack(alignment: .leading, spacing: 16) {
- // Header mit Bild
- if !viewModel.bookmarkDetail.imageUrl.isEmpty {
- AsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl)) { image in
- image
- .resizable()
- .aspectRatio(contentMode: .fill)
- } placeholder: {
- Rectangle()
- .fill(Color.gray.opacity(0.3))
- .frame(height: 200)
- }
- .frame(height: 200)
- .clipped()
- }
-
- VStack(alignment: .leading, spacing: 6) {
- // Titel
- Text(viewModel.bookmarkDetail.title)
- .font(.largeTitle)
- .fontWeight(.bold)
-
- // Meta-Informationen
- metaInfoSection
-
- Divider()
-
- // Artikel-Inhalt mit WebView
- if let settings = viewModel.settings, !viewModel.articleContent.isEmpty {
- WebView(htmlContent: viewModel.articleContent, settings: settings) { height in
- webViewHeight = height
- }
- .frame(height: webViewHeight)
- } else if viewModel.isLoadingArticle {
- ProgressView("Lade Artikel...")
- .frame(maxWidth: .infinity, alignment: .center)
- .padding()
+ GeometryReader { geometry in
+ ScrollView {
+ ZStack(alignment: .top) {
+ headerView(geometry: geometry)
+ VStack(alignment: .leading, spacing: 16) {
+ Color.clear.frame(height: viewModel.bookmarkDetail.imageUrl.isEmpty ? 84 : headerHeight)
+ titleSection
+ Divider().padding(.horizontal)
+ contentSection
+ Spacer(minLength: 40)
}
}
- .padding()
}
+ .ignoresSafeArea(edges: .top)
}
- .navigationBarTitleDisplayMode(.inline)
+ .navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
@@ -81,37 +55,95 @@ struct BookmarkDetailView: View {
}
}
+ // MARK: - ViewBuilder
+
+ @ViewBuilder
+ private func headerView(geometry: GeometryProxy) -> some View {
+ if !viewModel.bookmarkDetail.imageUrl.isEmpty {
+ GeometryReader { geo in
+ let offset = geo.frame(in: .global).minY
+ ZStack(alignment: .top) {
+ AsyncImage(url: URL(string: viewModel.bookmarkDetail.imageUrl)) { image in
+ image
+ .resizable()
+ .scaledToFill()
+ .frame(width: geometry.size.width, height: headerHeight + (offset > 0 ? offset : 0))
+ .clipped()
+ .offset(y: (offset > 0 ? -offset : 0))
+ } placeholder: {
+ Rectangle()
+ .fill(Color.gray.opacity(0.4))
+ .frame(width: geometry.size.width, height: headerHeight)
+ }
+ LinearGradient(
+ gradient: Gradient(colors: [Color.black.opacity(0.6), Color.clear]),
+ startPoint: .top,
+ endPoint: .bottom
+ )
+ .frame(height: 120)
+ .frame(maxWidth: .infinity)
+ }
+ }
+ .frame(height: headerHeight)
+ .ignoresSafeArea(edges: .top)
+ }
+ }
+
+ private var titleSection: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ Text(viewModel.bookmarkDetail.title)
+ .font(.title2)
+ .fontWeight(.semibold)
+ .foregroundColor(.primary)
+ .padding(.bottom, 2)
+ .shadow(color: Color.black.opacity(0.15), radius: 2, x: 0, y: 1)
+ metaInfoSection
+ }
+ .padding(.horizontal)
+ }
+
+ @ViewBuilder
+ private var contentSection: some View {
+ if let settings = viewModel.settings, !viewModel.articleContent.isEmpty {
+ WebView(htmlContent: viewModel.articleContent, settings: settings) { height in
+ webViewHeight = height
+ }
+ .frame(height: webViewHeight)
+ .cornerRadius(14)
+ .padding(.horizontal)
+ } else if viewModel.isLoadingArticle {
+ ProgressView("Lade Artikel...")
+ .frame(maxWidth: .infinity, alignment: .center)
+ .padding()
+ } else {
+ Button(action: {
+ SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url)
+ }) {
+ HStack {
+ Image(systemName: "safari")
+ Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Original Seite") + " öffnen")
+ }
+ .font(.title3.bold())
+ .frame(maxWidth: .infinity)
+ }
+ .buttonStyle(.borderedProminent)
+ .padding(.horizontal)
+ .padding(.top, 32)
+ }
+ }
+
private var metaInfoSection: some View {
VStack(alignment: .leading, spacing: 8) {
if !viewModel.bookmarkDetail.authors.isEmpty {
- HStack {
- Image(systemName: "person")
- Text(viewModel.bookmarkDetail.authors.joined(separator: ", "))
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
+ metaRow(icon: "person", text: (viewModel.bookmarkDetail.authors.count > 1 ? "Autor:innen: " : "Autor: ") + viewModel.bookmarkDetail.authors.joined(separator: ", "))
}
-
- HStack {
- Image(systemName: "calendar")
- Text("Erstellt: \(formatDate(viewModel.bookmarkDetail.created))")
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
-
- HStack {
- Image(systemName: "textformat")
- Text("\(viewModel.bookmarkDetail.wordCount ?? 0) Wörter • \(viewModel.bookmarkDetail.readingTime ?? 0) min Lesezeit")
- .font(.subheadline)
- .foregroundColor(.secondary)
- }
-
- HStack {
- Image(systemName: "safari")
+ metaRow(icon: "calendar", text: formatDate(viewModel.bookmarkDetail.created))
+ metaRow(icon: "textformat", text: "\(viewModel.bookmarkDetail.wordCount ?? 0) Wörter • \(viewModel.bookmarkDetail.readingTime ?? 0) min Lesezeit")
+ metaRow(icon: "safari") {
Button(action: {
SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url)
}) {
- Text("Original Seite öffnen")
+ Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Original Seite") + " öffnen")
.font(.subheadline)
.foregroundColor(.secondary)
}
@@ -119,23 +151,36 @@ struct BookmarkDetailView: View {
}
}
+ // ViewBuilder für Meta-Infos
+ @ViewBuilder
+ private func metaRow(icon: String, text: String) -> some View {
+ HStack {
+ Image(systemName: icon)
+ Text(text)
+ .font(.subheadline)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ @ViewBuilder
+ private func metaRow(icon: String, @ViewBuilder content: () -> some View) -> some View {
+ HStack {
+ Image(systemName: icon)
+ content()
+ }
+ }
+
private func formatDate(_ dateString: String) -> String {
- // Erstelle einen Formatter für das ISO8601-Format mit Millisekunden
let isoFormatter = ISO8601DateFormatter()
isoFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
-
- // Fallback für Format ohne Millisekunden
let isoFormatterNoMillis = ISO8601DateFormatter()
isoFormatterNoMillis.formatOptions = [.withInternetDateTime]
-
- // Versuche beide Formate
var date: Date?
if let parsedDate = isoFormatter.date(from: dateString) {
date = parsedDate
} else if let parsedDate = isoFormatterNoMillis.date(from: dateString) {
date = parsedDate
}
-
if let date = date {
let displayFormatter = DateFormatter()
displayFormatter.dateStyle = .medium
@@ -143,7 +188,6 @@ struct BookmarkDetailView: View {
displayFormatter.locale = Locale(identifier: "de_DE")
return displayFormatter.string(from: date)
}
-
return dateString
}
}
diff --git a/readeck/UI/Bookmarks/BookmarkCardView.swift b/readeck/UI/Bookmarks/BookmarkCardView.swift
index f7182f6..c708a63 100644
--- a/readeck/UI/Bookmarks/BookmarkCardView.swift
+++ b/readeck/UI/Bookmarks/BookmarkCardView.swift
@@ -15,15 +15,13 @@ struct BookmarkCardView: View {
image
.resizable()
.aspectRatio(contentMode: .fill)
+ .frame(height: 120)
} placeholder: {
- Rectangle()
- .fill(Color.gray.opacity(0.2))
- .overlay {
- Image(systemName: "photo")
- .foregroundColor(.gray)
- }
+ Image("placeholder")
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(height: 120)
}
- .frame(height: 120)
.clipShape(RoundedRectangle(cornerRadius: 8))
VStack(alignment: .leading, spacing: 4) {
@@ -60,7 +58,7 @@ struct BookmarkCardView: View {
}
HStack {
- Label("Original Seite öffnen", systemImage: "safari")
+ Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Seite") + " öffnen", systemImage: "safari")
.onTapGesture {
SafariUtil.openInSafari(url: bookmark.url)
}
diff --git a/readeck/UI/Bookmarks/BookmarksView.swift b/readeck/UI/Bookmarks/BookmarksView.swift
index fa756ff..533225c 100644
--- a/readeck/UI/Bookmarks/BookmarksView.swift
+++ b/readeck/UI/Bookmarks/BookmarksView.swift
@@ -92,8 +92,6 @@ struct BookmarksView: View {
)
}
}
- .searchable(
- text: $viewModel.searchQuery, placement: .automatic, prompt: "Search...")
}
// FAB Button - nur bei "Ungelesen" anzeigen
diff --git a/readeck/UI/DefaultUseCaseFactory.swift b/readeck/UI/DefaultUseCaseFactory.swift
index 2e0b60a..f137157 100644
--- a/readeck/UI/DefaultUseCaseFactory.swift
+++ b/readeck/UI/DefaultUseCaseFactory.swift
@@ -11,6 +11,8 @@ protocol UseCaseFactory {
func makeDeleteBookmarkUseCase() -> DeleteBookmarkUseCase
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase
func makeLogoutUseCase() -> LogoutUseCase
+ func makeSearchBookmarksUseCase() -> SearchBookmarksUseCase
+ func makeSaveServerSettingsUseCase() -> SaveServerSettingsUseCase
}
class DefaultUseCaseFactory: UseCaseFactory {
@@ -68,4 +70,12 @@ class DefaultUseCaseFactory: UseCaseFactory {
func makeCreateBookmarkUseCase() -> CreateBookmarkUseCase {
return CreateBookmarkUseCase(repository: bookmarksRepository)
}
+
+ func makeSearchBookmarksUseCase() -> SearchBookmarksUseCase {
+ return SearchBookmarksUseCase(repository: bookmarksRepository)
+ }
+
+ func makeSaveServerSettingsUseCase() -> SaveServerSettingsUseCase {
+ return SaveServerSettingsUseCase(repository: SettingsRepository())
+ }
}
diff --git a/readeck/UI/Menu/PadSidebarView.swift b/readeck/UI/Menu/PadSidebarView.swift
index 6999d5b..66d67a0 100644
--- a/readeck/UI/Menu/PadSidebarView.swift
+++ b/readeck/UI/Menu/PadSidebarView.swift
@@ -11,10 +11,12 @@ struct PadSidebarView: View {
@State private var selectedTab: SidebarTab = .unread
@State private var selectedBookmark: Bookmark?
+ private let sidebarTabs: [SidebarTab] = [.search, .all, .unread, .favorite, .archived, .article, .videos, .pictures, .tags]
+
var body: some View {
NavigationSplitView {
List {
- ForEach(SidebarTab.allCases.filter { $0 != .settings }, id: \.self) { tab in
+ ForEach(sidebarTabs, id: \.self) { tab in
Button(action: {
selectedTab = tab
}) {
@@ -26,11 +28,11 @@ struct PadSidebarView: View {
.listRowBackground(selectedTab == tab ? Color.accentColor.opacity(0.15) : Color.clear)
if tab == .archived {
- Spacer(minLength: 20)
+ Spacer()
}
if tab == .pictures {
- Spacer(minLength: 30)
+ Spacer()
Divider()
Spacer()
}
@@ -57,6 +59,8 @@ struct PadSidebarView: View {
} content: {
Group {
switch selectedTab {
+ case .search:
+ SearchBookmarksView(selectedBookmark: $selectedBookmark)
case .all:
BookmarksView(state: .all, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
case .unread:
diff --git a/readeck/UI/Menu/PhoneTabView.swift b/readeck/UI/Menu/PhoneTabView.swift
index f6b223f..6f845bc 100644
--- a/readeck/UI/Menu/PhoneTabView.swift
+++ b/readeck/UI/Menu/PhoneTabView.swift
@@ -9,7 +9,7 @@ import SwiftUI
struct PhoneTabView: View {
private let mainTabs: [SidebarTab] = [.all, .unread, .favorite, .archived]
- private let moreTabs: [SidebarTab] = [.article, .videos, .pictures, .tags, .settings]
+ private let moreTabs: [SidebarTab] = [.search, .article, .videos, .pictures, .tags, .settings]
@State private var selectedMoreTab: SidebarTab? = nil
@State private var selectedTabIndex: Int = 0
@@ -42,7 +42,6 @@ struct PhoneTabView: View {
}
.tag(mainTabs.count)
.onAppear {
- // Wenn der Mehr-Tab aktiv wird und wir in einer Detailansicht sind, zurücksetzen
if selectedTabIndex == mainTabs.count && selectedMoreTab != nil {
selectedMoreTab = nil
}
@@ -62,6 +61,8 @@ struct PhoneTabView: View {
BookmarksView(state: .favorite, type: [.article], selectedBookmark: .constant(nil))
case .archived:
BookmarksView(state: .archived, type: [.article], selectedBookmark: .constant(nil))
+ case .search:
+ SearchBookmarksView(selectedBookmark: .constant(nil))
case .settings:
SettingsView()
case .article:
diff --git a/readeck/UI/Menu/SidebarTab.swift b/readeck/UI/Menu/SidebarTab.swift
index 424470b..a51c9b5 100644
--- a/readeck/UI/Menu/SidebarTab.swift
+++ b/readeck/UI/Menu/SidebarTab.swift
@@ -6,7 +6,7 @@
//
enum SidebarTab: Hashable, CaseIterable, Identifiable {
- case all, unread, favorite, archived, settings, article, videos, pictures, tags
+ case search, all, unread, favorite, archived, article, videos, pictures, tags, settings
var id: Self { self }
@@ -16,6 +16,7 @@ enum SidebarTab: Hashable, CaseIterable, Identifiable {
case .unread: return "Ungelesen"
case .favorite: return "Favoriten"
case .archived: return "Archiv"
+ case .search: return "Suche"
case .settings: return "Einstellungen"
case .article: return "Artikel"
case .videos: return "Videos"
@@ -29,6 +30,7 @@ enum SidebarTab: Hashable, CaseIterable, Identifiable {
case .unread: return "house"
case .favorite: return "heart"
case .archived: return "archivebox"
+ case .search: return "magnifyingglass"
case .settings: return "gear"
case .all: return "list.bullet"
case .article: return "doc.plaintext"
diff --git a/readeck/UI/Search/SearchBookmarksView.swift b/readeck/UI/Search/SearchBookmarksView.swift
new file mode 100644
index 0000000..dd673fa
--- /dev/null
+++ b/readeck/UI/Search/SearchBookmarksView.swift
@@ -0,0 +1,79 @@
+import SwiftUI
+
+struct SearchBookmarksView: View {
+ @State private var viewModel = SearchBookmarksViewModel()
+ @FocusState private var searchFieldIsFocused: Bool
+ @State private var selectedBookmarkId: String?
+ @Binding var selectedBookmark: Bookmark?
+
+ var body: some View {
+ VStack(spacing: 0) {
+ HStack {
+ Image(systemName: "magnifyingglass")
+ .foregroundColor(.gray)
+ TextField("Suchbegriff eingeben...", text: $viewModel.searchQuery)
+ .focused($searchFieldIsFocused)
+ .textFieldStyle(PlainTextFieldStyle())
+ .autocapitalization(.none)
+ .disableAutocorrection(true)
+ if !viewModel.searchQuery.isEmpty {
+ Button(action: {
+ viewModel.searchQuery = ""
+ searchFieldIsFocused = true
+ }) {
+ Image(systemName: "xmark.circle.fill")
+ .foregroundColor(.gray)
+ }
+ .buttonStyle(.plain)
+ }
+ }
+ .padding(10)
+ .background(Color(.systemGray6))
+ .cornerRadius(12)
+ .padding([.horizontal, .top])
+
+ if viewModel.isLoading {
+ ProgressView("Suche...")
+ .padding()
+ }
+
+ if let error = viewModel.errorMessage {
+ Text(error)
+ .foregroundColor(.red)
+ .padding()
+ }
+
+ if let bookmarks = viewModel.bookmarks?.bookmarks, !bookmarks.isEmpty {
+ List(bookmarks) { bookmark in
+ Button(action: {
+
+ if UIDevice.isPhone {
+ selectedBookmarkId = bookmark.id
+ } else {
+ if selectedBookmark?.id == bookmark.id {
+ selectedBookmark = nil
+ DispatchQueue.main.async {
+ selectedBookmark = bookmark
+ }
+ } else {
+ selectedBookmark = bookmark
+ }
+ }
+ }) {
+ BookmarkCardView(bookmark: bookmark, currentState: .all, onArchive: {_ in }, onDelete: {_ in }, onToggleFavorite: {_ in })
+ .listRowBackground(Color(.systemBackground))
+ .padding(.vertical, 4)
+ }
+ .buttonStyle(.plain)
+ .listRowSeparator(.hidden)
+ }
+ .listStyle(.plain)
+ } else if !viewModel.isLoading && viewModel.bookmarks != nil {
+ ContentUnavailableView("Keine Ergebnisse", systemImage: "magnifyingglass", description: Text("Keine Bookmarks gefunden."))
+ .padding()
+ }
+ Spacer()
+ }
+ .navigationTitle("Suche")
+ }
+}
diff --git a/readeck/UI/Search/SearchBookmarksViewModel.swift b/readeck/UI/Search/SearchBookmarksViewModel.swift
new file mode 100644
index 0000000..649329b
--- /dev/null
+++ b/readeck/UI/Search/SearchBookmarksViewModel.swift
@@ -0,0 +1,49 @@
+import Foundation
+import Combine
+import SwiftUI
+
+@Observable
+class SearchBookmarksViewModel {
+ private let searchBookmarksUseCase = DefaultUseCaseFactory.shared.makeSearchBookmarksUseCase()
+
+ var searchQuery: String = "" {
+ didSet {
+ throttleSearch()
+ }
+ }
+ var bookmarks: BookmarksPage? = nil
+ var isLoading = false
+ var errorMessage: String? = nil
+
+ private var searchWorkItem: DispatchWorkItem?
+
+ private func throttleSearch() {
+ searchWorkItem?.cancel()
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let self = self else { return }
+ Task {
+ await self.search()
+ }
+ }
+ searchWorkItem = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: workItem)
+ }
+
+ @MainActor
+ func search() async {
+ guard !searchQuery.isEmpty else {
+ bookmarks = nil
+ return
+ }
+ isLoading = true
+ errorMessage = nil
+ do {
+ let result = try await searchBookmarksUseCase.execute(search: searchQuery)
+ bookmarks = result
+ } catch {
+ errorMessage = "Fehler bei der Suche"
+ bookmarks = nil
+ }
+ isLoading = false
+ }
+}
\ No newline at end of file
diff --git a/readeck/UI/Settings/SettingsServerView.swift b/readeck/UI/Settings/SettingsServerView.swift
index 859d024..d7a3bde 100644
--- a/readeck/UI/Settings/SettingsServerView.swift
+++ b/readeck/UI/Settings/SettingsServerView.swift
@@ -6,12 +6,9 @@
//
import SwiftUI
-// SectionHeader wird jetzt zentral importiert
struct SettingsServerView: View {
@State private var viewModel = SettingsServerViewModel()
- @State private var isTesting: Bool = false
- @State private var connectionTestSuccess: Bool = false
@State private var showingLogoutAlert = false
var body: some View {
@@ -41,7 +38,6 @@ struct SettingsServerView: View {
.onChange(of: viewModel.endpoint) {
if viewModel.isSetupMode {
viewModel.clearMessages()
- connectionTestSuccess = false
}
}
}
@@ -56,7 +52,6 @@ struct SettingsServerView: View {
.onChange(of: viewModel.username) {
if viewModel.isSetupMode {
viewModel.clearMessages()
- connectionTestSuccess = false
}
}
}
@@ -69,7 +64,6 @@ struct SettingsServerView: View {
.onChange(of: viewModel.password) {
if viewModel.isSetupMode {
viewModel.clearMessages()
- connectionTestSuccess = false
}
}
}
@@ -106,34 +100,11 @@ struct SettingsServerView: View {
}
}
- // Action Buttons
if viewModel.isSetupMode {
VStack(spacing: 10) {
Button(action: {
Task {
- await testConnection()
- }
- }) {
- HStack {
- if isTesting {
- ProgressView()
- .scaleEffect(0.8)
- .progressViewStyle(CircularProgressViewStyle(tint: .white))
- }
- Text(isTesting ? "Teste Verbindung..." : "Verbindung testen")
- .fontWeight(.semibold)
- }
- .frame(maxWidth: .infinity)
- .padding()
- .background(viewModel.canLogin ? Color.accentColor : Color.gray)
- .foregroundColor(.white)
- .cornerRadius(10)
- }
- .disabled(!viewModel.canLogin || isTesting || viewModel.isLoading)
-
- Button(action: {
- Task {
- await viewModel.login()
+ await viewModel.saveServerSettings()
}
}) {
HStack {
@@ -142,17 +113,16 @@ struct SettingsServerView: View {
.scaleEffect(0.8)
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
- Text(viewModel.isLoading ? "Anmelde..." : (viewModel.isLoggedIn ? "Erneut anmelden" : "Anmelden"))
+ Text(viewModel.isLoading ? "Speichern..." : (viewModel.isLoggedIn ? "Erneut anmelden & speichern" : "Anmelden & speichern"))
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.padding()
- .background((viewModel.canLogin && connectionTestSuccess) ? Color.blue : Color.gray)
+ .background(viewModel.canLogin ? Color.accentColor : Color.gray)
.foregroundColor(.white)
.cornerRadius(10)
}
- .disabled(!viewModel.canLogin || !connectionTestSuccess || viewModel.isLoading || isTesting)
-
+ .disabled(!viewModel.canLogin || viewModel.isLoading)
Button("Debug-Anmeldung") {
viewModel.username = "admin"
viewModel.password = "Diggah123"
@@ -192,12 +162,6 @@ struct SettingsServerView: View {
await viewModel.loadServerSettings()
}
}
-
- private func testConnection() async {
- isTesting = true
- connectionTestSuccess = await viewModel.testConnection()
- isTesting = false
- }
}
#Preview {
diff --git a/readeck/UI/Settings/SettingsServerViewModel.swift b/readeck/UI/Settings/SettingsServerViewModel.swift
index 209874b..14fe848 100644
--- a/readeck/UI/Settings/SettingsServerViewModel.swift
+++ b/readeck/UI/Settings/SettingsServerViewModel.swift
@@ -4,11 +4,13 @@ import SwiftUI
@Observable
class SettingsServerViewModel {
+
+ // MARK: - Use Cases
+
private let loginUseCase: LoginUseCase
private let logoutUseCase: LogoutUseCase
- private let saveSettingsUseCase: SaveSettingsUseCase
+ private let saveServerSettingsUseCase: SaveServerSettingsUseCase
private let loadSettingsUseCase: LoadSettingsUseCase
- private let settingsRepository: SettingsRepository
// MARK: - Server Settings
var endpoint = ""
@@ -16,21 +18,25 @@ class SettingsServerViewModel {
var password = ""
var isLoading = false
var isLoggedIn = false
+
// MARK: - Messages
var errorMessage: String?
var successMessage: String?
+ private var hasFinishedSetup: Bool {
+ SettingsRepository().hasFinishedSetup
+ }
+
init() {
let factory = DefaultUseCaseFactory.shared
self.loginUseCase = factory.makeLoginUseCase()
self.logoutUseCase = factory.makeLogoutUseCase()
- self.saveSettingsUseCase = factory.makeSaveSettingsUseCase()
+ self.saveServerSettingsUseCase = factory.makeSaveServerSettingsUseCase()
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
- self.settingsRepository = SettingsRepository()
}
var isSetupMode: Bool {
- !settingsRepository.hasFinishedSetup
+ !hasFinishedSetup
}
@MainActor
@@ -49,63 +55,25 @@ class SettingsServerViewModel {
@MainActor
func saveServerSettings() async {
- do {
- try await saveSettingsUseCase.execute(
- endpoint: endpoint,
- username: username,
- password: password
- )
- successMessage = "Server-Einstellungen gespeichert"
- } catch {
- errorMessage = "Fehler beim Speichern der Server-Einstellungen"
- }
- }
-
- @MainActor
- func testConnection() async -> Bool {
guard canLogin else {
errorMessage = "Bitte füllen Sie alle Felder aus."
- return false
+ return
}
-
clearMessages()
-
- do {
- // Test login without saving settings
- let _ = try await loginUseCase.execute(
- username: username.trimmingCharacters(in: .whitespacesAndNewlines),
- password: password
- )
-
-
- successMessage = "Verbindung erfolgreich getestet! ✓"
-
- return true
-
- } catch {
- errorMessage = "Verbindungstest fehlgeschlagen: \(error.localizedDescription)"
- }
-
- return false
- }
-
- @MainActor
- func login() async {
isLoading = true
- errorMessage = nil
- successMessage = nil
+ defer { isLoading = false }
do {
- let _ = try await loginUseCase.execute(username: username, password: password)
+ let user = try await loginUseCase.execute(endpoint: endpoint, username: username.trimmingCharacters(in: .whitespacesAndNewlines), password: password)
+ try await saveServerSettingsUseCase.execute(endpoint: endpoint, username: username, password: password, token: user.token)
isLoggedIn = true
- successMessage = "Erfolgreich angemeldet"
- try await settingsRepository.saveHasFinishedSetup(true)
+ successMessage = "Server-Einstellungen gespeichert und erfolgreich angemeldet."
+ try await SettingsRepository().saveHasFinishedSetup(true)
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
await DefaultUseCaseFactory.shared.refreshConfiguration()
} catch {
- errorMessage = "Anmeldung fehlgeschlagen"
+ errorMessage = "Verbindung oder Anmeldung fehlgeschlagen: \(error.localizedDescription)"
isLoggedIn = false
}
- isLoading = false
}
@MainActor
@@ -125,9 +93,6 @@ class SettingsServerViewModel {
successMessage = nil
}
- var canSave: Bool {
- !endpoint.isEmpty && !username.isEmpty && !password.isEmpty
- }
var canLogin: Bool {
!username.isEmpty && !password.isEmpty
}
diff --git a/readeck/Utils/SafariUtil.swift b/readeck/Utils/SafariUtil.swift
index f1c9ff8..de2966f 100644
--- a/readeck/Utils/SafariUtil.swift
+++ b/readeck/Utils/SafariUtil.swift
@@ -23,3 +23,10 @@ class SafariUtil {
}
}
}
+
+struct URLUtil {
+ static func extractDomain(from urlString: String) -> String? {
+ guard let url = URL(string: urlString), let host = url.host else { return nil }
+ return host.replacingOccurrences(of: "www.", with: "")
+ }
+}