feat(mobile): ios widgets (#19148)

* feat: working widgets

* chore/feat: cleaned up API, added album picker to random widget

* album filtering for requests

* check album and throw if not found

* fix app IDs and project configuration

* switch to repository/service model for updating widgets

* fix: remove home widget import

* revert info.plist formatting changes

* ran swift-format on widget code

* more formatting changes (this time run from xcode)

* show memory on widget picker snapshot

* fix: dart changes from code review

* fix: swift code review changes (not including task groups)

* fix: use task groups to run image retrievals concurrently, get rid of do catch in favor of if let

* chore: cleanup widget service in dart app

* chore: format swift

* fix: remove comma

why does xcode not freak out over this >:(

* switch to preview size for thumbnail

* chore: cropped large image

* fix: properly resize widgets so we dont OOM

* fix: set app group on logout

happens on first install

* fix: stupid app ids

* fix: revert back to thumbnail

we are hitting OOM exceptions due to resizing, once we have on-the-fly resizing on server this can be upgraded

* fix: more memory efficient resizing method, remove extraneous resize commands from API call

* fix: random widget use 12 entries instead of 24 to save memory

* fix: modify duration of entries to 20 minutes and only generate 10 at a time to avoid OOM

* feat: toggle to show album name on random widget

* Podfile lock

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
Brandon Wees
2025-06-17 09:43:09 -05:00
committed by GitHub
parent 15c488ccd9
commit a0f44f147b
19 changed files with 1092 additions and 1 deletions
+12
View File
@@ -20,3 +20,15 @@ const String kSecuredPinCode = "secured_pin_code";
const int kTimelineNoneSegmentSize = 120;
const int kTimelineAssetLoadBatchSize = 256;
const int kTimelineAssetLoadOppositeSize = 64;
// Widget keys
const String kWidgetAuthToken = "widget_auth_token";
const String kWidgetServerEndpoint = "widget_server_url";
const String appShareGroupId = "group.app.immich.share";
// add widget identifiers here for new widgets
// these are used to force a widget refresh
const List<String> kWidgetNames = [
'com.immich.widget.random',
'com.immich.widget.memory',
];
@@ -0,0 +1,5 @@
abstract interface class IWidgetRepository {
Future<void> saveData(String key, String value);
Future<void> refresh(String name);
Future<void> setAppGroupId(String appGroupId);
}
+11
View File
@@ -13,6 +13,7 @@ import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
import 'package:immich_mobile/services/secure_storage.service.dart';
import 'package:immich_mobile/services/widget.service.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
@@ -23,6 +24,7 @@ final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
ref.watch(apiServiceProvider),
ref.watch(userServiceProvider),
ref.watch(secureStorageServiceProvider),
ref.watch(widgetServiceProvider),
);
});
@@ -31,6 +33,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
final ApiService _apiService;
final UserService _userService;
final SecureStorageService _secureStorageService;
final WidgetService _widgetService;
final _log = Logger("AuthenticationNotifier");
static const Duration _timeoutDuration = Duration(seconds: 7);
@@ -40,6 +43,7 @@ class AuthNotifier extends StateNotifier<AuthState> {
this._apiService,
this._userService,
this._secureStorageService,
this._widgetService,
) : super(
AuthState(
deviceId: "",
@@ -76,6 +80,8 @@ class AuthNotifier extends StateNotifier<AuthState> {
Future<void> logout() async {
try {
await _secureStorageService.delete(kSecuredPinCode);
await _widgetService.clearCredentials();
await _authService.logout();
} finally {
await _cleanUp();
@@ -112,6 +118,11 @@ class AuthNotifier extends StateNotifier<AuthState> {
}) async {
await _apiService.setAccessToken(accessToken);
await _widgetService.writeCredentials(
Store.get(StoreKey.serverEndpoint),
accessToken,
);
// Get the deviceid from the store if it exists, otherwise generate a new one
String deviceId =
Store.tryGet(StoreKey.deviceId) ?? await FlutterUdid.consistentUdid;
@@ -0,0 +1,24 @@
import 'package:home_widget/home_widget.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/interfaces/widget.interface.dart';
final widgetRepositoryProvider = Provider((_) => WidgetRepository());
class WidgetRepository implements IWidgetRepository {
WidgetRepository();
@override
Future<void> saveData(String key, String value) async {
await HomeWidget.saveWidgetData<String>(key, value);
}
@override
Future<void> refresh(String name) async {
await HomeWidget.updateWidget(name: name, iOSName: name);
}
@override
Future<void> setAppGroupId(String appGroupId) async {
await HomeWidget.setAppGroupId(appGroupId);
}
}
+40
View File
@@ -0,0 +1,40 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/interfaces/widget.interface.dart';
import 'package:immich_mobile/repositories/widget.repository.dart';
final widgetServiceProvider = Provider((ref) {
return WidgetService(
ref.watch(widgetRepositoryProvider),
);
});
class WidgetService {
final IWidgetRepository _repository;
WidgetService(this._repository);
Future<void> writeCredentials(String serverURL, 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> clearCredentials() async {
await _repository.setAppGroupId(appShareGroupId);
await _repository.saveData(kWidgetServerEndpoint, "");
await _repository.saveData(kWidgetAuthToken, "");
// wait 3 seconds to ensure the widget is updated, dont block
Future.delayed(const Duration(seconds: 3), refreshWidgets);
}
Future<void> refreshWidgets() async {
for (final name in kWidgetNames) {
await _repository.refresh(name);
}
}
}