Compare commits

..

2 Commits

Author SHA1 Message Date
Yaros d74a11f979 chore: rename restoreSystemUI to restoreEdgeToEdge 2026-06-24 13:06:28 +02:00
Yaros fb0d356ea7 fix(mobile): app doesn't exit full-screen mode 2026-06-24 12:01:50 +02:00
7 changed files with 45 additions and 31 deletions
@@ -12,6 +12,7 @@ import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.widget.dart';
import 'package:immich_mobile/presentation/widgets/memory/memory_card.widget.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
import 'package:immich_mobile/utils/system_ui.utils.dart';
import 'package:immich_mobile/widgets/memories/memory_epilogue.dart';
import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart';
@@ -49,7 +50,7 @@ class DriftMemoryPage extends HookConsumerWidget {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
return () {
// Clean up to normal edge to edge when we are done
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
restoreEdgeToEdge();
};
});
@@ -328,7 +329,7 @@ class DriftMemoryPage extends HookConsumerWidget {
// turn off full screen mode here
// https://github.com/Milad-Akarie/auto_route_library/issues/1799
context.maybePop();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
restoreEdgeToEdge();
},
shape: const CircleBorder(),
color: Colors.white.withValues(alpha: 0.2),
@@ -19,6 +19,7 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'
import 'package:immich_mobile/providers/asset_viewer/video_player_provider.dart';
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/system_ui.utils.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
@@ -76,7 +77,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
_pageController.dispose();
_crossfadeController.dispose();
unawaited(WakelockPlus.disable());
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
unawaited(restoreEdgeToEdge());
super.dispose();
}
@@ -255,7 +256,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
}
void _onTapUp() async {
await SystemChrome.setEnabledSystemUIMode(_showAppBar ? SystemUiMode.immersive : SystemUiMode.edgeToEdge);
await (_showAppBar ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive) : restoreEdgeToEdge());
WidgetsBinding.instance.addPostFrameCallback((_) {
setState(() {
@@ -23,6 +23,7 @@ import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart'
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/utils/system_ui.utils.dart';
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
@RoutePage()
@@ -128,7 +129,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
_reloadSubscription?.cancel();
_stackChildrenKeepAlive?.close();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
unawaited(restoreEdgeToEdge());
super.dispose();
}
@@ -251,10 +252,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
}
void _setSystemUIMode(bool controls, bool details) {
final mode = !controls || (CurrentPlatform.isIOS && details)
? SystemUiMode.immersiveSticky
: SystemUiMode.edgeToEdge;
unawaited(SystemChrome.setEnabledSystemUIMode(mode));
final immersive = !controls || (CurrentPlatform.isIOS && details);
unawaited(immersive ? SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky) : restoreEdgeToEdge());
}
@override
+14
View File
@@ -0,0 +1,14 @@
import 'dart:async';
import 'package:flutter/services.dart';
/// Restore the system bars and return to edge-to-edge layout.
///
/// On Android 15+/API 36 edge-to-edge is enforced, so calling
/// setEnabledSystemUIMode(edgeToEdge) does NOT re-show bars that an immersive
/// mode (immersive / immersiveSticky) previously hid. Explicitly request all
/// overlays first, then return to edge-to-edge layout.
Future<void> restoreEdgeToEdge() async {
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: SystemUiOverlay.values);
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
@@ -5,25 +5,32 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/presentation/actions/partner.action.dart';
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:mocktail/mocktail.dart';
import '../../factories/user_factory.dart';
import '../../mocks.dart';
import '../../presentation_context.dart';
void main() {
late PresentationContext context;
late UserDto currentUser;
final mocks = ServiceMocks();
setUp(() async {
currentUser = UserFactory.createDto();
context = await PresentationContext.create();
when(mocks.user.tryGetMyUser).thenReturn(currentUser);
});
tearDown(() {
context.dispose();
tearDown(() async {
mocks.resetAll();
await context.dispose();
});
List<Override> overrides({List<User> candidates = const []}) => [
...context.overrides,
partnerServiceProvider.overrideWithValue(context.mocks.partner.service),
currentUserProvider.overrideWith((ref) => CurrentUserProvider(mocks.user.service)),
partnerServiceProvider.overrideWithValue(mocks.partner.service),
candidatesStateProvider.overrideWith((ref) => Stream<Iterable<User>>.value(candidates)),
];
@@ -36,9 +43,7 @@ void main() {
await tester.tap(find.text(candidate.name));
await tester.pumpAndSettle();
verify(
() => context.mocks.partner.service.create(sharedById: context.currentUser.id, sharedWithId: candidate.id),
).called(1);
verify(() => mocks.partner.service.create(sharedById: currentUser.id, sharedWithId: candidate.id)).called(1);
});
testWidgets('creates nothing when the selection dialog is dismissed', (tester) async {
@@ -46,7 +51,7 @@ void main() {
await tester.sendKeyEvent(LogicalKeyboardKey.escape); // dismiss without selecting
await tester.pumpAndSettle();
verifyNever(context.mocks.partner.create);
verifyNever(mocks.partner.create);
});
});
@@ -60,9 +65,7 @@ void main() {
await tester.tap(find.byType(TextButton).last); // confirm
await tester.pumpAndSettle();
verify(
() => context.mocks.partner.service.delete(sharedById: context.currentUser.id, sharedWithId: partner.id),
).called(1);
verify(() => mocks.partner.service.delete(sharedById: currentUser.id, sharedWithId: partner.id)).called(1);
});
testWidgets('deletes nothing when the confirmation is cancelled', (tester) async {
@@ -74,7 +77,7 @@ void main() {
await tester.tap(find.byType(TextButton).first); // cancel
await tester.pumpAndSettle();
verifyNever(context.mocks.partner.delete);
verifyNever(mocks.partner.delete);
});
});
}
@@ -13,7 +13,7 @@ void main() {
late PresentationContext context;
setUp(() async => context = await PresentationContext.create());
tearDown(() => context.dispose());
tearDown(() async => await context.dispose());
group('PartnerSharedByList', () {
testWidgets('shows the empty-state add button when there are no partners', (tester) async {
+5 -9
View File
@@ -23,7 +23,7 @@ import 'mocks.dart';
class PresentationContext {
PresentationContext._({required UserDto user}) : currentUser = user, mocks = ServiceMocks() {
setup();
when(mocks.user.tryGetMyUser).thenReturn(currentUser);
}
static const String serverEndpoint = 'http://localhost:3000';
@@ -46,14 +46,10 @@ class PresentationContext {
return PresentationContext._(user: UserFactory.createDto());
}
void setup() {
when(mocks.user.tryGetMyUser).thenReturn(currentUser);
}
void dispose() {
addTearDown(() {
mocks.resetAll();
});
Future<void> dispose() async {
// TODO: Dispose the store and database after each test.
// This is currently not possible because the store is a singleton and is used across tests.
// Refactor the store to be created per test to allow proper disposal.
}
}