Compare commits

...

4 Commits

Author SHA1 Message Date
Ben Beckford 777e02e92b chore: update workflow method wrapper type 2026-06-24 21:47:07 -07:00
Ben Beckford d67313c625 Merge branch 'main' into feat/workflow-exif-filter 2026-06-24 21:43:52 -07:00
Yaros 4099fa6b4a fix(mobile): app doesn't exit full-screen mode (#29301)
* fix(mobile): app doesn't exit full-screen mode

* chore: rename restoreSystemUI to restoreEdgeToEdge
2026-06-24 20:48:01 -05:00
Ben Beckford d954591ca8 feat: exif metadata workflow filter 2026-06-23 16:02:04 -07:00
7 changed files with 140 additions and 40 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);
}
+70
View File
@@ -203,6 +203,76 @@
},
"uiHints": ["Filter"]
},
{
"name": "assetExifFilter",
"title": "Filter by EXIF metadata",
"description": "Filter assets by their EXIF properties",
"types": ["AssetV1"],
"schema": {
"type": "object",
"properties": {
"property": {
"title": "Property",
"description": "EXIF property to match",
"type": "string",
"enum": [
"make",
"model",
"exifImageWidth",
"exifImageHeight",
"fileSizeInByte",
"orientation",
"lensModel",
"fNumber",
"focalLength",
"iso",
"description",
"fps",
"exposureTime",
"livePhotoCID",
"timeZone",
"projectionType",
"profileDescription",
"colorspace",
"bitsPerSample",
"rating"
],
"uiHint": {
"order": 1
}
},
"pattern": {
"type": "string",
"title": "Pattern",
"description": "Text or regex pattern to match against property value",
"uiHint": {
"order": 2
}
},
"matchType": {
"type": "string",
"title": "Match type",
"enum": ["contains", "startsWith", "exact", "regex"],
"default": "contains",
"description": "Type of pattern matching to perform",
"uiHint": {
"order": 3
}
},
"caseSensitive": {
"type": "boolean",
"default": false,
"title": "Case sensitive",
"description": "Whether matching should be case-sensitive",
"uiHint": {
"order": 4
}
}
},
"required": ["property", "pattern"]
},
"uiHints": ["Filter"]
},
{
"name": "assetArchive",
"title": "Archive asset",
+1
View File
@@ -15,6 +15,7 @@ declare module 'main' {
export function assetMissingTimeZoneFilter(): I32;
export function assetLocationFilter(): I32;
export function assetTypeFilter(): I32;
export function assetExifFilter(): I32;
// updates
export function assetFavorite(): I32;
+45 -31
View File
@@ -4,40 +4,44 @@ import type manifestType from '../dist/manifest';
const wrapper = getWrapper<manifestType>();
export const assetFileFilter = () => {
return wrapper<'assetFileFilter'>(({ data, config }) => {
const { pattern, matchType = 'contains', caseSensitive = false } = config;
type MatchValueConfig = {
pattern: string;
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith';
caseSensitive?: boolean;
};
const { asset } = data;
const matchValueResult = (value: string, config: MatchValueConfig) => {
const { pattern, matchType = 'contains', caseSensitive = false } = config;
const searchName = caseSensitive ? value : value.toLowerCase();
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
const fileName = asset.originalFileName || '';
const searchName = caseSensitive ? fileName : fileName.toLowerCase();
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
switch (matchType) {
case 'contains': {
return { workflow: { continue: searchName.includes(searchPattern) } };
}
case 'exact': {
return { workflow: { continue: searchName === searchPattern } };
}
case 'startsWith': {
return { workflow: { continue: searchName.startsWith(searchPattern) } };
}
case 'regex': {
const flags = caseSensitive ? '' : 'i';
const regex = new RegExp(searchPattern, flags);
return { workflow: { continue: regex.test(fileName) } };
}
default: {
return {};
}
switch (matchType) {
case 'contains': {
return { workflow: { continue: searchName.includes(searchPattern) } };
}
});
case 'exact': {
return { workflow: { continue: searchName === searchPattern } };
}
case 'startsWith': {
return { workflow: { continue: searchName.startsWith(searchPattern) } };
}
case 'regex': {
const flags = caseSensitive ? '' : 'i';
const regex = new RegExp(searchPattern, flags);
return { workflow: { continue: regex.test(value) } };
}
default: {
return {};
}
}
};
export const assetFileFilter = () => {
return wrapper<'assetFileFilter'>(({ data, config }) => matchValueResult(data.asset.originalFileName || '', config));
};
export const assetMissingTimeZoneFilter = () => {
@@ -87,6 +91,16 @@ export const assetLocationFilter = () => {
});
};
export const assetExifFilter = () => {
return wrapper<'assetExifFilter'>(({ config, data }) => {
if (!data.asset.exifInfo) {
return { workflow: { continue: false } };
}
return matchValueResult(String(data.asset.exifInfo[config.property] || ''), config);
});
};
export const assetTypeFilter = () => {
return wrapper<'assetTypeFilter'>(({ config, data }) => {
return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };