This commit introduces a comprehensive refactoring of the tag management system, replacing the previous API-based approach with a Core Data-first strategy for improved performance and offline support. Major Changes: Tag Management Architecture: - Add CoreDataTagManagementView using @FetchRequest for reactive updates - Implement cache-first sync strategy in LabelsRepository - Create SyncTagsUseCase following Clean Architecture principles - Add TagSortOrder enum for configurable tag sorting (by count/alphabetically) - Mark LegacyTagManagementView as deprecated Share Extension Improvements: - Replace API-based tag loading with Core Data queries - Display top 150 tags sorted by usage count - Remove unnecessary label fetching logic - Add "Most used tags" localized title - Improve offline bookmark tag management Main App Enhancements: - Add tag sync triggers in AddBookmarkView and BookmarkLabelsView - Implement user-configurable tag sorting in settings - Add sort order indicator labels with localization - Automatic UI updates via SwiftUI @FetchRequest reactivity Settings & Configuration: - Add TagSortOrder setting with persistence - Refactor Settings model structure - Add FontFamily and FontSize domain models - Improve settings repository with tag sort order support Use Case Layer: - Add SyncTagsUseCase for background tag synchronization - Update UseCaseFactory with tag sync support - Add mock implementations for testing Localization: - Add German and English translations for: - "Most used tags" - "Sorted by usage count" - "Sorted alphabetically" Technical Improvements: - Batch tag updates with conflict detection - Background sync with silent failure handling - Reduced server load through local caching - Better separation of concerns following Clean Architecture
278 lines
9.8 KiB
Swift
278 lines
9.8 KiB
Swift
//
|
|
// MockUseCaseFactory.swift
|
|
// readeck
|
|
//
|
|
// Created by Ilyas Hallak on 18.07.25.
|
|
//
|
|
|
|
import Foundation
|
|
import Combine
|
|
|
|
class MockUseCaseFactory: UseCaseFactory {
|
|
func makeCheckServerReachabilityUseCase() -> any PCheckServerReachabilityUseCase {
|
|
MockCheckServerReachabilityUseCase()
|
|
}
|
|
|
|
func makeOfflineBookmarkSyncUseCase() -> any POfflineBookmarkSyncUseCase {
|
|
MockOfflineBookmarkSyncUseCase()
|
|
}
|
|
|
|
func makeLoginUseCase() -> any PLoginUseCase {
|
|
MockLoginUserCase()
|
|
}
|
|
|
|
func makeGetBookmarksUseCase() -> any PGetBookmarksUseCase {
|
|
MockGetBookmarksUseCase()
|
|
}
|
|
|
|
func makeGetBookmarkUseCase() -> any PGetBookmarkUseCase {
|
|
MockGetBookmarkUseCase()
|
|
}
|
|
|
|
func makeGetBookmarkArticleUseCase() -> any PGetBookmarkArticleUseCase {
|
|
MockGetBookmarkArticleUseCase()
|
|
}
|
|
|
|
func makeSaveSettingsUseCase() -> any PSaveSettingsUseCase {
|
|
MockSaveSettingsUseCase()
|
|
}
|
|
|
|
func makeLoadSettingsUseCase() -> any PLoadSettingsUseCase {
|
|
MockLoadSettingsUseCase()
|
|
}
|
|
|
|
func makeUpdateBookmarkUseCase() -> any PUpdateBookmarkUseCase {
|
|
MockUpdateBookmarkUseCase()
|
|
}
|
|
|
|
func makeDeleteBookmarkUseCase() -> any PDeleteBookmarkUseCase {
|
|
MockDeleteBookmarkUseCase()
|
|
}
|
|
|
|
func makeCreateBookmarkUseCase() -> any PCreateBookmarkUseCase {
|
|
MockCreateBookmarkUseCase()
|
|
}
|
|
|
|
func makeLogoutUseCase() -> any PLogoutUseCase {
|
|
MockLogoutUseCase()
|
|
}
|
|
|
|
func makeSearchBookmarksUseCase() -> any PSearchBookmarksUseCase {
|
|
MockSearchBookmarksUseCase()
|
|
}
|
|
|
|
func makeSaveServerSettingsUseCase() -> any PSaveServerSettingsUseCase {
|
|
MockSaveServerSettingsUseCase()
|
|
}
|
|
|
|
func makeAddLabelsToBookmarkUseCase() -> any PAddLabelsToBookmarkUseCase {
|
|
MockAddLabelsToBookmarkUseCase()
|
|
}
|
|
|
|
func makeRemoveLabelsFromBookmarkUseCase() -> any PRemoveLabelsFromBookmarkUseCase {
|
|
MockRemoveLabelsFromBookmarkUseCase()
|
|
}
|
|
|
|
func makeGetLabelsUseCase() -> any PGetLabelsUseCase {
|
|
MockGetLabelsUseCase()
|
|
}
|
|
|
|
func makeSyncTagsUseCase() -> any PSyncTagsUseCase {
|
|
MockSyncTagsUseCase()
|
|
}
|
|
|
|
func makeAddTextToSpeechQueueUseCase() -> any PAddTextToSpeechQueueUseCase {
|
|
MockAddTextToSpeechQueueUseCase()
|
|
}
|
|
|
|
func makeLoadCardLayoutUseCase() -> PLoadCardLayoutUseCase {
|
|
MockLoadCardLayoutUseCase()
|
|
}
|
|
|
|
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase {
|
|
MockSaveCardLayoutUseCase()
|
|
}
|
|
|
|
func makeGetBookmarkAnnotationsUseCase() -> PGetBookmarkAnnotationsUseCase {
|
|
MockGetBookmarkAnnotationsUseCase()
|
|
}
|
|
|
|
func makeDeleteAnnotationUseCase() -> PDeleteAnnotationUseCase {
|
|
MockDeleteAnnotationUseCase()
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: Mocked Use Cases
|
|
|
|
class MockLoginUserCase: PLoginUseCase {
|
|
func execute(endpoint: String, username: String, password: String) async throws -> User {
|
|
return User(id: "123", token: "abc")
|
|
}
|
|
}
|
|
|
|
class MockLogoutUseCase: PLogoutUseCase {
|
|
func execute() async throws {}
|
|
}
|
|
|
|
class MockCreateBookmarkUseCase: PCreateBookmarkUseCase {
|
|
func execute(createRequest: CreateBookmarkRequest) async throws -> String { "mock-bookmark-id" }
|
|
func createFromURL(_ url: String) async throws -> String { "mock-bookmark-id" }
|
|
func createFromURLWithTitle(_ url: String, title: String) async throws -> String { "mock-bookmark-id" }
|
|
func createFromURLWithLabels(_ url: String, labels: [String]) async throws -> String { "mock-bookmark-id" }
|
|
func createFromClipboard() async throws -> String? { "mock-bookmark-id" }
|
|
}
|
|
|
|
class MockGetLabelsUseCase: PGetLabelsUseCase {
|
|
func execute() async throws -> [BookmarkLabel] {
|
|
[BookmarkLabel(name: "Test", count: 1, href: "mock-href")]
|
|
}
|
|
}
|
|
|
|
class MockSyncTagsUseCase: PSyncTagsUseCase {
|
|
func execute() async throws {
|
|
// Mock implementation - does nothing
|
|
}
|
|
}
|
|
|
|
class MockSearchBookmarksUseCase: PSearchBookmarksUseCase {
|
|
func execute(search: String) async throws -> BookmarksPage {
|
|
BookmarksPage(bookmarks: [], currentPage: 1, totalCount: 0, totalPages: 1, links: nil)
|
|
}
|
|
}
|
|
|
|
class MockReadBookmarkUseCase: PReadBookmarkUseCase {
|
|
func execute(bookmarkDetail: BookmarkDetail) {}
|
|
}
|
|
|
|
class MockGetBookmarksUseCase: PGetBookmarksUseCase {
|
|
func execute(state: BookmarkState?, limit: Int?, offset: Int?, search: String?, type: [BookmarkType]?, tag: String?) async throws -> BookmarksPage {
|
|
BookmarksPage(bookmarks: [
|
|
Bookmark.mock
|
|
], currentPage: 1, totalCount: 0, totalPages: 1, links: nil)
|
|
}
|
|
}
|
|
|
|
class MockUpdateBookmarkUseCase: PUpdateBookmarkUseCase {
|
|
func execute(bookmarkId: String, updateRequest: BookmarkUpdateRequest) async throws {}
|
|
func toggleArchive(bookmarkId: String, isArchived: Bool) async throws {}
|
|
func toggleFavorite(bookmarkId: String, isMarked: Bool) async throws {}
|
|
func markAsDeleted(bookmarkId: String) async throws {}
|
|
func updateReadProgress(bookmarkId: String, progress: Int, anchor: String?) async throws {}
|
|
func updateTitle(bookmarkId: String, title: String) async throws {}
|
|
func updateLabels(bookmarkId: String, labels: [String]) async throws {}
|
|
func addLabels(bookmarkId: String, labels: [String]) async throws {}
|
|
func removeLabels(bookmarkId: String, labels: [String]) async throws {}
|
|
}
|
|
|
|
class MockSaveSettingsUseCase: PSaveSettingsUseCase {
|
|
func execute(endpoint: String, username: String, password: String) async throws {}
|
|
func execute(endpoint: String, username: String, password: String, hasFinishedSetup: Bool) async throws {}
|
|
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 {}
|
|
func execute(urlOpener: UrlOpener) async throws {}
|
|
}
|
|
|
|
class MockGetBookmarkUseCase: PGetBookmarkUseCase {
|
|
func execute(id: String) async throws -> BookmarkDetail {
|
|
BookmarkDetail(id: "123", title: "Test", url: "https://www.google.com", description: "Test", siteName: "Test", authors: ["Test"], created: "2021-01-01", updated: "2021-01-01", wordCount: 100, readingTime: 100, hasArticle: true, isMarked: false, isArchived: false, labels: ["Test"], thumbnailUrl: "https://picsum.photos/30/30", imageUrl: "https://picsum.photos/400/400", lang: "en", readProgress: 0)
|
|
}
|
|
}
|
|
|
|
class MockLoadSettingsUseCase: PLoadSettingsUseCase {
|
|
func execute() async throws -> Settings? {
|
|
Settings(endpoint: "mock-endpoint", username: "mock-user", password: "mock-pw", token: "mock-token", fontFamily: .system, fontSize: .medium, hasFinishedSetup: true)
|
|
}
|
|
}
|
|
|
|
class MockDeleteBookmarkUseCase: PDeleteBookmarkUseCase {
|
|
func execute(bookmarkId: String) async throws {}
|
|
}
|
|
|
|
class MockGetBookmarkArticleUseCase: PGetBookmarkArticleUseCase {
|
|
func execute(id: String) async throws -> String {
|
|
let path = Bundle.main.path(forResource: "article", ofType: "html")
|
|
return try String(contentsOfFile: path!)
|
|
}
|
|
}
|
|
|
|
class MockAddLabelsToBookmarkUseCase: PAddLabelsToBookmarkUseCase {
|
|
func execute(bookmarkId: String, labels: [String]) async throws {}
|
|
func execute(bookmarkId: String, label: String) async throws {}
|
|
}
|
|
|
|
class MockRemoveLabelsFromBookmarkUseCase: PRemoveLabelsFromBookmarkUseCase {
|
|
func execute(bookmarkId: String, labels: [String]) async throws {}
|
|
func execute(bookmarkId: String, label: String) async throws {}
|
|
}
|
|
|
|
class MockSaveServerSettingsUseCase: PSaveServerSettingsUseCase {
|
|
func execute(endpoint: String, username: String, password: String, token: String) async throws {}
|
|
}
|
|
|
|
class MockAddTextToSpeechQueueUseCase: PAddTextToSpeechQueueUseCase {
|
|
func execute(bookmarkDetail: BookmarkDetail) {}
|
|
}
|
|
|
|
class MockOfflineBookmarkSyncUseCase: POfflineBookmarkSyncUseCase {
|
|
var isSyncing: AnyPublisher<Bool, Never> {
|
|
Just(false).eraseToAnyPublisher()
|
|
}
|
|
|
|
var syncStatus: AnyPublisher<String?, Never> {
|
|
Just(nil).eraseToAnyPublisher()
|
|
}
|
|
|
|
func getOfflineBookmarksCount() -> Int {
|
|
return 0
|
|
}
|
|
|
|
func syncOfflineBookmarks() async {
|
|
// Mock implementation - do nothing
|
|
}
|
|
}
|
|
|
|
class MockLoadCardLayoutUseCase: PLoadCardLayoutUseCase {
|
|
func execute() async -> CardLayoutStyle {
|
|
return .magazine
|
|
}
|
|
}
|
|
|
|
class MockSaveCardLayoutUseCase: PSaveCardLayoutUseCase {
|
|
func execute(layout: CardLayoutStyle) async {
|
|
// Mock implementation - do nothing
|
|
}
|
|
}
|
|
|
|
class MockCheckServerReachabilityUseCase: PCheckServerReachabilityUseCase {
|
|
func execute() async -> Bool {
|
|
return true
|
|
}
|
|
|
|
func getServerInfo() async throws -> ServerInfo {
|
|
return ServerInfo(version: "1.0.0", buildDate: nil, userAgent: nil, isReachable: true)
|
|
}
|
|
}
|
|
|
|
class MockGetBookmarkAnnotationsUseCase: PGetBookmarkAnnotationsUseCase {
|
|
func execute(bookmarkId: String) async throws -> [Annotation] {
|
|
return [
|
|
.init(id: "1", text: "bla", created: "", startOffset: 0, endOffset: 1, startSelector: "", endSelector: "")
|
|
]
|
|
}
|
|
}
|
|
|
|
class MockDeleteAnnotationUseCase: PDeleteAnnotationUseCase {
|
|
func execute(bookmarkId: String, annotationId: String) async throws {
|
|
// Mock implementation - do nothing
|
|
}
|
|
}
|
|
|
|
extension Bookmark {
|
|
static let mock: Bookmark = .init(
|
|
id: "123", title: "title", url: "https://example.com", href: "https://example.com", description: "description", authors: ["Tom"], created: "", published: "", updated: "", siteName: "example.com", site: "https://example.com", readingTime: 2, wordCount: 20, hasArticle: true, isArchived: false, isDeleted: false, isMarked: true, labels: ["Test"], lang: "EN", loaded: false, readProgress: 0, documentType: "", state: 0, textDirection: "ltr", type: "", resources: .init(article: nil, icon: nil, image: nil, log: nil, props: nil, thumbnail: nil)
|
|
)
|
|
}
|