diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index a20e5fa..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,27 +0,0 @@ -# Changelog - -All changes to this project will be documented in this file. - -## Planned for Version 1.0.0 - -**Initial release:** -- Browse and manage bookmarks (All, Unread, Favorites, Archive, Article, Videos, Pictures) -- Share Extension for adding URLs from Safari and other apps -- Swipe actions for quick bookmark management -- Native iOS design with Dark Mode support -- Full iPad Support with Multi-Column Split View -- Font Customization -- Article View with Reading Time and Word Count -- Search functionality -- Support for tags -- Support for reading progress -- Save bookmarks when server is unavailable and sync when reconnected - -## Planned for Version 1.1.0 - -- [ ] Add support for bookmark filtering and sorting options -- [ ] Add support for collection management -- [ ] Add support for custom themes -- [ ] Text highlighting of selected text in a article -- [ ] Multiple selection of bookmarks for bulk actions - diff --git a/README.md b/README.md index ae254e1..ce6d876 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ For early access to new features and beta versions (use with caution). To partic What to test: - See the feature list below for an overview of what you can try out. -- For details and recent changes, please refer to the release notes in TestFlight or the [Changelog](./CHANGELOG.md). +- For details and recent changes, please refer to the release notes in TestFlight or the [Release Notes](./readeck/UI/Resources/RELEASE_NOTES.md). Please report any bugs, crashes, or suggestions directly through TestFlight, or email me at ilhallak@gmail.com. Thank you for helping make Readeck better! @@ -84,7 +84,7 @@ The app includes a Share Extension that allows adding bookmarks directly from Sa ## Versions -[see Changelog](./CHANGELOG.md) +[see Release Notes](./readeck/UI/Resources/RELEASE_NOTES.md) ## Contributing diff --git a/URLShare/ShareBookmarkView.swift b/URLShare/ShareBookmarkView.swift index 5778a21..933431e 100644 --- a/URLShare/ShareBookmarkView.swift +++ b/URLShare/ShareBookmarkView.swift @@ -142,7 +142,8 @@ struct ShareBookmarkView: View { searchFieldFocus: $focusedField, fetchLimit: 150, sortOrder: viewModel.tagSortOrder, - availableTagsTitle: "Most used tags", + availableLabelsTitle: "Most used labels", + context: viewContext, onAddCustomTag: { addCustomTag() }, @@ -200,19 +201,6 @@ struct ShareBookmarkView: View { // MARK: - Helper Functions private func addCustomTag() { - let splitLabels = LabelUtils.splitLabelsFromInput(viewModel.searchText) - - // Fetch available labels from Core Data - let fetchRequest: NSFetchRequest = TagEntity.fetchRequest() - let availableLabels = (try? viewContext.fetch(fetchRequest))?.compactMap { $0.name } ?? [] - - let currentLabels = Array(viewModel.selectedLabels) - let uniqueLabels = LabelUtils.filterUniqueLabels(splitLabels, currentLabels: currentLabels, availableLabels: availableLabels) - - for label in uniqueLabels { - viewModel.selectedLabels.insert(label) - } - - viewModel.searchText = "" + viewModel.addCustomTag(context: viewContext) } } diff --git a/URLShare/ShareBookmarkViewModel.swift b/URLShare/ShareBookmarkViewModel.swift index 66941a8..1d22ad5 100644 --- a/URLShare/ShareBookmarkViewModel.swift +++ b/URLShare/ShareBookmarkViewModel.swift @@ -16,6 +16,7 @@ class ShareBookmarkViewModel: ObservableObject { private let logger = Logger.viewModel private let serverCheck = ShareExtensionServerCheck.shared + private let tagRepository = TagRepository() init(extensionContext: NSExtensionContext?) { self.extensionContext = extensionContext @@ -149,14 +150,37 @@ class ShareBookmarkViewModel: ObservableObject { } } } - + + func addCustomTag(context: NSManagedObjectContext) { + let splitLabels = LabelUtils.splitLabelsFromInput(searchText) + + // Fetch available labels from Core Data + let fetchRequest: NSFetchRequest = TagEntity.fetchRequest() + let availableLabels = (try? context.fetch(fetchRequest))?.compactMap { $0.name } ?? [] + + let currentLabels = Array(selectedLabels) + let uniqueLabels = LabelUtils.filterUniqueLabels(splitLabels, currentLabels: currentLabels, availableLabels: availableLabels) + + for label in uniqueLabels { + selectedLabels.insert(label) + // Save new label to Core Data so it's available next time + tagRepository.saveNewLabel(name: label, context: context) + } + + // Force refresh of @FetchRequest in CoreDataTagManagementView + // This ensures newly created labels appear immediately in the search results + context.refreshAllObjects() + + searchText = "" + } + private func completeExtensionRequest() { logger.debug("Completing extension request") guard let context = extensionContext else { logger.warning("Extension context not available for completion") return } - + context.completeRequest(returningItems: []) { [weak self] error in if error { self?.logger.error("Extension completion failed: \(error)") diff --git a/URLShare/TagRepository.swift b/URLShare/TagRepository.swift new file mode 100644 index 0000000..26b2e8d --- /dev/null +++ b/URLShare/TagRepository.swift @@ -0,0 +1,63 @@ +import Foundation +import CoreData + +/// Simple repository for managing tags in Share Extension +class TagRepository { + + private let logger = Logger.data + + /// Saves a new label to Core Data if it doesn't already exist + /// - Parameters: + /// - name: The label name to save + /// - context: The managed object context to use + func saveNewLabel(name: String, context: NSManagedObjectContext) { + let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedName.isEmpty else { return } + + // Perform save in a synchronous block to ensure it completes before extension closes + context.performAndWait { + // Check if label already exists + let fetchRequest: NSFetchRequest = TagEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "name == %@", trimmedName) + fetchRequest.fetchLimit = 1 + + do { + let existingTags = try context.fetch(fetchRequest) + + // Only create if it doesn't exist + if existingTags.isEmpty { + let newTag = TagEntity(context: context) + newTag.name = trimmedName + newTag.count = 1 // New label is being used immediately + + try context.save() + logger.info("Successfully saved new label '\(trimmedName)' to Core Data") + + // Force immediate persistence to disk for share extension + // Based on: https://www.avanderlee.com/swift/core-data-app-extension-data-sharing/ + // 1. Process pending changes + context.processPendingChanges() + + // 2. Ensure persistent store coordinator writes to disk + // This is critical for extensions as they may be terminated quickly + if context.persistentStoreCoordinator != nil { + // Refresh all objects to ensure changes are pushed to store + context.refreshAllObjects() + + // Reset staleness interval temporarily to force immediate persistence + let originalStalenessInterval = context.stalenessInterval + context.stalenessInterval = 0 + context.refreshAllObjects() + context.stalenessInterval = originalStalenessInterval + + logger.debug("Forced context refresh to ensure persistence") + } + } else { + logger.debug("Label '\(trimmedName)' already exists, skipping creation") + } + } catch { + logger.error("Failed to save new label '\(trimmedName)': \(error.localizedDescription)") + } + } + } +} diff --git a/readeck.xcodeproj/project.pbxproj b/readeck.xcodeproj/project.pbxproj index 8ec5892..ba2ec86 100644 --- a/readeck.xcodeproj/project.pbxproj +++ b/readeck.xcodeproj/project.pbxproj @@ -452,7 +452,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 36; DEVELOPMENT_TEAM = 8J69P655GN; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = URLShare/Info.plist; @@ -485,7 +485,7 @@ buildSettings = { CODE_SIGN_ENTITLEMENTS = URLShare/URLShare.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 36; DEVELOPMENT_TEAM = 8J69P655GN; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = URLShare/Info.plist; @@ -640,7 +640,7 @@ CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 36; DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\""; DEVELOPMENT_TEAM = 8J69P655GN; ENABLE_HARDENED_RUNTIME = YES; @@ -684,7 +684,7 @@ CODE_SIGN_ENTITLEMENTS = readeck/readeck.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 36; DEVELOPMENT_ASSET_PATHS = "\"readeck/Preview Content\""; DEVELOPMENT_TEAM = 8J69P655GN; ENABLE_HARDENED_RUNTIME = YES; diff --git a/readeck/Data/CoreData/CoreDataManager.swift b/readeck/Data/CoreData/CoreDataManager.swift index b5b6593..7bb0fa0 100644 --- a/readeck/Data/CoreData/CoreDataManager.swift +++ b/readeck/Data/CoreData/CoreDataManager.swift @@ -43,6 +43,11 @@ class CoreDataManager { self?.logger.info("Core Data persistent store loaded successfully") } } + + // Configure viewContext for better extension support + container.viewContext.automaticallyMergesChangesFromParent = true + container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy + return container }() diff --git a/readeck/Data/Repository/LabelsRepository.swift b/readeck/Data/Repository/LabelsRepository.swift index 989e433..4b91528 100644 --- a/readeck/Data/Repository/LabelsRepository.swift +++ b/readeck/Data/Repository/LabelsRepository.swift @@ -89,4 +89,29 @@ class LabelsRepository: PLabelsRepository, @unchecked Sendable { } } } + + func saveNewLabel(name: String) async throws { + let backgroundContext = coreDataManager.newBackgroundContext() + + try await backgroundContext.perform { + let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines) + guard !trimmedName.isEmpty else { return } + + // Check if label already exists + let fetchRequest: NSFetchRequest = TagEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "name == %@", trimmedName) + fetchRequest.fetchLimit = 1 + + let existingTags = try backgroundContext.fetch(fetchRequest) + + // Only create if it doesn't exist + if existingTags.isEmpty { + let newTag = TagEntity(context: backgroundContext) + newTag.name = trimmedName + newTag.count = 1 // New label is being used immediately + + try backgroundContext.save() + } + } + } } diff --git a/readeck/Domain/Protocols/PLabelsRepository.swift b/readeck/Domain/Protocols/PLabelsRepository.swift index 4ad6cf6..f33863a 100644 --- a/readeck/Domain/Protocols/PLabelsRepository.swift +++ b/readeck/Domain/Protocols/PLabelsRepository.swift @@ -3,4 +3,5 @@ import Foundation protocol PLabelsRepository { func getLabels() async throws -> [BookmarkLabel] func saveLabels(_ dtos: [BookmarkLabelDto]) async throws + func saveNewLabel(name: String) async throws } diff --git a/readeck/Domain/UseCase/CreateLabelUseCase.swift b/readeck/Domain/UseCase/CreateLabelUseCase.swift new file mode 100644 index 0000000..722f444 --- /dev/null +++ b/readeck/Domain/UseCase/CreateLabelUseCase.swift @@ -0,0 +1,17 @@ +import Foundation + +protocol PCreateLabelUseCase { + func execute(name: String) async throws +} + +class CreateLabelUseCase: PCreateLabelUseCase { + private let labelsRepository: PLabelsRepository + + init(labelsRepository: PLabelsRepository) { + self.labelsRepository = labelsRepository + } + + func execute(name: String) async throws { + try await labelsRepository.saveNewLabel(name: name) + } +} diff --git a/readeck/UI/AddBookmark/AddBookmarkView.swift b/readeck/UI/AddBookmark/AddBookmarkView.swift index 65f22f1..f9da7a5 100644 --- a/readeck/UI/AddBookmark/AddBookmarkView.swift +++ b/readeck/UI/AddBookmark/AddBookmarkView.swift @@ -190,6 +190,7 @@ struct AddBookmarkView: View { searchFieldFocus: $focusedField, fetchLimit: nil, sortOrder: appSettings.tagSortOrder, + context: viewContext, onAddCustomTag: { viewModel.addCustomTag() }, diff --git a/readeck/UI/AddBookmark/AddBookmarkViewModel.swift b/readeck/UI/AddBookmark/AddBookmarkViewModel.swift index 01f113a..5e4a2d8 100644 --- a/readeck/UI/AddBookmark/AddBookmarkViewModel.swift +++ b/readeck/UI/AddBookmark/AddBookmarkViewModel.swift @@ -8,6 +8,7 @@ class AddBookmarkViewModel { private let createBookmarkUseCase = DefaultUseCaseFactory.shared.makeCreateBookmarkUseCase() private let getLabelsUseCase = DefaultUseCaseFactory.shared.makeGetLabelsUseCase() + private let createLabelUseCase = DefaultUseCaseFactory.shared.makeCreateLabelUseCase() private let syncTagsUseCase = DefaultUseCaseFactory.shared.makeSyncTagsUseCase() // MARK: - Form Data @@ -87,17 +88,22 @@ class AddBookmarkViewModel { func addCustomTag() { let trimmed = searchText.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } - + let lowercased = trimmed.lowercased() let allExisting = Set(allLabels.map { $0.name.lowercased() }) let allSelected = Set(selectedLabels.map { $0.lowercased() }) - + if allExisting.contains(lowercased) || allSelected.contains(lowercased) { // Tag already exists, don't add return } else { selectedLabels.insert(trimmed) searchText = "" + + // Save new label to Core Data so it's available next time + Task { + try? await createLabelUseCase.execute(name: trimmed) + } } } diff --git a/readeck/UI/AppViewModel.swift b/readeck/UI/AppViewModel.swift index cf97bc9..43f3277 100644 --- a/readeck/UI/AppViewModel.swift +++ b/readeck/UI/AppViewModel.swift @@ -13,12 +13,16 @@ import SwiftUI class AppViewModel { private let settingsRepository = SettingsRepository() private let factory: UseCaseFactory + private let syncTagsUseCase: PSyncTagsUseCase var hasFinishedSetup: Bool = true var isServerReachable: Bool = false + private var lastAppStartTagSyncTime: Date? + init(factory: UseCaseFactory = DefaultUseCaseFactory.shared) { self.factory = factory + self.syncTagsUseCase = factory.makeSyncTagsUseCase() setupNotificationObservers() loadSetupStatus() @@ -65,12 +69,29 @@ class AppViewModel { func onAppResume() async { await checkServerReachability() + await syncTagsOnAppStart() } private func checkServerReachability() async { isServerReachable = await factory.makeCheckServerReachabilityUseCase().execute() } + private func syncTagsOnAppStart() async { + let now = Date() + + // Check if last sync was less than 2 minutes ago + if let lastSync = lastAppStartTagSyncTime, + now.timeIntervalSince(lastSync) < 120 { + print("AppViewModel: Skipping tag sync - last sync was less than 2 minutes ago") + return + } + + // Sync tags from server to Core Data + print("AppViewModel: Syncing tags on app start") + try? await syncTagsUseCase.execute() + lastAppStartTagSyncTime = now + } + deinit { NotificationCenter.default.removeObserver(self) } diff --git a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift index 27c86b0..6e1bb01 100644 --- a/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift +++ b/readeck/UI/BookmarkDetail/BookmarkLabelsView.swift @@ -71,6 +71,7 @@ struct BookmarkLabelsView: View { searchText: $viewModel.searchText, fetchLimit: nil, sortOrder: appSettings.tagSortOrder, + context: viewContext, onAddCustomTag: { Task { await viewModel.addLabel(to: bookmarkId, label: viewModel.searchText) diff --git a/readeck/UI/Components/CoreDataTagManagementView.swift b/readeck/UI/Components/CoreDataTagManagementView.swift index 24f630b..665dc4f 100644 --- a/readeck/UI/Components/CoreDataTagManagementView.swift +++ b/readeck/UI/Components/CoreDataTagManagementView.swift @@ -9,7 +9,8 @@ struct CoreDataTagManagementView: View { let searchText: Binding let searchFieldFocus: FocusState.Binding? let sortOrder: TagSortOrder - let availableTagsTitle: String? + let availableLabelsTitle: String? + let context: NSManagedObjectContext // MARK: - Callbacks @@ -22,6 +23,11 @@ struct CoreDataTagManagementView: View { @FetchRequest private var tagEntities: FetchedResults + // MARK: - Search State + + @State private var searchResults: [TagEntity] = [] + @State private var isSearchActive: Bool = false + // MARK: - Initialization init( @@ -30,7 +36,8 @@ struct CoreDataTagManagementView: View { searchFieldFocus: FocusState.Binding? = nil, fetchLimit: Int? = nil, sortOrder: TagSortOrder = .byCount, - availableTagsTitle: String? = nil, + availableLabelsTitle: String? = nil, + context: NSManagedObjectContext, onAddCustomTag: @escaping () -> Void, onToggleLabel: @escaping (String) -> Void, onRemoveLabel: @escaping (String) -> Void @@ -39,7 +46,8 @@ struct CoreDataTagManagementView: View { self.searchText = searchText self.searchFieldFocus = searchFieldFocus self.sortOrder = sortOrder - self.availableTagsTitle = availableTagsTitle + self.availableLabelsTitle = availableLabelsTitle + self.context = context self.onAddCustomTag = onAddCustomTag self.onToggleLabel = onToggleLabel self.onRemoveLabel = onRemoveLabel @@ -79,13 +87,16 @@ struct CoreDataTagManagementView: View { availableLabels selectedLabels } + .onChange(of: searchText.wrappedValue) { oldValue, newValue in + performSearch(query: newValue) + } } // MARK: - View Components @ViewBuilder private var searchField: some View { - TextField("Search or add new tag...", text: searchText) + TextField("Search or add new label...", text: searchText) .textFieldStyle(CustomTextFieldStyle()) .keyboardType(.default) .autocorrectionDisabled(true) @@ -102,7 +113,7 @@ struct CoreDataTagManagementView: View { !allTagNames.contains(where: { $0.lowercased() == searchText.wrappedValue.lowercased() }) && !selectedLabelsSet.contains(searchText.wrappedValue) { HStack { - Text("Add new tag:") + Text("Add new label:") .font(.subheadline) .foregroundColor(.secondary) Text(searchText.wrappedValue) @@ -132,7 +143,7 @@ struct CoreDataTagManagementView: View { if !tagEntities.isEmpty { VStack(alignment: .leading, spacing: 8) { HStack { - Text(searchText.wrappedValue.isEmpty ? (availableTagsTitle ?? "Available tags") : "Search results") + Text(searchText.wrappedValue.isEmpty ? (availableLabelsTitle ?? "Available labels") : "Search results") .font(.subheadline) .fontWeight(.medium) if !searchText.wrappedValue.isEmpty { @@ -144,16 +155,31 @@ struct CoreDataTagManagementView: View { } if availableUnselectedTagsCount == 0 { - VStack { - Image(systemName: "checkmark.circle.fill") - .font(.system(size: 24)) - .foregroundColor(.green) - Text("All tags selected") - .font(.caption) - .foregroundColor(.secondary) + // Show "All labels selected" only if there are actually filtered results + // Otherwise show "No labels found" for empty search results + if filteredTagsCount > 0 { + VStack { + Image(systemName: "checkmark.circle.fill") + .font(.system(size: 24)) + .foregroundColor(.green) + Text("All labels selected") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 20) + } else if !searchText.wrappedValue.isEmpty { + VStack { + Image(systemName: "magnifyingglass") + .font(.system(size: 24)) + .foregroundColor(.secondary) + Text("No labels found") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 20) } - .frame(maxWidth: .infinity) - .padding(.vertical, 20) } else { labelsScrollView } @@ -174,17 +200,26 @@ struct CoreDataTagManagementView: View { alignment: .top, spacing: 8 ) { - ForEach(tagEntities, id: \.objectID) { entity in - if let name = entity.name, shouldShowTag(name) { - UnifiedLabelChip( - label: name, - isSelected: false, - isRemovable: false, - onTap: { - onToggleLabel(name) - } - ) - .fixedSize(horizontal: true, vertical: false) + // Use searchResults when search is active, otherwise use tagEntities + let tagsToDisplay = isSearchActive ? searchResults : Array(tagEntities) + + ForEach(tagsToDisplay, id: \.objectID) { entity in + if let name = entity.name { + // When searching, show all results (already filtered by predicate) + // When not searching, filter with shouldShowTag() + let shouldShow = isSearchActive ? !selectedLabelsSet.contains(name) : shouldShowTag(name) + + if shouldShow { + UnifiedLabelChip( + label: name, + isSelected: false, + isRemovable: false, + onTap: { + onToggleLabel(name) + } + ) + .fixedSize(horizontal: true, vertical: false) + } } } } @@ -200,7 +235,9 @@ struct CoreDataTagManagementView: View { } private var filteredTagsCount: Int { - if searchText.wrappedValue.isEmpty { + if isSearchActive { + return searchResults.count + } else if searchText.wrappedValue.isEmpty { return tagEntities.count } else { return tagEntities.filter { entity in @@ -211,12 +248,19 @@ struct CoreDataTagManagementView: View { } private var availableUnselectedTagsCount: Int { - tagEntities.filter { entity in - guard let name = entity.name else { return false } - let matchesSearch = searchText.wrappedValue.isEmpty || name.localizedCaseInsensitiveContains(searchText.wrappedValue) - let isNotSelected = !selectedLabelsSet.contains(name) - return matchesSearch && isNotSelected - }.count + if isSearchActive { + return searchResults.filter { entity in + guard let name = entity.name else { return false } + return !selectedLabelsSet.contains(name) + }.count + } else { + return tagEntities.filter { entity in + guard let name = entity.name else { return false } + let matchesSearch = searchText.wrappedValue.isEmpty || name.localizedCaseInsensitiveContains(searchText.wrappedValue) + let isNotSelected = !selectedLabelsSet.contains(name) + return matchesSearch && isNotSelected + }.count + } } private func shouldShowTag(_ name: String) -> Bool { @@ -225,11 +269,42 @@ struct CoreDataTagManagementView: View { return matchesSearch && isNotSelected } + private func performSearch(query: String) { + guard !query.isEmpty else { + isSearchActive = false + searchResults = [] + return + } + + // Search directly in Core Data without fetchLimit + let fetchRequest: NSFetchRequest = TagEntity.fetchRequest() + fetchRequest.predicate = NSPredicate(format: "name CONTAINS[cd] %@", query) + + // Use same sort order as main fetch + let sortDescriptors: [NSSortDescriptor] + switch sortOrder { + case .byCount: + sortDescriptors = [ + NSSortDescriptor(keyPath: \TagEntity.count, ascending: false), + NSSortDescriptor(keyPath: \TagEntity.name, ascending: true) + ] + case .alphabetically: + sortDescriptors = [ + NSSortDescriptor(keyPath: \TagEntity.name, ascending: true) + ] + } + fetchRequest.sortDescriptors = sortDescriptors + + // NO fetchLimit - search ALL tags in database + searchResults = (try? context.fetch(fetchRequest)) ?? [] + isSearchActive = true + } + @ViewBuilder private var selectedLabels: some View { if !selectedLabelsSet.isEmpty { VStack(alignment: .leading, spacing: 8) { - Text("Selected tags") + Text("Selected labels") .font(.subheadline) .fontWeight(.medium) diff --git a/readeck/UI/Factory/DefaultUseCaseFactory.swift b/readeck/UI/Factory/DefaultUseCaseFactory.swift index dd23504..7ed78fe 100644 --- a/readeck/UI/Factory/DefaultUseCaseFactory.swift +++ b/readeck/UI/Factory/DefaultUseCaseFactory.swift @@ -16,6 +16,7 @@ protocol UseCaseFactory { func makeAddLabelsToBookmarkUseCase() -> PAddLabelsToBookmarkUseCase func makeRemoveLabelsFromBookmarkUseCase() -> PRemoveLabelsFromBookmarkUseCase func makeGetLabelsUseCase() -> PGetLabelsUseCase + func makeCreateLabelUseCase() -> PCreateLabelUseCase func makeSyncTagsUseCase() -> PSyncTagsUseCase func makeAddTextToSpeechQueueUseCase() -> PAddTextToSpeechQueueUseCase func makeOfflineBookmarkSyncUseCase() -> POfflineBookmarkSyncUseCase @@ -104,6 +105,12 @@ class DefaultUseCaseFactory: UseCaseFactory { return GetLabelsUseCase(labelsRepository: labelsRepository) } + func makeCreateLabelUseCase() -> PCreateLabelUseCase { + let api = API(tokenProvider: KeychainTokenProvider()) + let labelsRepository = LabelsRepository(api: api) + return CreateLabelUseCase(labelsRepository: labelsRepository) + } + func makeSyncTagsUseCase() -> PSyncTagsUseCase { let api = API(tokenProvider: KeychainTokenProvider()) let labelsRepository = LabelsRepository(api: api) diff --git a/readeck/UI/Factory/MockUseCaseFactory.swift b/readeck/UI/Factory/MockUseCaseFactory.swift index a86aba6..a6659fc 100644 --- a/readeck/UI/Factory/MockUseCaseFactory.swift +++ b/readeck/UI/Factory/MockUseCaseFactory.swift @@ -77,6 +77,10 @@ class MockUseCaseFactory: UseCaseFactory { MockGetLabelsUseCase() } + func makeCreateLabelUseCase() -> any PCreateLabelUseCase { + MockCreateLabelUseCase() + } + func makeSyncTagsUseCase() -> any PSyncTagsUseCase { MockSyncTagsUseCase() } @@ -129,6 +133,12 @@ class MockGetLabelsUseCase: PGetLabelsUseCase { } } +class MockCreateLabelUseCase: PCreateLabelUseCase { + func execute(name: String) async throws { + // Mock implementation - does nothing + } +} + class MockSyncTagsUseCase: PSyncTagsUseCase { func execute() async throws { // Mock implementation - does nothing