Refactor UI navigation and settings management
- Split TabView and Sidebar logic into PhoneTabView, PadSidebarView, SidebarTab, and BookmarkState for better device adaptation - Remove old SettingsViewModel, introduce SettingsGeneralViewModel and SettingsServerViewModel for modular settings - Update BookmarksView and BookmarksViewModel for new paginated and filtered data model - Clean up and modularize settings UI (SettingsGeneralView, SettingsServerView, FontSettingsView) - Remove obsolete files (old TabView, File.swift, SettingsViewModel, etc.) - Add BookmarksPageDto and update related data flow - Various UI/UX improvements and code cleanup BREAKING: Settings and navigation structure refactored, old settings logic removed
This commit is contained in:
parent
7df56687c7
commit
be68538da3
@ -10,7 +10,7 @@ import Foundation
|
||||
protocol PAPI {
|
||||
var tokenProvider: TokenProvider { get }
|
||||
func login(username: String, password: String) async throws -> UserDto
|
||||
func getBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?) async throws -> [BookmarkDto]
|
||||
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
|
||||
@ -37,6 +37,46 @@ class API: PAPI {
|
||||
}
|
||||
}
|
||||
|
||||
private func makeJSONRequestWithHeaders<T: Codable>(
|
||||
endpoint: String,
|
||||
method: HTTPMethod = .GET,
|
||||
body: Data? = nil,
|
||||
responseType: T.Type
|
||||
) async throws -> (T, HTTPURLResponse) {
|
||||
let baseURL = await self.baseURL
|
||||
let fullEndpoint = endpoint.hasPrefix("/api") ? endpoint : "/api\(endpoint)"
|
||||
|
||||
guard let url = URL(string: "\(baseURL)\(fullEndpoint)") else {
|
||||
throw APIError.invalidURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = method.rawValue
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
if let token = await tokenProvider.getToken() {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
}
|
||||
|
||||
if let body = body {
|
||||
request.httpBody = body
|
||||
}
|
||||
|
||||
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 {
|
||||
print("Server Error: \(httpResponse.statusCode) - \(String(data: data, encoding: .utf8) ?? "No response body")")
|
||||
throw APIError.serverError(httpResponse.statusCode)
|
||||
}
|
||||
|
||||
let decoded = try JSONDecoder().decode(T.self, from: data)
|
||||
return (decoded, httpResponse)
|
||||
}
|
||||
|
||||
// Separate Methode für JSON-Requests
|
||||
private func makeJSONRequest<T: Codable>(
|
||||
endpoint: String,
|
||||
@ -131,7 +171,8 @@ class API: PAPI {
|
||||
return userDto
|
||||
}
|
||||
|
||||
func getBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil) async throws -> [BookmarkDto] {
|
||||
// Angepasste getBookmarks-Methode mit Header-Auslesen
|
||||
func getBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil) async throws -> BookmarksPageDto {
|
||||
var endpoint = "/api/bookmarks"
|
||||
var queryItems: [URLQueryItem] = []
|
||||
|
||||
@ -145,6 +186,8 @@ class API: PAPI {
|
||||
queryItems.append(URLQueryItem(name: "is_marked", value: "true"))
|
||||
case .archived:
|
||||
queryItems.append(URLQueryItem(name: "is_archived", value: "true"))
|
||||
case .all:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,15 +202,37 @@ class API: PAPI {
|
||||
queryItems.append(URLQueryItem(name: "search", value: search))
|
||||
}
|
||||
|
||||
// type-Parameter als Array von BookmarkType
|
||||
if let type = type, !type.isEmpty {
|
||||
for t in type {
|
||||
queryItems.append(URLQueryItem(name: "type", value: t.rawValue))
|
||||
}
|
||||
}
|
||||
|
||||
if !queryItems.isEmpty {
|
||||
let queryString = queryItems.map { "\($0.name)=\($0.value ?? "")" }.joined(separator: "&")
|
||||
endpoint += "?\(queryString)"
|
||||
}
|
||||
|
||||
return try await makeJSONRequest(
|
||||
let (bookmarks, response) = try await makeJSONRequestWithHeaders(
|
||||
endpoint: endpoint,
|
||||
responseType: [BookmarkDto].self
|
||||
)
|
||||
|
||||
// Header auslesen
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
func getBookmark(id: String) async throws -> BookmarkDetailDto {
|
||||
|
||||
6
readeck/Data/API/DTOs/CreateBookmarkResponseDto.swift
Normal file
6
readeck/Data/API/DTOs/CreateBookmarkResponseDto.swift
Normal file
@ -0,0 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
struct CreateBookmarkResponseDto: Codable {
|
||||
let message: String
|
||||
let status: Int
|
||||
}
|
||||
11
readeck/Data/API/DTOs/UserDto.swift
Normal file
11
readeck/Data/API/DTOs/UserDto.swift
Normal file
@ -0,0 +1,11 @@
|
||||
import Foundation
|
||||
|
||||
struct UserDto: Codable {
|
||||
let id: String
|
||||
let token: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case token
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
extension BookmarksPageDto {
|
||||
func toDomain() -> BookmarksPage {
|
||||
return BookmarksPage(
|
||||
bookmarks: bookmarks.map { $0.toDomain() },
|
||||
currentPage: currentPage,
|
||||
totalCount: totalCount,
|
||||
totalPages: totalPages,
|
||||
links: links
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - BookmarkDto to Domain Mapping
|
||||
extension BookmarkDto {
|
||||
func toDomain() -> Bookmark {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
protocol PBookmarksRepository {
|
||||
func fetchBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?) async throws -> [Bookmark]
|
||||
func fetchBookmarks(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?) async throws -> BookmarksPage
|
||||
func fetchBookmark(id: String) async throws -> BookmarkDetail
|
||||
func fetchBookmarkArticle(id: String) async throws -> String
|
||||
func createBookmark(createRequest: CreateBookmarkRequest) async throws -> String
|
||||
@ -16,9 +16,9 @@ class BookmarksRepository: PBookmarksRepository {
|
||||
self.api = api
|
||||
}
|
||||
|
||||
func fetchBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil) async throws -> [Bookmark] {
|
||||
let bookmarkDtos = try await api.getBookmarks(state: state, limit: limit, offset: offset, search: search)
|
||||
return bookmarkDtos.map { $0.toDomain() }
|
||||
func fetchBookmarks(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil) async throws -> BookmarksPage {
|
||||
let bookmarkDtos = try await api.getBookmarks(state: state, limit: limit, offset: offset, search: search, type: type)
|
||||
return bookmarkDtos.toDomain()
|
||||
}
|
||||
|
||||
func fetchBookmark(id: String) async throws -> BookmarkDetail {
|
||||
|
||||
@ -1,5 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
struct BookmarksPage {
|
||||
var bookmarks: [Bookmark]
|
||||
let currentPage: Int?
|
||||
let totalCount: Int?
|
||||
let totalPages: Int?
|
||||
let links: [String]?
|
||||
}
|
||||
|
||||
struct Bookmark {
|
||||
let id: String
|
||||
let title: String
|
||||
|
||||
7
readeck/Domain/Model/BookmarkType.swift
Normal file
7
readeck/Domain/Model/BookmarkType.swift
Normal file
@ -0,0 +1,7 @@
|
||||
import Foundation
|
||||
|
||||
public enum BookmarkType: String, CaseIterable, Codable {
|
||||
case article
|
||||
case photo
|
||||
case video
|
||||
}
|
||||
@ -7,13 +7,14 @@ class GetBookmarksUseCase {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func execute(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil) async throws -> [Bookmark] {
|
||||
let allBookmarks = try await repository.fetchBookmarks(state: state, limit: limit, offset: offset, search: search)
|
||||
func execute(state: BookmarkState? = nil, limit: Int? = nil, offset: Int? = nil, search: String? = nil, type: [BookmarkType]? = nil) async throws -> BookmarksPage {
|
||||
var allBookmarks = try await repository.fetchBookmarks(state: state, limit: limit, offset: offset, search: search, type: type)
|
||||
|
||||
// Fallback-Filterung auf Client-Seite falls API keine Query-Parameter unterstützt
|
||||
if let state = state {
|
||||
return allBookmarks.filter { bookmark in
|
||||
allBookmarks.bookmarks = allBookmarks.bookmarks.filter { bookmark in
|
||||
switch state {
|
||||
case .all:
|
||||
return true
|
||||
case .unread:
|
||||
return !bookmark.isArchived && !bookmark.isMarked
|
||||
case .favorite:
|
||||
|
||||
@ -82,22 +82,11 @@ struct BookmarkCardView: View {
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12))
|
||||
.shadow(color: .black.opacity(0.1), radius: 2, x: 0, y: 1)
|
||||
// Swipe Actions hinzufügen
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
// Löschen (ganz rechts)
|
||||
Button("Löschen", role: .destructive) {
|
||||
onDelete(bookmark)
|
||||
}
|
||||
.tint(.red)
|
||||
|
||||
// Favorit (rechts)
|
||||
Button {
|
||||
onToggleFavorite(bookmark)
|
||||
} label: {
|
||||
Label(bookmark.isMarked ? "Entfernen" : "Favorit",
|
||||
systemImage: bookmark.isMarked ? "heart.slash" : "heart.fill")
|
||||
}
|
||||
.tint(bookmark.isMarked ? .gray : .pink)
|
||||
}
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
// Archivieren (links)
|
||||
@ -111,6 +100,14 @@ struct BookmarkCardView: View {
|
||||
}
|
||||
}
|
||||
.tint(currentState == .archived ? .blue : .orange)
|
||||
|
||||
Button {
|
||||
onToggleFavorite(bookmark)
|
||||
} label: {
|
||||
Label(bookmark.isMarked ? "Entfernen" : "Favorit",
|
||||
systemImage: bookmark.isMarked ? "heart.slash" : "heart.fill")
|
||||
}
|
||||
.tint(bookmark.isMarked ? .gray : .pink)
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,7 +118,6 @@ struct BookmarkCardView: View {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prüfe auf Unix Epoch (1970-01-01) - bedeutet "kein Datum"
|
||||
if published.contains("1970-01-01") {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1,154 +1,164 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct BookmarksView: View {
|
||||
|
||||
// MARK: States
|
||||
|
||||
@State private var viewModel = BookmarksViewModel()
|
||||
@State private var showingAddBookmark = false
|
||||
@State private var selectedBookmarkId: String?
|
||||
let state: BookmarkState
|
||||
|
||||
@Binding var selectedBookmark: Bookmark?
|
||||
|
||||
@State private var showingAddBookmarkFromShare = false
|
||||
@State private var shareURL = ""
|
||||
@State private var shareTitle = ""
|
||||
|
||||
let state: BookmarkState
|
||||
let type: [BookmarkType]
|
||||
|
||||
@Binding var selectedBookmark: Bookmark?
|
||||
|
||||
// MARK: Environments
|
||||
|
||||
@Environment(\.horizontalSizeClass) var horizontalSizeClass
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
if viewModel.isLoading && viewModel.bookmarks.isEmpty {
|
||||
ProgressView("Lade \(state.displayName)...")
|
||||
} else {
|
||||
List {
|
||||
ForEach(viewModel.bookmarks, id: \.id) { bookmark in
|
||||
Button(action: {
|
||||
if UIDevice.isPhone {
|
||||
selectedBookmarkId = bookmark.id
|
||||
} else {
|
||||
if selectedBookmark?.id == bookmark.id {
|
||||
// Optional: Deselect, um erneutes Auswählen zu ermöglichen
|
||||
selectedBookmark = nil
|
||||
DispatchQueue.main.async {
|
||||
selectedBookmark = bookmark
|
||||
}
|
||||
} else {
|
||||
ZStack {
|
||||
if viewModel.isLoading && viewModel.bookmarks?.bookmarks.isEmpty == true {
|
||||
ProgressView("Lade \(state.displayName)...")
|
||||
} else {
|
||||
List {
|
||||
ForEach(viewModel.bookmarks?.bookmarks ?? [], id: \.id) { bookmark in
|
||||
Button(action: {
|
||||
if UIDevice.isPhone {
|
||||
selectedBookmarkId = bookmark.id
|
||||
} else {
|
||||
if selectedBookmark?.id == bookmark.id {
|
||||
selectedBookmark = nil
|
||||
DispatchQueue.main.async {
|
||||
selectedBookmark = bookmark
|
||||
}
|
||||
}
|
||||
}) {
|
||||
BookmarkCardView(
|
||||
bookmark: bookmark,
|
||||
currentState: state,
|
||||
onArchive: { bookmark in
|
||||
Task {
|
||||
await viewModel.toggleArchive(bookmark: bookmark)
|
||||
}
|
||||
},
|
||||
onDelete: { bookmark in
|
||||
Task {
|
||||
await viewModel.deleteBookmark(bookmark: bookmark)
|
||||
}
|
||||
},
|
||||
onToggleFavorite: { bookmark in
|
||||
Task {
|
||||
await viewModel.toggleFavorite(bookmark: bookmark)
|
||||
}
|
||||
}
|
||||
)
|
||||
.onAppear {
|
||||
if bookmark.id == viewModel.bookmarks.last?.id {
|
||||
Task {
|
||||
await viewModel.loadMoreBookmarks()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
selectedBookmark = bookmark
|
||||
}
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.refreshable {
|
||||
await viewModel.refreshBookmarks()
|
||||
}
|
||||
.overlay {
|
||||
if viewModel.bookmarks.isEmpty && !viewModel.isLoading {
|
||||
ContentUnavailableView(
|
||||
"Keine Bookmarks",
|
||||
systemImage: "bookmark",
|
||||
description: Text("Es wurden noch keine Bookmarks in \(state.displayName.lowercased()) gefunden.")
|
||||
}) {
|
||||
BookmarkCardView(
|
||||
bookmark: bookmark,
|
||||
currentState: state,
|
||||
onArchive: { bookmark in
|
||||
Task {
|
||||
await viewModel.toggleArchive(bookmark: bookmark)
|
||||
}
|
||||
},
|
||||
onDelete: { bookmark in
|
||||
Task {
|
||||
await viewModel.deleteBookmark(bookmark: bookmark)
|
||||
}
|
||||
},
|
||||
onToggleFavorite: { bookmark in
|
||||
Task {
|
||||
await viewModel.toggleFavorite(bookmark: bookmark)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// FAB Button - nur bei "Ungelesen" anzeigen
|
||||
if state == .unread {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
showingAddBookmark = true
|
||||
}) {
|
||||
Image(systemName: "plus")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 56, height: 56)
|
||||
.background(Color.accentColor)
|
||||
.clipShape(Circle())
|
||||
.shadow(color: .black.opacity(0.25), radius: 6, x: 0, y: 3)
|
||||
.onAppear {
|
||||
if bookmark.id == viewModel.bookmarks?.bookmarks.last?.id {
|
||||
Task {
|
||||
await viewModel.loadMoreBookmarks()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.trailing, 20)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.clear)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(state.displayName)
|
||||
.navigationDestination(item: Binding<String?>(
|
||||
get: { selectedBookmarkId },
|
||||
set: { selectedBookmarkId = $0 }
|
||||
)) { bookmarkId in
|
||||
BookmarkDetailView(bookmarkId: bookmarkId)
|
||||
}
|
||||
.sheet(isPresented: $showingAddBookmark) {
|
||||
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)
|
||||
}
|
||||
.sheet(isPresented: $viewModel.showingAddBookmarkFromShare, content: {
|
||||
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)
|
||||
})
|
||||
/*.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||||
Button("OK", role: .cancel) {
|
||||
viewModel.errorMessage = nil
|
||||
.listStyle(.plain)
|
||||
.refreshable {
|
||||
await viewModel.refreshBookmarks()
|
||||
}
|
||||
} message: {
|
||||
Text(viewModel.errorMessage ?? "")
|
||||
}*/
|
||||
.onAppear {
|
||||
Task {
|
||||
await viewModel.loadBookmarks(state: state)
|
||||
.overlay {
|
||||
if viewModel.bookmarks?.bookmarks.isEmpty == true && !viewModel.isLoading {
|
||||
ContentUnavailableView(
|
||||
"Keine Bookmarks",
|
||||
systemImage: "bookmark",
|
||||
description: Text(
|
||||
"Es wurden noch keine Bookmarks in \(state.displayName.lowercased()) gefunden."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.searchable(
|
||||
text: $viewModel.searchQuery, placement: .automatic, prompt: "Search...")
|
||||
}
|
||||
.onChange(of: showingAddBookmark) { oldValue, newValue in
|
||||
// Refresh bookmarks when sheet is dismissed
|
||||
if oldValue && !newValue {
|
||||
Task {
|
||||
await viewModel.loadBookmarks(state: state)
|
||||
|
||||
// FAB Button - nur bei "Ungelesen" anzeigen
|
||||
if state == .unread {
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
showingAddBookmark = true
|
||||
}) {
|
||||
Image(systemName: "plus")
|
||||
.font(.title2)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 56, height: 56)
|
||||
.background(Color.accentColor)
|
||||
.clipShape(Circle())
|
||||
.shadow(color: .black.opacity(0.25), radius: 6, x: 0, y: 3)
|
||||
}
|
||||
.padding(.trailing, 20)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $viewModel.searchQuery, placement: .automatic, prompt: "Search...")
|
||||
.navigationTitle(state.displayName)
|
||||
.navigationDestination(
|
||||
item: Binding<String?>(
|
||||
get: { selectedBookmarkId },
|
||||
set: { selectedBookmarkId = $0 }
|
||||
)
|
||||
) { bookmarkId in
|
||||
BookmarkDetailView(bookmarkId: bookmarkId)
|
||||
}
|
||||
.sheet(isPresented: $showingAddBookmark) {
|
||||
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)
|
||||
}
|
||||
.sheet(
|
||||
isPresented: $viewModel.showingAddBookmarkFromShare,
|
||||
content: {
|
||||
AddBookmarkView(prefilledURL: shareURL, prefilledTitle: shareTitle)
|
||||
}
|
||||
)
|
||||
/*.alert("Fehler", isPresented: .constant(viewModel.errorMessage != nil)) {
|
||||
Button("OK", role: .cancel) {
|
||||
viewModel.errorMessage = nil
|
||||
}
|
||||
} message: {
|
||||
Text(viewModel.errorMessage ?? "")
|
||||
}*/
|
||||
.onAppear {
|
||||
Task {
|
||||
await viewModel.loadBookmarks(state: state)
|
||||
}
|
||||
}
|
||||
.onChange(of: showingAddBookmark) { oldValue, newValue in
|
||||
// Refresh bookmarks when sheet is dismissed
|
||||
if oldValue && !newValue {
|
||||
Task {
|
||||
await viewModel.loadBookmarks(state: state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,21 +8,21 @@ class BookmarksViewModel {
|
||||
private let updateBookmarkUseCase = DefaultUseCaseFactory.shared.makeUpdateBookmarkUseCase()
|
||||
private let deleteBookmarkUseCase = DefaultUseCaseFactory.shared.makeDeleteBookmarkUseCase()
|
||||
|
||||
var bookmarks: [Bookmark] = []
|
||||
var bookmarks: BookmarksPage?
|
||||
var isLoading = false
|
||||
var errorMessage: String?
|
||||
var currentState: BookmarkState = .unread
|
||||
var type = [BookmarkType.article]
|
||||
|
||||
var showingAddBookmarkFromShare = false
|
||||
var shareURL = ""
|
||||
var shareTitle = ""
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// Pagination-Variablen
|
||||
private var limit = 20
|
||||
private var offset = 0
|
||||
private var hasMoreData = true
|
||||
private var searchWorkItem: DispatchWorkItem?
|
||||
|
||||
var searchQuery: String = "" {
|
||||
didSet {
|
||||
@ -30,8 +30,6 @@ class BookmarksViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
private var searchWorkItem: DispatchWorkItem?
|
||||
|
||||
init() {
|
||||
setupNotificationObserver()
|
||||
}
|
||||
@ -88,13 +86,14 @@ class BookmarksViewModel {
|
||||
state: state,
|
||||
limit: limit,
|
||||
offset: offset,
|
||||
search: searchQuery // Suche integrieren
|
||||
search: searchQuery,
|
||||
type: type
|
||||
)
|
||||
bookmarks = newBookmarks
|
||||
hasMoreData = newBookmarks.count == limit // Prüfen, ob weitere Daten verfügbar sind
|
||||
hasMoreData = newBookmarks.bookmarks.count == limit // Prüfen, ob weitere Daten verfügbar sind
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Laden der Bookmarks"
|
||||
bookmarks = []
|
||||
bookmarks = nil
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
@ -110,8 +109,8 @@ class BookmarksViewModel {
|
||||
do {
|
||||
offset += limit // Offset erhöhen
|
||||
let newBookmarks = try await getBooksmarksUseCase.execute(state: currentState, limit: limit, offset: offset)
|
||||
bookmarks.append(contentsOf: newBookmarks) // Neue Bookmarks hinzufügen
|
||||
hasMoreData = newBookmarks.count == limit // Prüfen, ob weitere Daten verfügbar sind
|
||||
bookmarks?.bookmarks.append(contentsOf: newBookmarks.bookmarks) // Neue Bookmarks hinzufügen
|
||||
hasMoreData = newBookmarks.bookmarks.count == limit // Prüfen,
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Nachladen der Bookmarks"
|
||||
}
|
||||
@ -163,7 +162,7 @@ class BookmarksViewModel {
|
||||
try await deleteBookmarkUseCase.execute(bookmarkId: bookmark.id)
|
||||
|
||||
// Lokal aus der Liste entfernen (optimistische Update)
|
||||
bookmarks.removeAll { $0.id == bookmark.id }
|
||||
bookmarks?.bookmarks.removeAll { $0.id == bookmark.id }
|
||||
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Löschen des Bookmarks"
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
//
|
||||
// File.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Ilyas Hallak on 25.06.25.
|
||||
//
|
||||
|
||||
@ -1,10 +1,20 @@
|
||||
//
|
||||
// BookmarkState.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Ilyas Hallak on 01.07.25.
|
||||
//
|
||||
|
||||
enum BookmarkState: String, CaseIterable {
|
||||
case all = "all"
|
||||
case unread = "unread"
|
||||
case favorite = "favorite"
|
||||
case archived = "archived"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return "Alle"
|
||||
case .unread:
|
||||
return "Ungelesen"
|
||||
case .favorite:
|
||||
@ -16,6 +26,8 @@ enum BookmarkState: String, CaseIterable {
|
||||
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return "list.bullet"
|
||||
case .unread:
|
||||
return "house"
|
||||
case .favorite:
|
||||
|
||||
@ -1,3 +1,12 @@
|
||||
//
|
||||
// PadSidebarView.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Ilyas Hallak on 01.07.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PadSidebarView: View {
|
||||
@State private var selectedTab: SidebarTab = .unread
|
||||
@State private var selectedBookmark: Bookmark?
|
||||
@ -14,12 +23,14 @@ struct PadSidebarView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
|
||||
if tab == .article {
|
||||
Spacer()
|
||||
if tab == .archived {
|
||||
Spacer(minLength: 20)
|
||||
}
|
||||
|
||||
if tab == .pictures {
|
||||
Spacer(minLength: 30)
|
||||
Divider()
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.listRowBackground(selectedTab == tab ? Color.accentColor.opacity(0.15) : Color.clear)
|
||||
@ -46,21 +57,21 @@ struct PadSidebarView: View {
|
||||
} content: {
|
||||
switch selectedTab {
|
||||
case .all:
|
||||
Text("All")
|
||||
BookmarksView(state: .all, type: [.article, .video, .photo], selectedBookmark: $selectedBookmark)
|
||||
case .unread:
|
||||
BookmarksView(state: .unread, selectedBookmark: $selectedBookmark)
|
||||
BookmarksView(state: .unread, type: [.article], selectedBookmark: $selectedBookmark)
|
||||
case .favorite:
|
||||
BookmarksView(state: .favorite, selectedBookmark: $selectedBookmark)
|
||||
BookmarksView(state: .favorite, type: [.article], selectedBookmark: $selectedBookmark)
|
||||
case .archived:
|
||||
BookmarksView(state: .archived, selectedBookmark: $selectedBookmark)
|
||||
BookmarksView(state: .archived, type: [.article], selectedBookmark: $selectedBookmark)
|
||||
case .settings:
|
||||
SettingsView()
|
||||
case .article:
|
||||
Text("Artikel")
|
||||
BookmarksView(state: .all, type: [.article], selectedBookmark: $selectedBookmark)
|
||||
case .videos:
|
||||
Text("Videos")
|
||||
BookmarksView(state: .all, type: [.video], selectedBookmark: $selectedBookmark)
|
||||
case .pictures:
|
||||
Text("Pictures")
|
||||
BookmarksView(state: .all, type: [.photo], selectedBookmark: $selectedBookmark)
|
||||
case .tags:
|
||||
Text("Tags")
|
||||
}
|
||||
|
||||
@ -1,19 +1,36 @@
|
||||
//
|
||||
// PhoneTabView.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Ilyas Hallak on 01.07.25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct PhoneTabView: View {
|
||||
var body: some View {
|
||||
TabView {
|
||||
|
||||
NavigationStack {
|
||||
BookmarksView(state: .unread, selectedBookmark: .constant(nil))
|
||||
BookmarksView(state: .unread, type: [.article, .video, .photo], selectedBookmark: .constant(nil))
|
||||
}
|
||||
.tabItem {
|
||||
Label("Alle", systemImage: "list.bullet")
|
||||
}
|
||||
|
||||
NavigationStack {
|
||||
BookmarksView(state: .unread, type: [.article], selectedBookmark: .constant(nil))
|
||||
}
|
||||
.tabItem {
|
||||
Label("Ungelesen", systemImage: "house")
|
||||
}
|
||||
|
||||
BookmarksView(state: .favorite, selectedBookmark: .constant(nil))
|
||||
BookmarksView(state: .favorite, type: [.article], selectedBookmark: .constant(nil))
|
||||
.tabItem {
|
||||
Label("Favoriten", systemImage: "heart")
|
||||
}
|
||||
|
||||
BookmarksView(state: .archived, selectedBookmark: .constant(nil))
|
||||
BookmarksView(state: .archived, type: [.article], selectedBookmark: .constant(nil))
|
||||
.tabItem {
|
||||
Label("Archiv", systemImage: "archivebox")
|
||||
}
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
//
|
||||
// SidebarTab.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Ilyas Hallak on 01.07.25.
|
||||
//
|
||||
|
||||
enum SidebarTab: Hashable, CaseIterable, Identifiable {
|
||||
case all, unread, favorite, archived, settings, article, videos, pictures, tags
|
||||
|
||||
|
||||
@ -1,34 +1,6 @@
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
enum BookmarkState: String, CaseIterable {
|
||||
case unread = "unread"
|
||||
case favorite = "favorite"
|
||||
case archived = "archived"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .unread:
|
||||
return "Ungelesen"
|
||||
case .favorite:
|
||||
return "Favoriten"
|
||||
case .archived:
|
||||
return "Archiv"
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .unread:
|
||||
return "house"
|
||||
case .favorite:
|
||||
return "heart"
|
||||
case .archived:
|
||||
return "archivebox"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MainTabView: View {
|
||||
@State private var selectedTab: SidebarTab = .unread
|
||||
@State var selectedBookmark: Bookmark?
|
||||
@ -42,162 +14,13 @@ struct MainTabView: View {
|
||||
|
||||
var body: some View {
|
||||
if UIDevice.isPhone {
|
||||
PhoneView()
|
||||
PhoneTabView()
|
||||
} else {
|
||||
PadSidebarView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sidebar Tabs
|
||||
enum SidebarTab: Hashable, CaseIterable, Identifiable {
|
||||
case all, unread, favorite, archived, settings, article, videos, pictures, tags
|
||||
|
||||
var id: Self { self }
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .all: return "Alle"
|
||||
case .unread: return "Ungelesen"
|
||||
case .favorite: return "Favoriten"
|
||||
case .archived: return "Archiv"
|
||||
case .settings: return "Einstellungen"
|
||||
case .article: return "Artikel"
|
||||
case .videos: return "Videos"
|
||||
case .pictures: return "Bilder"
|
||||
case .tags: return "Tags"
|
||||
}
|
||||
}
|
||||
|
||||
var systemImage: String {
|
||||
switch self {
|
||||
case .unread: return "house"
|
||||
case .favorite: return "heart"
|
||||
case .archived: return "archivebox"
|
||||
case .settings: return "gear"
|
||||
case .all: return "list.bullet"
|
||||
case .article: return "doc.plaintext"
|
||||
case .videos: return "film"
|
||||
case .pictures: return "photo"
|
||||
case .tags: return "tag"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PadSidebarView: View {
|
||||
@State private var selectedTab: SidebarTab = .unread
|
||||
@State private var selectedBookmark: Bookmark?
|
||||
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
List {
|
||||
ForEach(SidebarTab.allCases.filter { $0 != .settings }, id: \.self) { tab in
|
||||
Button(action: {
|
||||
selectedTab = tab
|
||||
}) {
|
||||
Label(tab.label, systemImage: tab.systemImage)
|
||||
.foregroundColor(selectedTab == tab ? .accentColor : .primary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
|
||||
if tab == .article {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
if tab == .pictures {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
.listRowBackground(selectedTab == tab ? Color.accentColor.opacity(0.15) : Color.clear)
|
||||
}
|
||||
}
|
||||
.listStyle(.sidebar)
|
||||
.safeAreaInset(edge: .bottom, alignment: .center) {
|
||||
VStack(spacing: 0) {
|
||||
Divider()
|
||||
Button(action: {
|
||||
selectedTab = .settings
|
||||
}) {
|
||||
Label(SidebarTab.settings.label, systemImage: SidebarTab.settings.systemImage)
|
||||
.foregroundColor(selectedTab == .settings ? .accentColor : .primary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.vertical, 12)
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
.listRowBackground(selectedTab == .settings ? Color.accentColor.opacity(0.15) : Color.clear)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
} content: {
|
||||
switch selectedTab {
|
||||
case .all:
|
||||
Text("All")
|
||||
case .unread:
|
||||
BookmarksView(state: .unread, selectedBookmark: $selectedBookmark)
|
||||
case .favorite:
|
||||
BookmarksView(state: .favorite, selectedBookmark: $selectedBookmark)
|
||||
case .archived:
|
||||
BookmarksView(state: .archived, selectedBookmark: $selectedBookmark)
|
||||
case .settings:
|
||||
SettingsView()
|
||||
case .article:
|
||||
Text("Artikel")
|
||||
case .videos:
|
||||
Text("Videos")
|
||||
case .pictures:
|
||||
Text("Pictures")
|
||||
case .tags:
|
||||
Text("Tags")
|
||||
}
|
||||
} detail: {
|
||||
if let bookmark = selectedBookmark, selectedTab != .settings {
|
||||
BookmarkDetailView(bookmarkId: bookmark.id)
|
||||
} else {
|
||||
Text(selectedTab == .settings ? "" : "Select a bookmark")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// iPhone: TabView bleibt wie gehabt
|
||||
extension MainTabView {
|
||||
@ViewBuilder
|
||||
fileprivate func PhoneView() -> some View {
|
||||
TabView {
|
||||
NavigationStack {
|
||||
BookmarksView(state: .unread, selectedBookmark: .constant(nil))
|
||||
}
|
||||
.tabItem {
|
||||
Label("Ungelesen", systemImage: "house")
|
||||
}
|
||||
|
||||
NavigationView {
|
||||
BookmarksView(state: .favorite, selectedBookmark: .constant(nil))
|
||||
.tabItem {
|
||||
Label("Favoriten", systemImage: "heart")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationView {
|
||||
BookmarksView(state: .archived, selectedBookmark: .constant(nil))
|
||||
.tabItem {
|
||||
Label("Archiv", systemImage: "archivebox")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationView {
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
.accentColor(.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MainTabView()
|
||||
}
|
||||
|
||||
@ -8,31 +8,23 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsContainerView: View {
|
||||
@State private var viewModel = SettingsViewModel()
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 20) {
|
||||
// Server-Card immer anzeigen
|
||||
SettingsServerView(viewModel: viewModel)
|
||||
.cardStyle()
|
||||
ScrollView {
|
||||
LazyVStack(spacing: 20) {
|
||||
SettingsServerView()
|
||||
.cardStyle()
|
||||
|
||||
// Allgemeine Einstellungen nur im normalen Modus anzeigen
|
||||
if !viewModel.isSetupMode {
|
||||
SettingsGeneralView(viewModel: viewModel)
|
||||
.cardStyle()
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
FontSettingsView()
|
||||
.cardStyle()
|
||||
|
||||
SettingsGeneralView()
|
||||
.cardStyle()
|
||||
}
|
||||
.navigationTitle("Einstellungen")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
.task {
|
||||
await viewModel.loadSettings()
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
}
|
||||
.navigationTitle("Einstellungen")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,7 @@ import SwiftUI
|
||||
// SectionHeader wird jetzt zentral importiert
|
||||
|
||||
struct SettingsGeneralView: View {
|
||||
@State var viewModel: SettingsViewModel
|
||||
@State private var viewModel = SettingsGeneralViewModel()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
@ -28,10 +28,6 @@ struct SettingsGeneralView: View {
|
||||
.pickerStyle(.segmented)
|
||||
}
|
||||
|
||||
// Font Settings
|
||||
FontSettingsView()
|
||||
.padding(.vertical, 4)
|
||||
|
||||
// Sync Settings
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Sync-Einstellungen")
|
||||
@ -118,26 +114,19 @@ struct SettingsGeneralView: View {
|
||||
// Save Button
|
||||
Button(action: {
|
||||
Task {
|
||||
await viewModel.saveSettings()
|
||||
await viewModel.saveGeneralSettings()
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
if viewModel.isSaving {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
}
|
||||
Text(viewModel.isSaving ? "Speichere..." : "Einstellungen speichern")
|
||||
Text("Einstellungen speichern")
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(viewModel.canSave ? Color.accentColor : Color.gray)
|
||||
.background(Color.accentColor)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.disabled(!viewModel.canSave || viewModel.isSaving)
|
||||
|
||||
// Messages
|
||||
if let successMessage = viewModel.successMessage {
|
||||
HStack {
|
||||
@ -158,9 +147,27 @@ struct SettingsGeneralView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await viewModel.loadGeneralSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsGeneralView(viewModel: SettingsViewModel())
|
||||
enum Theme: String, CaseIterable {
|
||||
case system = "system"
|
||||
case light = "light"
|
||||
case dark = "dark"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .system: return "System"
|
||||
case .light: return "Hell"
|
||||
case .dark: return "Dunkel"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
SettingsGeneralView()
|
||||
}
|
||||
|
||||
@ -37,13 +37,13 @@ class SettingsGeneralViewModel {
|
||||
do {
|
||||
if let settings = try await loadSettingsUseCase.execute() {
|
||||
selectedTheme = .system // settings.theme ?? .system
|
||||
autoSyncEnabled = settings.autoSyncEnabled
|
||||
syncInterval = settings.syncInterval
|
||||
enableReaderMode = settings.enableReaderMode
|
||||
openExternalLinksInApp = settings.openExternalLinksInApp
|
||||
autoMarkAsRead = settings.autoMarkAsRead
|
||||
appVersion = settings.appVersion ?? "1.0.0"
|
||||
developerName = settings.developerName ?? "Your Name"
|
||||
autoSyncEnabled = false // settings.autoSyncEnabled
|
||||
// syncInterval = settings.syncInterval
|
||||
// enableReaderMode = settings.enableReaderMode
|
||||
// openExternalLinksInApp = settings.openExternalLinksInApp
|
||||
// autoMarkAsRead = settings.autoMarkAsRead
|
||||
appVersion = "1.0.0"
|
||||
developerName = "Ilyas Hallak"
|
||||
}
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Laden der Einstellungen"
|
||||
@ -53,14 +53,17 @@ class SettingsGeneralViewModel {
|
||||
@MainActor
|
||||
func saveGeneralSettings() async {
|
||||
do {
|
||||
try await saveSettingsUseCase.execute(
|
||||
|
||||
// TODO: add save general settings here
|
||||
/*try await saveSettingsUseCase.execute(
|
||||
token: "",
|
||||
selectedTheme: selectedTheme,
|
||||
autoSyncEnabled: autoSyncEnabled,
|
||||
syncInterval: syncInterval,
|
||||
enableReaderMode: enableReaderMode,
|
||||
openExternalLinksInApp: openExternalLinksInApp,
|
||||
autoMarkAsRead: autoMarkAsRead
|
||||
)
|
||||
)*/
|
||||
successMessage = "Einstellungen gespeichert"
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Speichern der Einstellungen"
|
||||
|
||||
@ -9,7 +9,7 @@ import SwiftUI
|
||||
// SectionHeader wird jetzt zentral importiert
|
||||
|
||||
struct SettingsServerView: View {
|
||||
@State var viewModel = SettingsViewModel()
|
||||
@State private var viewModel = SettingsServerViewModel()
|
||||
@State private var isTesting: Bool = false
|
||||
@State private var connectionTestSuccess: Bool = false
|
||||
@State private var showingLogoutAlert = false
|
||||
@ -188,38 +188,18 @@ struct SettingsServerView: View {
|
||||
} message: {
|
||||
Text("Möchten Sie sich wirklich abmelden? Dies wird alle Ihre Anmeldedaten löschen und Sie zur Einrichtung zurückführen.")
|
||||
}
|
||||
.task {
|
||||
await viewModel.loadServerSettings()
|
||||
}
|
||||
}
|
||||
|
||||
private func testConnection() async {
|
||||
guard viewModel.canLogin else {
|
||||
viewModel.errorMessage = "Bitte füllen Sie alle Felder aus."
|
||||
return
|
||||
}
|
||||
|
||||
isTesting = true
|
||||
viewModel.clearMessages()
|
||||
connectionTestSuccess = false
|
||||
|
||||
do {
|
||||
// Test login without saving settings
|
||||
let _ = try await viewModel.loginUseCase.execute(
|
||||
username: viewModel.username.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
password: viewModel.password
|
||||
)
|
||||
|
||||
// If we get here, the test was successful
|
||||
connectionTestSuccess = true
|
||||
viewModel.successMessage = "Verbindung erfolgreich getestet! ✓"
|
||||
|
||||
} catch {
|
||||
connectionTestSuccess = false
|
||||
viewModel.errorMessage = "Verbindungstest fehlgeschlagen: \(error.localizedDescription)"
|
||||
}
|
||||
|
||||
connectionTestSuccess = await viewModel.testConnection()
|
||||
isTesting = false
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsServerView(viewModel: SettingsViewModel())
|
||||
SettingsServerView()
|
||||
}
|
||||
|
||||
@ -61,6 +61,34 @@ class SettingsServerViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testConnection() async -> Bool {
|
||||
guard canLogin else {
|
||||
errorMessage = "Bitte füllen Sie alle Felder aus."
|
||||
return false
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@ -1,169 +0,0 @@
|
||||
import Foundation
|
||||
import Observation
|
||||
import SwiftUI
|
||||
|
||||
@Observable
|
||||
class SettingsViewModel {
|
||||
private let _loginUseCase: LoginUseCase
|
||||
private let saveSettingsUseCase: SaveSettingsUseCase
|
||||
private let loadSettingsUseCase: LoadSettingsUseCase
|
||||
private let logoutUseCase: LogoutUseCase
|
||||
private let settingsRepository: SettingsRepository
|
||||
|
||||
// MARK: - Server Settings
|
||||
var endpoint = ""
|
||||
var username = ""
|
||||
var password = ""
|
||||
var isLoading = false
|
||||
var isSaving = false
|
||||
var isLoggedIn = false
|
||||
|
||||
// MARK: - UI Settings
|
||||
var selectedTheme: Theme = .system
|
||||
|
||||
// MARK: - Sync Settings
|
||||
var autoSyncEnabled: Bool = true
|
||||
var syncInterval: Int = 15
|
||||
|
||||
// MARK: - Reading Settings
|
||||
var enableReaderMode: Bool = false
|
||||
var openExternalLinksInApp: Bool = true
|
||||
var autoMarkAsRead: Bool = false
|
||||
|
||||
// MARK: - App Info
|
||||
var appVersion: String = "1.0.0"
|
||||
var developerName: String = "Your Name"
|
||||
|
||||
|
||||
// MARK: - Messages
|
||||
var errorMessage: String?
|
||||
var successMessage: String?
|
||||
|
||||
init() {
|
||||
let factory = DefaultUseCaseFactory.shared
|
||||
self._loginUseCase = factory.makeLoginUseCase()
|
||||
self.saveSettingsUseCase = factory.makeSaveSettingsUseCase()
|
||||
self.loadSettingsUseCase = factory.makeLoadSettingsUseCase()
|
||||
self.logoutUseCase = factory.makeLogoutUseCase()
|
||||
self.settingsRepository = SettingsRepository()
|
||||
}
|
||||
|
||||
var isSetupMode: Bool {
|
||||
!settingsRepository.hasFinishedSetup
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func loadSettings() async {
|
||||
do {
|
||||
if let settings = try await loadSettingsUseCase.execute() {
|
||||
endpoint = settings.endpoint ?? ""
|
||||
username = settings.username ?? ""
|
||||
password = settings.password ?? ""
|
||||
isLoggedIn = settings.isLoggedIn
|
||||
}
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Laden der Einstellungen"
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func saveSettings() async {
|
||||
isSaving = true
|
||||
errorMessage = nil
|
||||
successMessage = nil
|
||||
|
||||
do {
|
||||
try await saveSettingsUseCase.execute(
|
||||
endpoint: endpoint,
|
||||
username: username,
|
||||
password: password
|
||||
)
|
||||
successMessage = "Einstellungen gespeichert"
|
||||
|
||||
// Factory-Konfiguration aktualisieren
|
||||
await DefaultUseCaseFactory.shared.refreshConfiguration()
|
||||
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Speichern der Einstellungen"
|
||||
}
|
||||
|
||||
isSaving = false
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func login() async {
|
||||
isLoading = true
|
||||
errorMessage = nil
|
||||
successMessage = nil
|
||||
|
||||
do {
|
||||
let user = try await _loginUseCase.execute(username: username, password: password)
|
||||
|
||||
isLoggedIn = true
|
||||
successMessage = "Erfolgreich angemeldet"
|
||||
|
||||
// Setup als abgeschlossen markieren
|
||||
try await settingsRepository.saveHasFinishedSetup(true)
|
||||
|
||||
// Notification senden, dass sich der Setup-Status geändert hat
|
||||
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||
|
||||
// Factory-Konfiguration aktualisieren (Token wird automatisch gespeichert)
|
||||
await DefaultUseCaseFactory.shared.refreshConfiguration()
|
||||
|
||||
} catch {
|
||||
errorMessage = "Anmeldung fehlgeschlagen"
|
||||
isLoggedIn = false
|
||||
}
|
||||
|
||||
isLoading = false
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func logout() async {
|
||||
do {
|
||||
try await logoutUseCase.execute()
|
||||
isLoggedIn = false
|
||||
successMessage = "Abgemeldet"
|
||||
|
||||
// Notification senden, dass sich der Setup-Status geändert hat
|
||||
NotificationCenter.default.post(name: NSNotification.Name("SetupStatusChanged"), object: nil)
|
||||
|
||||
} catch {
|
||||
errorMessage = "Fehler beim Abmelden"
|
||||
}
|
||||
}
|
||||
|
||||
func clearMessages() {
|
||||
errorMessage = nil
|
||||
successMessage = nil
|
||||
}
|
||||
|
||||
var canSave: Bool {
|
||||
!endpoint.isEmpty && !username.isEmpty && !password.isEmpty
|
||||
}
|
||||
|
||||
var canLogin: Bool {
|
||||
!username.isEmpty && !password.isEmpty
|
||||
}
|
||||
|
||||
// Expose loginUseCase for testing purposes
|
||||
var loginUseCase: LoginUseCase {
|
||||
return _loginUseCase
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum Theme: String, CaseIterable {
|
||||
case system = "system"
|
||||
case light = "light"
|
||||
case dark = "dark"
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .system: return "System"
|
||||
case .light: return "Hell"
|
||||
case .dark: return "Dunkel"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -20,7 +20,8 @@ struct readeckApp: App {
|
||||
MainTabView()
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
} else {
|
||||
SettingsContainerView()
|
||||
SettingsServerView()
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.onOpenURL { url in
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user