diff --git a/mobile/ios/WidgetExtension/ImmichAPI.swift b/mobile/ios/WidgetExtension/ImmichAPI.swift index 4da610f1c7..e7c514549a 100644 --- a/mobile/ios/WidgetExtension/ImmichAPI.swift +++ b/mobile/ios/WidgetExtension/ImmichAPI.swift @@ -17,6 +17,14 @@ enum AssetType: String, Codable { case other = "OTHER" } +struct ServerWellKnown: Codable { + struct APIInfo: Codable{ + let endpoint: String + } + + let api: APIInfo +} + struct SearchResult: Codable { let id: String let type: AssetType @@ -66,13 +74,50 @@ class ImmichAPI { if serverURL == "" || sessionKey == "" { throw WidgetError.noLogin } - - serverConfig = ServerConfig( - serverEndpoint: serverURL, - sessionKey: sessionKey - ) + + // check if the stored value is an array of URLs + if serverURL.starts(with: "[") { + guard let urls = try? JSONDecoder().decode([String].self, from: serverURL.data(using: .utf8)!) else { + throw WidgetError.noLogin + } + + for url in urls { + guard let endpointURL = URL(string: url) else { continue } + + if let apiURL = await Self.validateServer(at: endpointURL) { + serverConfig = ServerConfig( + serverEndpoint: apiURL.absoluteString, + sessionKey: sessionKey + ) + return + } + } + + throw WidgetError.fetchFailed + } else { + serverConfig = ServerConfig( + serverEndpoint: serverURL, + sessionKey: sessionKey + ) + } } + private static func validateServer(at endpointURL: URL) async -> URL? { + let baseURL = URL(string: endpointURL.scheme! + "://" + endpointURL.host!)! + + var pingURL = baseURL + pingURL.appendPathComponent(".well-known") + pingURL.appendPathComponent("immich") + + guard let (serverPingJSON, _) = try? await URLSession.shared.data(from: pingURL) else { return nil } + guard let apiInfo = try? JSONDecoder().decode(ServerWellKnown.self, from: serverPingJSON) else { return nil } + + var apiURL = baseURL + apiURL.appendPathComponent(apiInfo.api.endpoint) + + return apiURL + } + private func buildRequestURL( serverConfig: ServerConfig, endpoint: String, diff --git a/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift b/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift index 516bf6905e..82dc400b37 100644 --- a/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift +++ b/mobile/ios/WidgetExtension/widgets/MemoryWidget.swift @@ -20,8 +20,11 @@ struct ImmichMemoryProvider: TimelineProvider { completion: @escaping @Sendable (ImageEntry) -> Void ) { Task { - guard let api = try? await ImmichAPI() else { - completion(ImageEntry(date: Date(), image: nil, error: .noLogin)) + var api: ImmichAPI + do { + api = try await ImmichAPI() + } catch let error as WidgetError { + completion(ImageEntry(date: Date(), image: nil, error: error)) return } @@ -79,9 +82,13 @@ struct ImmichMemoryProvider: TimelineProvider { Task { var entries: [ImageEntry] = [] let now = Date() - - guard let api = try? await ImmichAPI() else { - entries.append(ImageEntry(date: now, image: nil, error: .noLogin)) + + + var api: ImmichAPI + do { + api = try await ImmichAPI() + } catch let error as WidgetError { + entries.append(ImageEntry(date: now, image: nil, error: error)) completion(Timeline(entries: entries, policy: .atEnd)) return } diff --git a/mobile/ios/WidgetExtension/widgets/RandomWidget.swift b/mobile/ios/WidgetExtension/widgets/RandomWidget.swift index 99968c4baa..515d91b879 100644 --- a/mobile/ios/WidgetExtension/widgets/RandomWidget.swift +++ b/mobile/ios/WidgetExtension/widgets/RandomWidget.swift @@ -63,10 +63,15 @@ struct ImmichRandomProvider: AppIntentTimelineProvider { ) async -> ImageEntry { - guard let api = try? await ImmichAPI() else { - return ImageEntry(date: Date(), image: nil, error: .noLogin) + var api: ImmichAPI + do { + api = try await ImmichAPI() + } catch let error as WidgetError { + return ImageEntry(date: Date(), image: nil, error: error) + } catch { + return ImageEntry(date: Date(), image: nil, error: .fetchFailed) } - + guard let randomImage = try? await api.fetchSearchResults( with: SearchFilters(size: 1) @@ -100,15 +105,21 @@ struct ImmichRandomProvider: AppIntentTimelineProvider { let now = Date() // If we don't have a server config, return an entry with an error - guard let api = try? await ImmichAPI() else { - entries.append(ImageEntry(date: now, image: nil, error: .noLogin)) + var api: ImmichAPI + do { + api = try await ImmichAPI() + } catch let error as WidgetError { + entries.append(ImageEntry(date: now, image: nil, error: error)) + return Timeline(entries: entries, policy: .atEnd) + } catch { + entries.append(ImageEntry(date: now, image: nil, error: .fetchFailed)) return Timeline(entries: entries, policy: .atEnd) } // nil if album is NONE or nil let albumId = configuration.album?.id != "NONE" ? configuration.album?.id : nil - var albumName: String? = albumId != nil ? configuration.album?.albumName : nil + let albumName: String? = albumId != nil ? configuration.album?.albumName : nil if albumId != nil { // make sure the album exists on server, otherwise show error diff --git a/mobile/lib/providers/auth.provider.dart b/mobile/lib/providers/auth.provider.dart index dfbd18953a..56114e8627 100644 --- a/mobile/lib/providers/auth.provider.dart +++ b/mobile/lib/providers/auth.provider.dart @@ -118,10 +118,8 @@ class AuthNotifier extends StateNotifier { }) async { await _apiService.setAccessToken(accessToken); - await _widgetService.writeCredentials( - Store.get(StoreKey.serverEndpoint), - accessToken, - ); + await _widgetService.writeSessionKey(accessToken); + await _widgetService.writeServerList(); // Get the deviceid from the store if it exists, otherwise generate a new one String deviceId = @@ -190,6 +188,7 @@ class AuthNotifier extends StateNotifier { Future saveLocalEndpoint(String url) async { await Store.put(StoreKey.localEndpoint, url); + await _widgetService.writeServerList(); } String? getSavedWifiName() { diff --git a/mobile/lib/services/widget.service.dart b/mobile/lib/services/widget.service.dart index e71733f4b9..b3943b839e 100644 --- a/mobile/lib/services/widget.service.dart +++ b/mobile/lib/services/widget.service.dart @@ -1,5 +1,10 @@ +import 'dart:convert'; + import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/constants.dart'; +import 'package:immich_mobile/domain/models/store.model.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; import 'package:immich_mobile/repositories/widget.repository.dart'; final widgetServiceProvider = Provider((ref) { @@ -13,15 +18,48 @@ class WidgetService { WidgetService(this._repository); - Future writeCredentials(String serverURL, String sessionKey) async { + Future writeSessionKey( + String sessionKey, + ) async { await _repository.setAppGroupId(appShareGroupId); - await _repository.saveData(kWidgetServerEndpoint, serverURL); await _repository.saveData(kWidgetAuthToken, sessionKey); // wait 3 seconds to ensure the widget is updated, dont block Future.delayed(const Duration(seconds: 3), refreshWidgets); } + Future writeServerList() async { + await _repository.setAppGroupId(appShareGroupId); + + // create json string from serverURLS + final serverURLSString = jsonEncode(_buildServerList()); + + await _repository.saveData(kWidgetServerEndpoint, serverURLSString); + Future.delayed(const Duration(seconds: 3), refreshWidgets); + } + + List _buildServerList() { + final List jsonList = + jsonDecode(Store.tryGet(StoreKey.externalEndpointList) ?? "[]"); + final endpointList = + jsonList.map((e) => AuxilaryEndpoint.fromJson(e)).toList(); + + final String? localEndpoint = Store.tryGet(StoreKey.localEndpoint); + final String? serverUrl = Store.tryGet(StoreKey.serverUrl); + + final List serverUrlList = endpointList.map((e) => e.url).toList(); + + if (localEndpoint != null) { + serverUrlList.insert(0, localEndpoint); + } + + if (serverUrl != null && serverUrl != localEndpoint) { + serverUrlList.insert(0, serverUrl); + } + + return serverUrlList.cast(); + } + Future clearCredentials() async { await _repository.setAppGroupId(appShareGroupId); await _repository.saveData(kWidgetServerEndpoint, ""); diff --git a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart index 633d84c9c8..904095f9f5 100644 --- a/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart +++ b/mobile/lib/widgets/settings/networking_settings/external_network_preference.dart @@ -8,6 +8,7 @@ import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/auth/auxilary_endpoint.model.dart'; +import 'package:immich_mobile/services/widget.service.dart'; import 'package:immich_mobile/widgets/settings/networking_settings/endpoint_input.dart'; class ExternalNetworkPreference extends HookConsumerWidget { @@ -35,6 +36,8 @@ class ExternalNetworkPreference extends HookConsumerWidget { StoreKey.externalEndpointList, jsonString, ); + + ref.read(widgetServiceProvider).writeServerList(); } updateValidationStatus(String url, int index, AuxCheckStatus status) {