diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 6cd4684..ac80a58 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -3,6 +3,9 @@ "strings" : { "" : { + }, + "(%lld found)" : { + }, "%" : { @@ -51,7 +54,7 @@ "Add" : { }, - "Add new tag..." : { + "Add custom tag:" : { }, "all" : { @@ -67,6 +70,9 @@ }, "All available tags" : { + }, + "All labels selected" : { + }, "Archive" : { @@ -85,6 +91,9 @@ }, "Automatically mark articles as read" : { + }, + "Available labels" : { + }, "Cancel" : { @@ -118,9 +127,6 @@ }, "Enter an optional title..." : { - }, - "Enter label..." : { - }, "Enter your Readeck server details to get started." : { @@ -133,6 +139,9 @@ }, "Favorite" : { + }, + "Fertig" : { + }, "Finished reading?" : { @@ -214,6 +223,9 @@ }, "No tags available" : { + }, + "No tags found" : { + }, "OK" : { @@ -293,11 +305,17 @@ }, "Saving..." : { + }, + "Search or add new tag..." : { + + }, + "Search results" : { + }, "Select a bookmark or tag" : { }, - "Select labels" : { + "Selected tags" : { }, "Server Endpoint" : { diff --git a/URLShare/ShareBookmarkView.swift b/URLShare/ShareBookmarkView.swift index 61d07f5..84fc291 100644 --- a/URLShare/ShareBookmarkView.swift +++ b/URLShare/ShareBookmarkView.swift @@ -3,172 +3,298 @@ import SwiftUI struct ShareBookmarkView: View { @ObservedObject var viewModel: ShareBookmarkViewModel + private func dismissKeyboard() { + NotificationCenter.default.post(name: NSNotification.Name("DismissKeyboard"), object: nil) + } + var body: some View { - VStack(spacing: 0) { - // Logo - Image("readeck") - .resizable() - .scaledToFit() - .frame(height: 40) - .padding(.top, 24) - .opacity(0.9) - // URL - if let url = viewModel.url { - HStack(spacing: 8) { - Image(systemName: "link") - .foregroundColor(.accentColor) - Text(url) - .font(.system(size: 15, weight: .bold, design: .default)) - .foregroundColor(.accentColor) - .lineLimit(2) - .truncationMode(.middle) + ScrollView { + VStack(spacing: 0) { + // Logo + Image("readeck") + .resizable() + .scaledToFit() + .frame(height: 40) + .padding(.top, 24) + .opacity(0.9) + // URL + if let url = viewModel.url { + HStack(spacing: 8) { + Image(systemName: "link") + .foregroundColor(.accentColor) + Text(url) + .font(.system(size: 15, weight: .bold, design: .default)) + .foregroundColor(.accentColor) + .lineLimit(2) + .truncationMode(.middle) + } + .padding(.top, 8) + .padding(.horizontal, 16) + .frame(maxWidth: .infinity, alignment: .leading) } - .padding(.top, 8) - .padding(.horizontal, 16) - .frame(maxWidth: .infinity, alignment: .leading) - } - // Titel - TextField("Enter an optional title...", text: $viewModel.title) - .font(.system(size: 17, weight: .medium)) - .padding(.horizontal, 10) - .foregroundColor(.primary) - .frame(height: 38) - .background( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(Color(.secondarySystemGroupedBackground)) - .shadow(color: Color.black.opacity(0.04), radius: 2, x: 0, y: 1) - ) - .overlay( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .stroke(Color.accentColor.opacity(viewModel.title.isEmpty ? 0.12 : 0.7), lineWidth: viewModel.title.isEmpty ? 1 : 2) + // Title + TextField("Enter an optional title...", text: $viewModel.title) + .font(.system(size: 17, weight: .medium)) + .padding(.horizontal, 10) + .foregroundColor(.primary) + .frame(height: 38) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(Color(.secondarySystemGroupedBackground)) + .shadow(color: Color.black.opacity(0.04), radius: 2, x: 0, y: 1) + ) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(Color.accentColor.opacity(viewModel.title.isEmpty ? 0.12 : 0.7), lineWidth: viewModel.title.isEmpty ? 1 : 2) + ) + .padding(.top, 20) + .padding(.horizontal, 16) + .frame(maxWidth: 420) + .frame(maxWidth: .infinity, alignment: .center) + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("Fertig") { + dismissKeyboard() + } + } + } + + // Manual tag entry (always visible) + ManualTagEntryView( + labels: viewModel.labels, + selectedLabels: $viewModel.selectedLabels, + searchText: $viewModel.searchText ) .padding(.top, 20) .padding(.horizontal, 16) - .frame(maxWidth: 420) - .frame(maxWidth: .infinity, alignment: .center) - - // Label Grid - if !viewModel.labels.isEmpty { - VStack(alignment: .leading, spacing: 8) { - Text("Select labels") - .font(.headline) + + // Unified Labels Section + if !viewModel.labels.isEmpty { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Available labels") + .font(.headline) + if !viewModel.searchText.isEmpty { + Text("(\(viewModel.filteredLabels.count) found)") + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + } .padding(.horizontal) - - let pageSize = Constants.Labels.pageSize - let pages = stride(from: 0, to: viewModel.labels.count, by: pageSize).map { - Array(viewModel.labels[$0.. 1 ? .automatic : .never)) + .frame(height: 180) + .padding(.top, -20) } } - .tabViewStyle(PageTabViewStyle(indexDisplayMode: .automatic)) - .frame(height: 180) - .padding(.top, -20) - } .padding(.top, 32) .frame(minHeight: 100) - } - - ManualTagEntryView( - labels: viewModel.labels, - selectedLabels: $viewModel.selectedLabels - ) - .padding(.top, 12) - .padding(.horizontal, 16) - - // Status - if let status = viewModel.statusMessage { - Text(status.emoji + " " + status.text) - .font(.system(size: 18, weight: .bold)) - .foregroundColor(status.isError ? .red : .green) - .padding(.top, 32) - .padding(.horizontal, 16) - } - Spacer() - // Save Button - Button(action: { viewModel.save() }) { - if viewModel.isSaving { - ProgressView() - .progressViewStyle(CircularProgressViewStyle()) - .frame(maxWidth: .infinity) - .padding() - } else { - Text("Save Bookmark") - .font(.system(size: 17, weight: .semibold)) - .frame(maxWidth: .infinity) - .padding() - .background(Color.accentColor) - .foregroundColor(.white) - .cornerRadius(16) } + + // Current selected labels + if !viewModel.selectedLabels.isEmpty { + VStack(alignment: .leading, spacing: 8) { + Text("Selected tags") + .font(.headline) + .padding(.horizontal) + + LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], alignment: .leading, spacing: 8) { + ForEach(Array(viewModel.selectedLabels), id: \.self) { label in + UnifiedLabelChip( + label: label, + isSelected: false, + isRemovable: true, + onTap: { + // No action for selected labels + }, + onRemove: { + viewModel.selectedLabels.remove(label) + } + ) + } + } + .padding(.horizontal) + } + .padding(.top, 20) + } + + // Status + if let status = viewModel.statusMessage { + Text(status.emoji + " " + status.text) + .font(.system(size: 18, weight: .bold)) + .foregroundColor(status.isError ? .red : .green) + .padding(.top, 32) + .padding(.horizontal, 16) + } + + // Save Button + Button(action: { viewModel.save() }) { + if viewModel.isSaving { + ProgressView() + .progressViewStyle(CircularProgressViewStyle()) + .frame(maxWidth: .infinity) + .padding() + } else { + Text("Save Bookmark") + .font(.system(size: 17, weight: .semibold)) + .frame(maxWidth: .infinity) + .padding() + .background(Color.accentColor) + .foregroundColor(.white) + .cornerRadius(16) + } + } + .padding(.horizontal, 16) + .padding(.top, 32) + .padding(.bottom, 32) + .disabled(viewModel.isSaving) } - .padding(.horizontal, 16) - .padding(.bottom, 32) - .disabled(viewModel.isSaving) } .background(Color(.systemGroupedBackground)) .onAppear { viewModel.onAppear() } + .ignoresSafeArea(.keyboard, edges: .bottom) + .background( + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + // Fallback for extensions: tap anywhere to dismiss keyboard + dismissKeyboard() + } + ) } } struct ManualTagEntryView: View { let labels: [BookmarkLabelDto] @Binding var selectedLabels: Set - @State private var manualTag: String = "" + @Binding var searchText: String @State private var error: String? = nil + private func dismissKeyboard() { + NotificationCenter.default.post(name: NSNotification.Name("DismissKeyboard"), object: nil) + } + var body: some View { - VStack(alignment: .leading, spacing: 4) { - HStack { - TextField("Add new tag...", text: $manualTag) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .autocapitalization(.none) - .disableAutocorrection(true) - Button(action: addTag) { - Text("Add") - .font(.system(size: 15, weight: .semibold)) + VStack(alignment: .leading, spacing: 8) { + // Search field + TextField("Search or add new tag...", text: $searchText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) + .disableAutocorrection(true) + .toolbar { + ToolbarItemGroup(placement: .keyboard) { + Spacer() + Button("Fertig") { + dismissKeyboard() + } + } } - .disabled(manualTag.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) + .onSubmit { + addCustomTag() + } + + // Show custom tag suggestion if search text doesn't match existing tags + if !searchText.isEmpty && !labels.contains(where: { $0.name.lowercased() == searchText.lowercased() }) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text("Add new tag:") + .font(.subheadline) + .foregroundColor(.secondary) + Text(searchText) + .font(.headline) + .fontWeight(.medium) + } + Spacer() + Button(action: addCustomTag) { + HStack(spacing: 6) { + Image(systemName: "plus.circle.fill") + .font(.title3) + Text("Add") + .font(.subheadline) + .fontWeight(.medium) + } + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background(Color.accentColor) + .foregroundColor(.white) + .cornerRadius(8) + } + } + .padding(.horizontal, 12) + .padding(.vertical, 12) + .background(Color.accentColor.opacity(0.1)) + .cornerRadius(10) } + if let error = error { Text(error) .font(.caption) .foregroundColor(.red) } } + .background( + Color.clear + .contentShape(Rectangle()) + .onTapGesture { + // Fallback for extensions: tap anywhere to dismiss keyboard + dismissKeyboard() + } + ) } - private func addTag() { - let trimmed = manualTag.trimmingCharacters(in: .whitespacesAndNewlines) + private func addCustomTag() { + let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } + let lowercased = trimmed.lowercased() let allExisting = Set(labels.map { $0.name.lowercased() }) let allSelected = Set(selectedLabels.map { $0.lowercased() }) + if allExisting.contains(lowercased) || allSelected.contains(lowercased) { error = "Tag already exists." } else { selectedLabels.insert(trimmed) - manualTag = "" + searchText = "" error = nil } } diff --git a/URLShare/ShareBookmarkViewModel.swift b/URLShare/ShareBookmarkViewModel.swift index a03a01b..9df436c 100644 --- a/URLShare/ShareBookmarkViewModel.swift +++ b/URLShare/ShareBookmarkViewModel.swift @@ -9,7 +9,35 @@ class ShareBookmarkViewModel: ObservableObject { @Published var selectedLabels: Set = [] @Published var statusMessage: (text: String, isError: Bool, emoji: String)? = nil @Published var isSaving: Bool = false - private weak var extensionContext: NSExtensionContext? + @Published var searchText: String = "" + let extensionContext: NSExtensionContext? + + // Computed properties for pagination + var availableLabels: [BookmarkLabelDto] { + return labels.filter { !selectedLabels.contains($0.name) } + } + + // Computed property for filtered labels based on search text + var filteredLabels: [BookmarkLabelDto] { + if searchText.isEmpty { + return availableLabels + } else { + return availableLabels.filter { $0.name.localizedCaseInsensitiveContains(searchText) } + } + } + + var availableLabelPages: [[BookmarkLabelDto]] { + let pageSize = Constants.Labels.pageSize + let labelsToShow = searchText.isEmpty ? availableLabels : filteredLabels + + if labelsToShow.count <= pageSize { + return [labelsToShow] + } else { + return stride(from: 0, to: labelsToShow.count, by: pageSize).map { + Array(labelsToShow[$0.. NSAllowsLocalNetworking + NSExceptionRequiresForwardSecrecy + UIBackgroundModes diff --git a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift index 0c9a798..1e6407c 100644 --- a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift @@ -16,35 +16,69 @@ struct BookmarkLabelsView: View { var body: some View { NavigationView { VStack(spacing: 12) { - // Add new label - HStack(spacing: 12) { - TextField("Enter label...", text: $viewModel.newLabelText) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .onSubmit { - Task { - await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText) + // Add new label with search functionality + VStack(spacing: 8) { + HStack(spacing: 12) { + TextField("Search or add new tag...", text: $viewModel.searchText) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .onSubmit { + Task { + await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText) + } } + + Button(action: { + Task { + await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText) + } + }) { + Image(systemName: "plus.circle.fill") + .font(.title2) + .frame(width: 32, height: 32) } - - Button(action: { - Task { - await viewModel.addLabel(to: bookmarkId, label: viewModel.newLabelText) - } - }) { - Image(systemName: "plus.circle.fill") - .font(.title2) - .frame(width: 32, height: 32) + .disabled(viewModel.searchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isLoading) + .foregroundColor(.accentColor) + } + + // Show custom tag suggestion if search text doesn't match existing tags + if !viewModel.searchText.isEmpty && !viewModel.filteredLabels.contains(where: { $0.name.lowercased() == viewModel.searchText.lowercased() }) { + HStack { + Text("Add new tag:") + .font(.caption) + .foregroundColor(.secondary) + Text(viewModel.searchText) + .font(.caption) + .fontWeight(.medium) + Spacer() + Button("Add") { + Task { + await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText) + } + } + .font(.caption) + .foregroundColor(.accentColor) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.accentColor.opacity(0.1)) + .cornerRadius(6) } - .disabled(viewModel.newLabelText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isLoading) - .foregroundColor(.accentColor) } .padding(.horizontal) // All available labels VStack(alignment: .leading, spacing: 8) { - Text("All available tags") - .font(.headline) - .padding(.horizontal) + HStack { + Text(viewModel.searchText.isEmpty ? "All available tags" : "Search results") + .font(.headline) + if !viewModel.searchText.isEmpty { + Text("(\(viewModel.filteredLabels.count) found)") + .font(.caption) + .foregroundColor(.secondary) + } + Spacer() + } + .padding(.horizontal) if viewModel.isInitialLoading { // Loading state @@ -58,14 +92,14 @@ struct BookmarkLabelsView: View { } .frame(maxWidth: .infinity) .frame(height: 180) - } else if viewModel.allLabels.isEmpty { + } else if viewModel.availableLabelPages.isEmpty { // Empty state VStack { - Image(systemName: "tag") + Image(systemName: viewModel.searchText.isEmpty ? "tag" : "magnifyingglass") .font(.system(size: 40)) .foregroundColor(.secondary) .padding(.vertical, 20) - Text("No tags available") + Text(viewModel.searchText.isEmpty ? "No tags available" : "No tags found") .font(.caption) .foregroundColor(.secondary) } diff --git a/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift b/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift index 9cc6852..72735d5 100644 --- a/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift +++ b/readeck/UI/BookmarkDetail/BookmarkLabelsViewModel.swift @@ -16,6 +16,11 @@ class BookmarkLabelsViewModel { } } var newLabelText = "" + var searchText = "" { + didSet { + calculatePages() + } + } var allLabels: [BookmarkLabel] = [] { didSet { @@ -30,6 +35,15 @@ class BookmarkLabelsViewModel { return allLabels.filter { currentLabels.contains($0.name) == false } } + // Computed property for filtered labels based on search text + var filteredLabels: [BookmarkLabel] { + if searchText.isEmpty { + return availableLabels + } else { + return availableLabels.filter { $0.name.localizedCaseInsensitiveContains(searchText) } + } + } + var availableLabelPages: [[BookmarkLabel]] = [] init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared, initialLabels: [String] = []) { @@ -85,6 +99,7 @@ class BookmarkLabelsViewModel { await addLabels(to: bookmarkId, labels: [trimmedLabel]) newLabelText = "" + searchText = "" } @MainActor @@ -142,13 +157,14 @@ class BookmarkLabelsViewModel { } } - // Calculate pages for available labels (excluding current labels) - if availableLabels.count <= pageSize { - availableLabelPages = [availableLabels] + // Calculate pages for filtered labels (search results or available labels) + let labelsToShow = searchText.isEmpty ? availableLabels : filteredLabels + if labelsToShow.count <= pageSize { + availableLabelPages = [labelsToShow] } else { // Normal pagination for larger datasets - availableLabelPages = stride(from: 0, to: availableLabels.count, by: pageSize).map { - Array(availableLabels[$0.. Void)? var onScroll: ((Double) -> Void)? + var hasHeightUpdate: Bool = false func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if navigationAction.navigationType == .linkActivated { @@ -297,6 +298,4 @@ class WebViewCoordinator: NSObject, WKNavigationDelegate, WKScriptMessageHandler } } } - - var hasHeightUpdate: Bool = false } diff --git a/readeck/UI/Factory/MockUseCaseFactory.swift b/readeck/UI/Factory/MockUseCaseFactory.swift index 0213bc2..a8b90ac 100644 --- a/readeck/UI/Factory/MockUseCaseFactory.swift +++ b/readeck/UI/Factory/MockUseCaseFactory.swift @@ -137,6 +137,7 @@ class MockSaveSettingsUseCase: PSaveSettingsUseCase { func execute(token: String) async throws {} func execute(selectedFontFamily: FontFamily, selectedFontSize: FontSize) async throws {} func execute(enableTTS: Bool) async throws {} + func execute(theme: Theme) async throws {} } class MockGetBookmarkUseCase: PGetBookmarkUseCase { diff --git a/readeck/UI/Models/AppSettings.swift b/readeck/UI/Models/AppSettings.swift index aecf792..857d124 100644 --- a/readeck/UI/Models/AppSettings.swift +++ b/readeck/UI/Models/AppSettings.swift @@ -22,6 +22,10 @@ class AppSettings: ObservableObject { var enableTTS: Bool { settings?.enableTTS ?? false } + + var theme: Theme { + settings?.theme ?? .system + } init(settings: Settings? = nil) { self.settings = settings diff --git a/readeck/UI/Settings/SettingsContainerView.swift b/readeck/UI/Settings/SettingsContainerView.swift index e1741ae..80b31af 100644 --- a/readeck/UI/Settings/SettingsContainerView.swift +++ b/readeck/UI/Settings/SettingsContainerView.swift @@ -18,19 +18,21 @@ struct SettingsContainerView: View { var body: some View { ScrollView { LazyVStack(spacing: 20) { - SettingsServerView() - .cardStyle() - FontSettingsView() .cardStyle() SettingsGeneralView() .cardStyle() + + SettingsServerView() + .cardStyle() } .padding() .background(Color(.systemGroupedBackground)) AppInfo() + + Spacer() } .background(Color(.systemGroupedBackground)) .navigationTitle("Settings") diff --git a/readeck/UI/Settings/SettingsGeneralView.swift b/readeck/UI/Settings/SettingsGeneralView.swift index f7d9afc..9145498 100644 --- a/readeck/UI/Settings/SettingsGeneralView.swift +++ b/readeck/UI/Settings/SettingsGeneralView.swift @@ -29,6 +29,11 @@ struct SettingsGeneralView: View { } } .pickerStyle(.segmented) + .onChange(of: viewModel.selectedTheme) { + Task { + await viewModel.saveGeneralSettings() + } + } } VStack(alignment: .leading, spacing: 12) { diff --git a/readeck/UI/Settings/SettingsGeneralViewModel.swift b/readeck/UI/Settings/SettingsGeneralViewModel.swift index 10b12c0..79f38b8 100644 --- a/readeck/UI/Settings/SettingsGeneralViewModel.swift +++ b/readeck/UI/Settings/SettingsGeneralViewModel.swift @@ -35,7 +35,7 @@ class SettingsGeneralViewModel { do { if let settings = try await loadSettingsUseCase.execute() { enableTTS = settings.enableTTS ?? false - selectedTheme = .system + selectedTheme = settings.theme ?? .system autoSyncEnabled = false } } catch { @@ -47,7 +47,12 @@ class SettingsGeneralViewModel { func saveGeneralSettings() async { do { try await saveSettingsUseCase.execute(enableTTS: enableTTS) + try await saveSettingsUseCase.execute(theme: selectedTheme) + successMessage = "Settings saved" + + // send notification to apply settings to the app + NotificationCenter.default.post(name: NSNotification.Name("SettingsChanged"), object: nil) } catch { errorMessage = "Error saving settings" } diff --git a/readeck/UI/Settings/SettingsServerView.swift b/readeck/UI/Settings/SettingsServerView.swift index 6534760..256d456 100644 --- a/readeck/UI/Settings/SettingsServerView.swift +++ b/readeck/UI/Settings/SettingsServerView.swift @@ -34,43 +34,61 @@ struct SettingsServerView: View { VStack(alignment: .leading, spacing: 6) { Text("Server Endpoint") .font(.headline) - TextField("https://readeck.example.com", text: $viewModel.endpoint) - .textFieldStyle(.roundedBorder) - .keyboardType(.URL) - .autocapitalization(.none) - .disableAutocorrection(true) - .disabled(!viewModel.isSetupMode) - .onChange(of: viewModel.endpoint) { - if viewModel.isSetupMode { + if viewModel.isSetupMode { + TextField("https://readeck.example.com", text: $viewModel.endpoint) + .textFieldStyle(.roundedBorder) + .keyboardType(.URL) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: viewModel.endpoint) { viewModel.clearMessages() } + } else { + HStack { + Image(systemName: "server.rack") + .foregroundColor(.accentColor) + Text(viewModel.endpoint.isEmpty ? "Not set" : viewModel.endpoint) + .foregroundColor(viewModel.endpoint.isEmpty ? .secondary : .primary) + Spacer() } + .padding(.horizontal, 12) + .padding(.vertical, 8) + } } VStack(alignment: .leading, spacing: 6) { Text("Username") .font(.headline) - TextField("Your Username", text: $viewModel.username) - .textFieldStyle(.roundedBorder) - .autocapitalization(.none) - .disableAutocorrection(true) - .disabled(!viewModel.isSetupMode) - .onChange(of: viewModel.username) { - if viewModel.isSetupMode { + if viewModel.isSetupMode { + TextField("Your Username", text: $viewModel.username) + .textFieldStyle(.roundedBorder) + .autocapitalization(.none) + .disableAutocorrection(true) + .onChange(of: viewModel.username) { viewModel.clearMessages() } + } else { + HStack { + Image(systemName: "person.circle.fill") + .foregroundColor(.accentColor) + Text(viewModel.username.isEmpty ? "Not set" : viewModel.username) + .foregroundColor(viewModel.username.isEmpty ? .secondary : .primary) + Spacer() } + .padding(.horizontal, 12) + .padding(.vertical, 8) + } } - VStack(alignment: .leading, spacing: 6) { - Text("Password") - .font(.headline) - SecureField("Your Password", text: $viewModel.password) - .textFieldStyle(.roundedBorder) - .disabled(!viewModel.isSetupMode) - .onChange(of: viewModel.password) { - if viewModel.isSetupMode { + if viewModel.isSetupMode { + VStack(alignment: .leading, spacing: 6) { + Text("Password") + .font(.headline) + + SecureField("Your Password", text: $viewModel.password) + .textFieldStyle(.roundedBorder) + .onChange(of: viewModel.password) { viewModel.clearMessages() } - } + } } } diff --git a/readeck/UI/readeckApp.swift b/readeck/UI/readeckApp.swift index 2df98fb..2e225e8 100644 --- a/readeck/UI/readeckApp.swift +++ b/readeck/UI/readeckApp.swift @@ -24,6 +24,7 @@ struct readeckApp: App { } } .environmentObject(appSettings) + .preferredColorScheme(appSettings.theme.colorScheme) .onAppear { #if DEBUG NFX.sharedInstance().start() @@ -37,6 +38,11 @@ struct readeckApp: App { await loadSetupStatus() } } + .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("SettingsChanged"))) { _ in + Task { + await loadSetupStatus() + } + } } } diff --git a/readeck/readeck.xcdatamodeld/readeck.xcdatamodel/contents b/readeck/readeck.xcdatamodeld/readeck.xcdatamodel/contents index 4f1c892..a8ef2ad 100644 --- a/readeck/readeck.xcdatamodeld/readeck.xcdatamodel/contents +++ b/readeck/readeck.xcdatamodeld/readeck.xcdatamodel/contents @@ -6,6 +6,7 @@ +