mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat: ios widget supports alternate server URLs
This commit is contained in:
parent
1923f1a887
commit
99a9914da2
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -118,10 +118,8 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
||||
}) 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<AuthState> {
|
||||
|
||||
Future<void> saveLocalEndpoint(String url) async {
|
||||
await Store.put(StoreKey.localEndpoint, url);
|
||||
await _widgetService.writeServerList();
|
||||
}
|
||||
|
||||
String? getSavedWifiName() {
|
||||
|
@ -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<void> writeCredentials(String serverURL, String sessionKey) async {
|
||||
Future<void> 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<void> 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<String> _buildServerList() {
|
||||
final List<dynamic> 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<dynamic> 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<String>();
|
||||
}
|
||||
|
||||
Future<void> clearCredentials() async {
|
||||
await _repository.setAppGroupId(appShareGroupId);
|
||||
await _repository.saveData(kWidgetServerEndpoint, "");
|
||||
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user