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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -15,6 +15,7 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
let extensionContext: NSExtensionContext?
|
let extensionContext: NSExtensionContext?
|
||||||
|
|
||||||
private let logger = Logger.viewModel
|
private let logger = Logger.viewModel
|
||||||
|
private let serverCheck = ShareExtensionServerCheck.shared
|
||||||
|
|
||||||
var availableLabels: [BookmarkLabelDto] {
|
var availableLabels: [BookmarkLabelDto] {
|
||||||
return labels.filter { !selectedLabels.contains($0.name) }
|
return labels.filter { !selectedLabels.contains($0.name) }
|
||||||
@ -56,10 +57,15 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
|
|
||||||
private func checkServerReachability() {
|
private func checkServerReachability() {
|
||||||
let measurement = PerformanceMeasurement(operation: "checkServerReachability", logger: logger)
|
let measurement = PerformanceMeasurement(operation: "checkServerReachability", logger: logger)
|
||||||
isServerReachable = ServerConnectivity.isServerReachableSync()
|
Task {
|
||||||
logger.info("Server reachability checked: \(isServerReachable)")
|
let reachable = await serverCheck.checkServerReachability()
|
||||||
|
await MainActor.run {
|
||||||
|
self.isServerReachable = reachable
|
||||||
|
logger.info("Server reachability checked: \(reachable)")
|
||||||
measurement.end()
|
measurement.end()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func extractSharedContent() {
|
private func extractSharedContent() {
|
||||||
logger.debug("Starting to extract shared content")
|
logger.debug("Starting to extract shared content")
|
||||||
@ -131,7 +137,7 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
let measurement = PerformanceMeasurement(operation: "loadLabels", logger: logger)
|
let measurement = PerformanceMeasurement(operation: "loadLabels", logger: logger)
|
||||||
logger.debug("Starting to load labels")
|
logger.debug("Starting to load labels")
|
||||||
Task {
|
Task {
|
||||||
let serverReachable = ServerConnectivity.isServerReachableSync()
|
let serverReachable = await serverCheck.checkServerReachability()
|
||||||
logger.debug("Server reachable for labels: \(serverReachable)")
|
logger.debug("Server reachable for labels: \(serverReachable)")
|
||||||
|
|
||||||
if serverReachable {
|
if serverReachable {
|
||||||
@ -170,12 +176,12 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
logger.debug("Set saving state to true")
|
logger.debug("Set saving state to true")
|
||||||
|
|
||||||
// Check server connectivity
|
// Check server connectivity
|
||||||
let serverReachable = ServerConnectivity.isServerReachableSync()
|
Task {
|
||||||
|
let serverReachable = await serverCheck.checkServerReachability()
|
||||||
logger.debug("Server connectivity for save: \(serverReachable)")
|
logger.debug("Server connectivity for save: \(serverReachable)")
|
||||||
if serverReachable {
|
if serverReachable {
|
||||||
// Online - try to save via API
|
// Online - try to save via API
|
||||||
logger.info("Attempting to save bookmark via API")
|
logger.info("Attempting to save bookmark via API")
|
||||||
Task {
|
|
||||||
await SimpleAPI.addBookmark(title: title, url: url, labels: Array(selectedLabels)) { [weak self] message, error in
|
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?.logger.info("API save completed - Success: \(!error), Message: \(message)")
|
||||||
self?.statusMessage = (message, error, error ? "❌" : "✅")
|
self?.statusMessage = (message, error, error ? "❌" : "✅")
|
||||||
@ -189,7 +195,6 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
self?.logger.error("Failed to save bookmark via API: \(message)")
|
self?.logger.error("Failed to save bookmark via API: \(message)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Server not reachable - save locally
|
// Server not reachable - save locally
|
||||||
logger.info("Server not reachable, attempting local save")
|
logger.info("Server not reachable, attempting local save")
|
||||||
@ -215,6 +220,7 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func completeExtensionRequest() {
|
private func completeExtensionRequest() {
|
||||||
logger.debug("Completing extension request")
|
logger.debug("Completing extension request")
|
||||||
|
|||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,6 +3,39 @@ import Foundation
|
|||||||
class SimpleAPI {
|
class SimpleAPI {
|
||||||
private static let logger = Logger.network
|
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
|
// MARK: - API Methods
|
||||||
static func addBookmark(title: String, url: String, labels: [String]? = nil, showStatus: @escaping (String, Bool) -> Void) async {
|
static func addBookmark(title: String, url: String, labels: [String]? = nil, showStatus: @escaping (String, Bool) -> Void) async {
|
||||||
logger.info("Adding bookmark: \(url)")
|
logger.info("Adding bookmark: \(url)")
|
||||||
|
|||||||
@ -1,5 +1,17 @@
|
|||||||
import Foundation
|
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 struct CreateBookmarkRequestDto: Codable {
|
||||||
public let labels: [String]?
|
public let labels: [String]?
|
||||||
public let title: String?
|
public let title: String?
|
||||||
@ -33,4 +45,3 @@ public struct BookmarkLabelDto: Codable, Identifiable {
|
|||||||
self.href = href
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,16 +10,19 @@ class OfflineSyncManager: ObservableObject, @unchecked Sendable {
|
|||||||
|
|
||||||
private let coreDataManager = CoreDataManager.shared
|
private let coreDataManager = CoreDataManager.shared
|
||||||
private let api: PAPI
|
private let api: PAPI
|
||||||
|
private let checkServerReachabilityUseCase: PCheckServerReachabilityUseCase
|
||||||
|
|
||||||
init(api: PAPI = API()) {
|
init(api: PAPI = API(),
|
||||||
|
checkServerReachabilityUseCase: PCheckServerReachabilityUseCase = DefaultUseCaseFactory.shared.makeCheckServerReachabilityUseCase()) {
|
||||||
self.api = api
|
self.api = api
|
||||||
|
self.checkServerReachabilityUseCase = checkServerReachabilityUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Sync Methods
|
// MARK: - Sync Methods
|
||||||
|
|
||||||
func syncOfflineBookmarks() async {
|
func syncOfflineBookmarks() async {
|
||||||
// First check if server is reachable
|
// First check if server is reachable
|
||||||
guard await ServerConnectivity.isServerReachable() else {
|
guard await checkServerReachabilityUseCase.execute() else {
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
isSyncing = false
|
isSyncing = false
|
||||||
syncStatus = "Server not reachable. Cannot sync."
|
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 {
|
class AppViewModel: ObservableObject {
|
||||||
private let settingsRepository = SettingsRepository()
|
private let settingsRepository = SettingsRepository()
|
||||||
private let logoutUseCase: LogoutUseCase
|
private let logoutUseCase: LogoutUseCase
|
||||||
|
private let checkServerReachabilityUseCase: PCheckServerReachabilityUseCase
|
||||||
|
|
||||||
@Published var hasFinishedSetup: Bool = true
|
@Published var hasFinishedSetup: Bool = true
|
||||||
|
@Published var isServerReachable: Bool = false
|
||||||
|
|
||||||
init(logoutUseCase: LogoutUseCase = LogoutUseCase()) {
|
init(logoutUseCase: LogoutUseCase = LogoutUseCase(),
|
||||||
|
checkServerReachabilityUseCase: PCheckServerReachabilityUseCase = DefaultUseCaseFactory.shared.makeCheckServerReachabilityUseCase()) {
|
||||||
self.logoutUseCase = logoutUseCase
|
self.logoutUseCase = logoutUseCase
|
||||||
|
self.checkServerReachabilityUseCase = checkServerReachabilityUseCase
|
||||||
setupNotificationObservers()
|
setupNotificationObservers()
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
await loadSetupStatus()
|
await loadSetupStatus()
|
||||||
|
await checkServerReachability()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -65,6 +70,11 @@ class AppViewModel: ObservableObject {
|
|||||||
hasFinishedSetup = settingsRepository.hasFinishedSetup
|
hasFinishedSetup = settingsRepository.hasFinishedSetup
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func checkServerReachability() async {
|
||||||
|
isServerReachable = await checkServerReachabilityUseCase.execute()
|
||||||
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
NotificationCenter.default.removeObserver(self)
|
NotificationCenter.default.removeObserver(self)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ protocol UseCaseFactory {
|
|||||||
func makeOfflineBookmarkSyncUseCase() -> POfflineBookmarkSyncUseCase
|
func makeOfflineBookmarkSyncUseCase() -> POfflineBookmarkSyncUseCase
|
||||||
func makeLoadCardLayoutUseCase() -> PLoadCardLayoutUseCase
|
func makeLoadCardLayoutUseCase() -> PLoadCardLayoutUseCase
|
||||||
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase
|
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase
|
||||||
|
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -30,6 +31,8 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
|||||||
private lazy var authRepository: PAuthRepository = AuthRepository(api: api, settingsRepository: settingsRepository)
|
private lazy var authRepository: PAuthRepository = AuthRepository(api: api, settingsRepository: settingsRepository)
|
||||||
private lazy var bookmarksRepository: PBookmarksRepository = BookmarksRepository(api: api)
|
private lazy var bookmarksRepository: PBookmarksRepository = BookmarksRepository(api: api)
|
||||||
private let settingsRepository: PSettingsRepository = SettingsRepository()
|
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()
|
static let shared = DefaultUseCaseFactory()
|
||||||
|
|
||||||
@ -112,4 +115,8 @@ class DefaultUseCaseFactory: UseCaseFactory {
|
|||||||
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase {
|
func makeSaveCardLayoutUseCase() -> PSaveCardLayoutUseCase {
|
||||||
return SaveCardLayoutUseCase(settingsRepository: settingsRepository)
|
return SaveCardLayoutUseCase(settingsRepository: settingsRepository)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func makeCheckServerReachabilityUseCase() -> PCheckServerReachabilityUseCase {
|
||||||
|
return CheckServerReachabilityUseCase(repository: serverInfoRepository)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,8 +11,8 @@ class OfflineBookmarksViewModel {
|
|||||||
private let successDelaySubject = PassthroughSubject<Int, Never>()
|
private let successDelaySubject = PassthroughSubject<Int, Never>()
|
||||||
private var completionTimerActive = false
|
private var completionTimerActive = false
|
||||||
|
|
||||||
init(syncUseCase: POfflineBookmarkSyncUseCase = OfflineBookmarkSyncUseCase()) {
|
init(_ factory: UseCaseFactory = DefaultUseCaseFactory.shared) {
|
||||||
self.syncUseCase = syncUseCase
|
self.syncUseCase = factory.makeOfflineBookmarkSyncUseCase()
|
||||||
setupBindings()
|
setupBindings()
|
||||||
refreshState()
|
refreshState()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ struct PhoneTabView: View {
|
|||||||
private let moreTabs: [SidebarTab] = [.article, .videos, .pictures, .tags, .settings]
|
private let moreTabs: [SidebarTab] = [.article, .videos, .pictures, .tags, .settings]
|
||||||
|
|
||||||
@State private var selectedTab: SidebarTab = .unread
|
@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
|
// Navigation paths for each tab
|
||||||
@State private var allPath = NavigationPath()
|
@State private var allPath = NavigationPath()
|
||||||
@ -149,9 +149,9 @@ struct PhoneTabView: View {
|
|||||||
.padding()
|
.padding()
|
||||||
} else if let bookmarks = searchViewModel.bookmarks?.bookmarks, !bookmarks.isEmpty {
|
} else if let bookmarks = searchViewModel.bookmarks?.bookmarks, !bookmarks.isEmpty {
|
||||||
List(bookmarks) { bookmark in
|
List(bookmarks) { bookmark in
|
||||||
// Hidden NavigationLink to remove disclosure indicator
|
|
||||||
// To restore: uncomment block below and remove ZStack
|
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
||||||
|
// Hidden NavigationLink to remove disclosure indicator
|
||||||
NavigationLink {
|
NavigationLink {
|
||||||
BookmarkDetailView(bookmarkId: bookmark.id)
|
BookmarkDetailView(bookmarkId: bookmark.id)
|
||||||
} label: {
|
} label: {
|
||||||
|
|||||||
@ -29,8 +29,6 @@ struct readeckApp: App {
|
|||||||
#if DEBUG
|
#if DEBUG
|
||||||
NFX.sharedInstance().start()
|
NFX.sharedInstance().start()
|
||||||
#endif
|
#endif
|
||||||
// Initialize server connectivity monitoring
|
|
||||||
_ = ServerConnectivity.shared
|
|
||||||
Task {
|
Task {
|
||||||
await loadAppSettings()
|
await loadAppSettings()
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user