refactor: Optimize server connectivity with Clean Architecture
- Replace ServerConnectivity with CheckServerReachabilityUseCase - Add InfoApiClient for /api/info endpoint - Implement ServerInfoRepository with 30s cache TTL and 5s rate limiting - Update ShareBookmarkViewModel to use ShareExtensionServerCheck manager - Add server reachability check in AppViewModel on app start - Update OfflineSyncManager to use new UseCase - Extend SimpleAPI with checkServerReachability for Share Extension
This commit is contained in:
parent
eddc8a35ff
commit
ef8ebd6f00
@ -1,62 +0,0 @@
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
class ServerConnectivity: ObservableObject {
|
||||
@Published var isServerReachable = false
|
||||
|
||||
static let shared = ServerConnectivity()
|
||||
|
||||
private init() {}
|
||||
|
||||
// Check if the Readeck server endpoint is reachable
|
||||
static func isServerReachable() async -> Bool {
|
||||
guard let endpoint = KeychainHelper.shared.loadEndpoint(),
|
||||
!endpoint.isEmpty,
|
||||
let url = URL(string: endpoint + "/api/health") else {
|
||||
return false
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.timeoutInterval = 5.0 // 5 second timeout
|
||||
|
||||
do {
|
||||
let (_, response) = try await URLSession.shared.data(for: request)
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
return httpResponse.statusCode == 200
|
||||
}
|
||||
} catch {
|
||||
print("Server connectivity check failed: \(error)")
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Alternative check using ping-style endpoint
|
||||
static func isServerReachableSync() -> Bool {
|
||||
guard let endpoint = KeychainHelper.shared.loadEndpoint(),
|
||||
!endpoint.isEmpty,
|
||||
let url = URL(string: endpoint) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
var isReachable = false
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "HEAD" // Just check if server responds
|
||||
request.timeoutInterval = 3.0
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) { _, response, error in
|
||||
if let httpResponse = response as? HTTPURLResponse {
|
||||
isReachable = httpResponse.statusCode < 500 // Accept any response that's not server error
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
|
||||
task.resume()
|
||||
_ = semaphore.wait(timeout: .now() + 3.0)
|
||||
|
||||
return isReachable
|
||||
}
|
||||
}
|
||||
@ -13,8 +13,9 @@ class ShareBookmarkViewModel: ObservableObject {
|
||||
@Published var searchText: String = ""
|
||||
@Published var isServerReachable: Bool = true
|
||||
let extensionContext: NSExtensionContext?
|
||||
|
||||
|
||||
private let logger = Logger.viewModel
|
||||
private let serverCheck = ShareExtensionServerCheck.shared
|
||||
|
||||
var availableLabels: [BookmarkLabelDto] {
|
||||
return labels.filter { !selectedLabels.contains($0.name) }
|
||||
@ -56,9 +57,14 @@ class ShareBookmarkViewModel: ObservableObject {
|
||||
|
||||
private func checkServerReachability() {
|
||||
let measurement = PerformanceMeasurement(operation: "checkServerReachability", logger: logger)
|
||||
isServerReachable = ServerConnectivity.isServerReachableSync()
|
||||
logger.info("Server reachability checked: \(isServerReachable)")
|
||||
measurement.end()
|
||||
Task {
|
||||
let reachable = await serverCheck.checkServerReachability()
|
||||
await MainActor.run {
|
||||
self.isServerReachable = reachable
|
||||
logger.info("Server reachability checked: \(reachable)")
|
||||
measurement.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func extractSharedContent() {
|
||||
@ -131,9 +137,9 @@ class ShareBookmarkViewModel: ObservableObject {
|
||||
let measurement = PerformanceMeasurement(operation: "loadLabels", logger: logger)
|
||||
logger.debug("Starting to load labels")
|
||||
Task {
|
||||
let serverReachable = ServerConnectivity.isServerReachableSync()
|
||||
let serverReachable = await serverCheck.checkServerReachability()
|
||||
logger.debug("Server reachable for labels: \(serverReachable)")
|
||||
|
||||
|
||||
if serverReachable {
|
||||
let loaded = await SimpleAPI.getBookmarkLabels { [weak self] message, error in
|
||||
self?.statusMessage = (message, error, error ? "❌" : "✅")
|
||||
@ -168,14 +174,14 @@ class ShareBookmarkViewModel: ObservableObject {
|
||||
}
|
||||
isSaving = true
|
||||
logger.debug("Set saving state to true")
|
||||
|
||||
|
||||
// Check server connectivity
|
||||
let serverReachable = ServerConnectivity.isServerReachableSync()
|
||||
logger.debug("Server connectivity for save: \(serverReachable)")
|
||||
if serverReachable {
|
||||
// Online - try to save via API
|
||||
logger.info("Attempting to save bookmark via API")
|
||||
Task {
|
||||
Task {
|
||||
let serverReachable = await serverCheck.checkServerReachability()
|
||||
logger.debug("Server connectivity for save: \(serverReachable)")
|
||||
if serverReachable {
|
||||
// Online - try to save via API
|
||||
logger.info("Attempting to save bookmark via API")
|
||||
await SimpleAPI.addBookmark(title: title, url: url, labels: Array(selectedLabels)) { [weak self] message, error in
|
||||
self?.logger.info("API save completed - Success: \(!error), Message: \(message)")
|
||||
self?.statusMessage = (message, error, error ? "❌" : "✅")
|
||||
@ -189,28 +195,28 @@ class ShareBookmarkViewModel: ObservableObject {
|
||||
self?.logger.error("Failed to save bookmark via API: \(message)")
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Server not reachable - save locally
|
||||
logger.info("Server not reachable, attempting local save")
|
||||
let success = OfflineBookmarkManager.shared.saveOfflineBookmark(
|
||||
url: url,
|
||||
title: title,
|
||||
tags: Array(selectedLabels)
|
||||
)
|
||||
logger.info("Local save result: \(success)")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isSaving = false
|
||||
if success {
|
||||
self.logger.info("Bookmark saved locally successfully")
|
||||
self.statusMessage = ("Server not reachable. Saved locally and will sync later.", false, "🏠")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
self.completeExtensionRequest()
|
||||
} else {
|
||||
// Server not reachable - save locally
|
||||
logger.info("Server not reachable, attempting local save")
|
||||
let success = OfflineBookmarkManager.shared.saveOfflineBookmark(
|
||||
url: url,
|
||||
title: title,
|
||||
tags: Array(selectedLabels)
|
||||
)
|
||||
logger.info("Local save result: \(success)")
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.isSaving = false
|
||||
if success {
|
||||
self.logger.info("Bookmark saved locally successfully")
|
||||
self.statusMessage = ("Server not reachable. Saved locally and will sync later.", false, "🏠")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
|
||||
self.completeExtensionRequest()
|
||||
}
|
||||
} else {
|
||||
self.logger.error("Failed to save bookmark locally")
|
||||
self.statusMessage = ("Failed to save locally.", true, "❌")
|
||||
}
|
||||
} else {
|
||||
self.logger.error("Failed to save bookmark locally")
|
||||
self.statusMessage = ("Failed to save locally.", true, "❌")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
41
URLShare/ShareExtensionServerCheck.swift
Normal file
41
URLShare/ShareExtensionServerCheck.swift
Normal file
@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
|
||||
/// Simple server check manager for Share Extension with caching
|
||||
class ShareExtensionServerCheck {
|
||||
static let shared = ShareExtensionServerCheck()
|
||||
|
||||
// Cache properties
|
||||
private var cachedResult: Bool?
|
||||
private var lastCheckTime: Date?
|
||||
private let cacheTTL: TimeInterval = 30.0
|
||||
|
||||
private init() {}
|
||||
|
||||
func checkServerReachability() async -> Bool {
|
||||
// Check cache first
|
||||
if let cached = getCachedResult() {
|
||||
return cached
|
||||
}
|
||||
|
||||
// Use SimpleAPI for actual check
|
||||
let result = await SimpleAPI.checkServerReachability()
|
||||
updateCache(result: result)
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: - Cache Management
|
||||
|
||||
private func getCachedResult() -> Bool? {
|
||||
guard let lastCheck = lastCheckTime,
|
||||
Date().timeIntervalSince(lastCheck) < cacheTTL,
|
||||
let cached = cachedResult else {
|
||||
return nil
|
||||
}
|
||||
return cached
|
||||
}
|
||||
|
||||
private func updateCache(result: Bool) {
|
||||
cachedResult = result
|
||||
lastCheckTime = Date()
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,40 @@ import Foundation
|
||||
|
||||
class SimpleAPI {
|
||||
private static let logger = Logger.network
|
||||
|
||||
|
||||
// MARK: - Server Info
|
||||
|
||||
static func checkServerReachability() async -> Bool {
|
||||
guard let endpoint = KeychainHelper.shared.loadEndpoint(),
|
||||
!endpoint.isEmpty,
|
||||
let url = URL(string: "\(endpoint)/api/info") else {
|
||||
return false
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "accept")
|
||||
request.timeoutInterval = 5.0
|
||||
|
||||
if let token = KeychainHelper.shared.loadToken() {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "authorization")
|
||||
}
|
||||
|
||||
do {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
if let httpResponse = response as? HTTPURLResponse,
|
||||
200...299 ~= httpResponse.statusCode {
|
||||
logger.info("Server is reachable")
|
||||
return true
|
||||
}
|
||||
} catch {
|
||||
logger.error("Server reachability check failed: \(error.localizedDescription)")
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// MARK: - API Methods
|
||||
static func addBookmark(title: String, url: String, labels: [String]? = nil, showStatus: @escaping (String, Bool) -> Void) async {
|
||||
logger.info("Adding bookmark: \(url)")
|
||||
|
||||
@ -1,5 +1,17 @@
|
||||
import Foundation
|
||||
|
||||
public struct ServerInfoDto: Codable {
|
||||
public let version: String
|
||||
public let buildDate: String?
|
||||
public let userAgent: String?
|
||||
|
||||
public enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case buildDate = "build_date"
|
||||
case userAgent = "user_agent"
|
||||
}
|
||||
}
|
||||
|
||||
public struct CreateBookmarkRequestDto: Codable {
|
||||
public let labels: [String]?
|
||||
public let title: String?
|
||||
@ -33,4 +45,3 @@ public struct BookmarkLabelDto: Codable, Identifiable {
|
||||
self.href = href
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
13
readeck/Data/API/DTOs/ServerInfoDto.swift
Normal file
13
readeck/Data/API/DTOs/ServerInfoDto.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
struct ServerInfoDto: Codable {
|
||||
let version: String
|
||||
let buildDate: String?
|
||||
let userAgent: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case buildDate = "build_date"
|
||||
case userAgent = "user_agent"
|
||||
}
|
||||
}
|
||||
55
readeck/Data/API/InfoApiClient.swift
Normal file
55
readeck/Data/API/InfoApiClient.swift
Normal file
@ -0,0 +1,55 @@
|
||||
//
|
||||
// InfoApiClient.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Claude Code
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol PInfoApiClient {
|
||||
func getServerInfo() async throws -> ServerInfoDto
|
||||
}
|
||||
|
||||
class InfoApiClient: PInfoApiClient {
|
||||
private let tokenProvider: TokenProvider
|
||||
private let logger = Logger.network
|
||||
|
||||
init(tokenProvider: TokenProvider = KeychainTokenProvider()) {
|
||||
self.tokenProvider = tokenProvider
|
||||
}
|
||||
|
||||
func getServerInfo() async throws -> ServerInfoDto {
|
||||
guard let endpoint = await tokenProvider.getEndpoint(),
|
||||
let url = URL(string: "\(endpoint)/api/info") else {
|
||||
logger.error("Invalid endpoint URL for server info")
|
||||
throw APIError.invalidURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: url)
|
||||
request.httpMethod = "GET"
|
||||
request.setValue("application/json", forHTTPHeaderField: "accept")
|
||||
request.timeoutInterval = 5.0
|
||||
|
||||
if let token = await tokenProvider.getToken() {
|
||||
request.setValue("Bearer \(token)", forHTTPHeaderField: "authorization")
|
||||
}
|
||||
|
||||
logger.logNetworkRequest(method: "GET", url: url.absoluteString)
|
||||
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
|
||||
guard let httpResponse = response as? HTTPURLResponse else {
|
||||
logger.error("Invalid HTTP response for server info")
|
||||
throw APIError.invalidResponse
|
||||
}
|
||||
|
||||
guard 200...299 ~= httpResponse.statusCode else {
|
||||
logger.logNetworkError(method: "GET", url: url.absoluteString, error: APIError.serverError(httpResponse.statusCode))
|
||||
throw APIError.serverError(httpResponse.statusCode)
|
||||
}
|
||||
|
||||
logger.logNetworkRequest(method: "GET", url: url.absoluteString, statusCode: httpResponse.statusCode)
|
||||
|
||||
return try JSONDecoder().decode(ServerInfoDto.self, from: data)
|
||||
}
|
||||
}
|
||||
@ -4,22 +4,25 @@ import SwiftUI
|
||||
|
||||
class OfflineSyncManager: ObservableObject, @unchecked Sendable {
|
||||
static let shared = OfflineSyncManager()
|
||||
|
||||
|
||||
@Published var isSyncing = false
|
||||
@Published var syncStatus: String?
|
||||
|
||||
|
||||
private let coreDataManager = CoreDataManager.shared
|
||||
private let api: PAPI
|
||||
|
||||
init(api: PAPI = API()) {
|
||||
private let checkServerReachabilityUseCase: PCheckServerReachabilityUseCase
|
||||
|
||||
init(api: PAPI = API(),
|
||||
checkServerReachabilityUseCase: PCheckServerReachabilityUseCase = DefaultUseCaseFactory.shared.makeCheckServerReachabilityUseCase()) {
|
||||
self.api = api
|
||||
self.checkServerReachabilityUseCase = checkServerReachabilityUseCase
|
||||
}
|
||||
|
||||
// MARK: - Sync Methods
|
||||
|
||||
func syncOfflineBookmarks() async {
|
||||
// First check if server is reachable
|
||||
guard await ServerConnectivity.isServerReachable() else {
|
||||
guard await checkServerReachabilityUseCase.execute() else {
|
||||
await MainActor.run {
|
||||
isSyncing = false
|
||||
syncStatus = "Server not reachable. Cannot sync."
|
||||
|
||||
114
readeck/Data/Repository/ServerInfoRepository.swift
Normal file
114
readeck/Data/Repository/ServerInfoRepository.swift
Normal file
@ -0,0 +1,114 @@
|
||||
//
|
||||
// ServerInfoRepository.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Claude Code
|
||||
|
||||
import Foundation
|
||||
|
||||
class ServerInfoRepository: PServerInfoRepository {
|
||||
private let apiClient: PInfoApiClient
|
||||
private let logger = Logger.network
|
||||
|
||||
// Cache properties
|
||||
private var cachedServerInfo: ServerInfo?
|
||||
private var lastCheckTime: Date?
|
||||
private let cacheTTL: TimeInterval = 30.0 // 30 seconds cache
|
||||
private let rateLimitInterval: TimeInterval = 5.0 // min 5 seconds between requests
|
||||
|
||||
// Thread safety
|
||||
private let queue = DispatchQueue(label: "com.readeck.serverInfoRepository", attributes: .concurrent)
|
||||
|
||||
init(apiClient: PInfoApiClient) {
|
||||
self.apiClient = apiClient
|
||||
}
|
||||
|
||||
func checkServerReachability() async -> Bool {
|
||||
// Check cache first
|
||||
if let cached = getCachedReachability() {
|
||||
logger.debug("Server reachability from cache: \(cached)")
|
||||
return cached
|
||||
}
|
||||
|
||||
// Check rate limiting
|
||||
if isRateLimited() {
|
||||
logger.debug("Server reachability check rate limited, using cached value")
|
||||
return cachedServerInfo?.isReachable ?? false
|
||||
}
|
||||
|
||||
// Perform actual check
|
||||
do {
|
||||
let info = try await apiClient.getServerInfo()
|
||||
let serverInfo = ServerInfo(from: info)
|
||||
updateCache(serverInfo: serverInfo)
|
||||
logger.info("Server reachability checked: true (version: \(info.version))")
|
||||
return true
|
||||
} catch {
|
||||
let unreachableInfo = ServerInfo.unreachable
|
||||
updateCache(serverInfo: unreachableInfo)
|
||||
logger.warning("Server reachability check failed: \(error.localizedDescription)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func getServerInfo() async throws -> ServerInfo {
|
||||
// Check cache first
|
||||
if let cached = getCachedServerInfo() {
|
||||
logger.debug("Server info from cache")
|
||||
return cached
|
||||
}
|
||||
|
||||
// Check rate limiting
|
||||
if isRateLimited(), let cached = cachedServerInfo {
|
||||
logger.debug("Server info check rate limited, using cached value")
|
||||
return cached
|
||||
}
|
||||
|
||||
// Fetch fresh info
|
||||
let dto = try await apiClient.getServerInfo()
|
||||
let serverInfo = ServerInfo(from: dto)
|
||||
updateCache(serverInfo: serverInfo)
|
||||
logger.info("Server info fetched: version \(dto.version)")
|
||||
return serverInfo
|
||||
}
|
||||
|
||||
// MARK: - Cache Management
|
||||
|
||||
private func getCachedReachability() -> Bool? {
|
||||
queue.sync {
|
||||
guard let lastCheck = lastCheckTime,
|
||||
Date().timeIntervalSince(lastCheck) < cacheTTL,
|
||||
let cached = cachedServerInfo else {
|
||||
return nil
|
||||
}
|
||||
return cached.isReachable
|
||||
}
|
||||
}
|
||||
|
||||
private func getCachedServerInfo() -> ServerInfo? {
|
||||
queue.sync {
|
||||
guard let lastCheck = lastCheckTime,
|
||||
Date().timeIntervalSince(lastCheck) < cacheTTL,
|
||||
let cached = cachedServerInfo else {
|
||||
return nil
|
||||
}
|
||||
return cached
|
||||
}
|
||||
}
|
||||
|
||||
private func isRateLimited() -> Bool {
|
||||
queue.sync {
|
||||
guard let lastCheck = lastCheckTime else {
|
||||
return false
|
||||
}
|
||||
return Date().timeIntervalSince(lastCheck) < rateLimitInterval
|
||||
}
|
||||
}
|
||||
|
||||
private func updateCache(serverInfo: ServerInfo) {
|
||||
queue.async(flags: .barrier) { [weak self] in
|
||||
self?.cachedServerInfo = serverInfo
|
||||
self?.lastCheckTime = Date()
|
||||
}
|
||||
}
|
||||
}
|
||||
21
readeck/Domain/Model/ServerInfo.swift
Normal file
21
readeck/Domain/Model/ServerInfo.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
|
||||
struct ServerInfo {
|
||||
let version: String
|
||||
let buildDate: String?
|
||||
let userAgent: String?
|
||||
let isReachable: Bool
|
||||
}
|
||||
|
||||
extension ServerInfo {
|
||||
init(from dto: ServerInfoDto) {
|
||||
self.version = dto.version
|
||||
self.buildDate = dto.buildDate
|
||||
self.userAgent = dto.userAgent
|
||||
self.isReachable = true
|
||||
}
|
||||
|
||||
static var unreachable: ServerInfo {
|
||||
ServerInfo(version: "", buildDate: nil, userAgent: nil, isReachable: false)
|
||||
}
|
||||
}
|
||||
10
readeck/Domain/Protocols/PServerInfoRepository.swift
Normal file
10
readeck/Domain/Protocols/PServerInfoRepository.swift
Normal file
@ -0,0 +1,10 @@
|
||||
//
|
||||
// PServerInfoRepository.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Claude Code
|
||||
|
||||
protocol PServerInfoRepository {
|
||||
func checkServerReachability() async -> Bool
|
||||
func getServerInfo() async throws -> ServerInfo
|
||||
}
|
||||
28
readeck/Domain/UseCase/CheckServerReachabilityUseCase.swift
Normal file
28
readeck/Domain/UseCase/CheckServerReachabilityUseCase.swift
Normal file
@ -0,0 +1,28 @@
|
||||
//
|
||||
// CheckServerReachabilityUseCase.swift
|
||||
// readeck
|
||||
//
|
||||
// Created by Claude Code
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol PCheckServerReachabilityUseCase {
|
||||
func execute() async -> Bool
|
||||
func getServerInfo() async throws -> ServerInfo
|
||||
}
|
||||
|
||||
class CheckServerReachabilityUseCase: PCheckServerReachabilityUseCase {
|
||||
private let repository: PServerInfoRepository
|
||||
|
||||
init(repository: PServerInfoRepository) {
|
||||
self.repository = repository
|
||||
}
|
||||
|
||||
func execute() async -> Bool {
|
||||
return await repository.checkServerReachability()
|
||||
}
|
||||
|
||||
func getServerInfo() async throws -> ServerInfo {
|
||||
return try await repository.getServerInfo()
|
||||
}
|
||||
}
|
||||
@ -11,15 +11,20 @@ import SwiftUI
|
||||
class AppViewModel: ObservableObject {
|
||||
private let settingsRepository = SettingsRepository()
|
||||
private let logoutUseCase: LogoutUseCase
|
||||
|
||||
private let checkServerReachabilityUseCase: PCheckServerReachabilityUseCase
|
||||
|
||||
@Published var hasFinishedSetup: Bool = true
|
||||
|
||||
init(logoutUseCase: LogoutUseCase = LogoutUseCase()) {
|
||||
@Published var isServerReachable: Bool = false
|
||||
|
||||
init(logoutUseCase: LogoutUseCase = LogoutUseCase(),
|
||||
checkServerReachabilityUseCase: PCheckServerReachabilityUseCase = DefaultUseCaseFactory.shared.makeCheckServerReachabilityUseCase()) {
|
||||
self.logoutUseCase = logoutUseCase
|
||||
self.checkServerReachabilityUseCase = checkServerReachabilityUseCase
|
||||
setupNotificationObservers()
|
||||
|
||||
|
||||
Task {
|
||||
await loadSetupStatus()
|
||||
await checkServerReachability()
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +69,12 @@ class AppViewModel: ObservableObject {
|
||||
private func loadSetupStatus() {
|
||||
hasFinishedSetup = settingsRepository.hasFinishedSetup
|
||||
}
|
||||
|
||||
|
||||
@MainActor
|
||||
private func checkServerReachability() async {
|
||||
isServerReachable = await checkServerReachabilityUseCase.execute()
|
||||
}
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ protocol UseCaseFactory {
|
||||
func makeOfflineBookmarkSyncUseCase() -> POfflineBookmarkSyncUseCase
|
||||
func makeLoadCardLayoutUseCase() -> PLoadCardLayoutUseCase
|
||||
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase
|
||||
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase
|
||||
}
|
||||
|
||||
|
||||
@ -30,9 +31,11 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
||||
private lazy var authRepository: PAuthRepository = AuthRepository(api: api, settingsRepository: settingsRepository)
|
||||
private lazy var bookmarksRepository: PBookmarksRepository = BookmarksRepository(api: api)
|
||||
private let settingsRepository: PSettingsRepository = SettingsRepository()
|
||||
|
||||
private lazy var infoApiClient: PInfoApiClient = InfoApiClient(tokenProvider: tokenProvider)
|
||||
private lazy var serverInfoRepository: PServerInfoRepository = ServerInfoRepository(apiClient: infoApiClient)
|
||||
|
||||
static let shared = DefaultUseCaseFactory()
|
||||
|
||||
|
||||
private init() {}
|
||||
|
||||
func makeLoginUseCase() -> PLoginUseCase {
|
||||
@ -112,4 +115,8 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
||||
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase {
|
||||
return SaveCardLayoutUseCase(settingsRepository: settingsRepository)
|
||||
}
|
||||
|
||||
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase {
|
||||
return CheckServerReachabilityUseCase(repository: serverInfoRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,8 +11,8 @@ class OfflineBookmarksViewModel {
|
||||
private let successDelaySubject = PassthroughSubject<Int, Never>()
|
||||
private var completionTimerActive = false
|
||||
|
||||
init(syncUseCase: POfflineBookmarkSyncUseCase = OfflineBookmarkSyncUseCase()) {
|
||||
self.syncUseCase = syncUseCase
|
||||
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
|
||||
self.syncUseCase = factory.makeOfflineBookmarkSyncUseCase()
|
||||
setupBindings()
|
||||
refreshState()
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ struct PhoneTabView: View {
|
||||
private let moreTabs: [SidebarTab] = [.article, .videos, .pictures, .tags, .settings]
|
||||
|
||||
@State private var selectedTab: SidebarTab = .unread
|
||||
@State private var offlineBookmarksViewModel = OfflineBookmarksViewModel(syncUseCase: DefaultUseCaseFactory.shared.makeOfflineBookmarkSyncUseCase())
|
||||
@State private var offlineBookmarksViewModel = OfflineBookmarksViewModel()
|
||||
|
||||
// Navigation paths for each tab
|
||||
@State private var allPath = NavigationPath()
|
||||
@ -149,9 +149,9 @@ struct PhoneTabView: View {
|
||||
.padding()
|
||||
} else if let bookmarks = searchViewModel.bookmarks?.bookmarks, !bookmarks.isEmpty {
|
||||
List(bookmarks) { bookmark in
|
||||
// Hidden NavigationLink to remove disclosure indicator
|
||||
// To restore: uncomment block below and remove ZStack
|
||||
ZStack {
|
||||
|
||||
// Hidden NavigationLink to remove disclosure indicator
|
||||
NavigationLink {
|
||||
BookmarkDetailView(bookmarkId: bookmark.id)
|
||||
} label: {
|
||||
|
||||
@ -29,8 +29,6 @@ struct readeckApp: App {
|
||||
#if DEBUG
|
||||
NFX.sharedInstance().start()
|
||||
#endif
|
||||
// Initialize server connectivity monitoring
|
||||
_ = ServerConnectivity.shared
|
||||
Task {
|
||||
await loadAppSettings()
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user