chore: bump dart sdk to 3.8 (#20355)

* chore: bump dart sdk to 3.8

* chore: make build

* make pigeon

* chore: format files

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2025-07-29 00:34:03 +05:30
committed by GitHub
parent 9b3718120b
commit e52b9d15b5
643 changed files with 32561 additions and 35292 deletions
+5 -8
View File
@@ -16,9 +16,7 @@ import 'package:immich_mobile/widgets/activities/dismissible_activity.dart';
@RoutePage()
class ActivitiesPage extends HookConsumerWidget {
const ActivitiesPage({
super.key,
});
const ActivitiesPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -60,9 +58,7 @@ class ActivitiesPage extends HookConsumerWidget {
itemBuilder: (context, index) {
// Additional vertical gap after the last element
if (index == data.length) {
return const SizedBox(
height: 80,
);
return const SizedBox(height: 80);
}
final activity = data[index];
@@ -73,8 +69,9 @@ class ActivitiesPage extends HookConsumerWidget {
child: DismissibleActivity(
activity.id,
ActivityTile(activity),
onDismiss:
canDelete ? (activityId) async => await activityNotifier.removeActivity(activity.id) : null,
onDismiss: canDelete
? (activityId) async => await activityNotifier.removeActivity(activity.id)
: null,
),
);
},
+19 -51
View File
@@ -12,17 +12,13 @@ import 'package:intl/intl.dart';
@RoutePage()
class AppLogPage extends HookConsumerWidget {
const AppLogPage({
super.key,
});
const AppLogPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final immichLogger = LogService.I;
final shouldReload = useState(false);
final logMessages = useFuture(
useMemoized(() => immichLogger.getMessages(), [shouldReload.value]),
);
final logMessages = useFuture(useMemoized(() => immichLogger.getMessages(), [shouldReload.value]));
Widget colorStatusIndicator(Color color) {
return Column(
@@ -31,38 +27,29 @@ class AppLogPage extends HookConsumerWidget {
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
],
);
}
Widget buildLeadingIcon(LogLevel level) => switch (level) {
LogLevel.info => colorStatusIndicator(context.primaryColor),
LogLevel.severe => colorStatusIndicator(Colors.redAccent),
LogLevel.warning => colorStatusIndicator(Colors.orangeAccent),
_ => colorStatusIndicator(Colors.grey),
};
LogLevel.info => colorStatusIndicator(context.primaryColor),
LogLevel.severe => colorStatusIndicator(Colors.redAccent),
LogLevel.warning => colorStatusIndicator(Colors.orangeAccent),
_ => colorStatusIndicator(Colors.grey),
};
Color getTileColor(LogLevel level) => switch (level) {
LogLevel.info => Colors.transparent,
LogLevel.severe => Colors.redAccent.withValues(alpha: 0.25),
LogLevel.warning => Colors.orangeAccent.withValues(alpha: 0.25),
_ => context.primaryColor.withValues(alpha: 0.1),
};
LogLevel.info => Colors.transparent,
LogLevel.severe => Colors.redAccent.withValues(alpha: 0.25),
LogLevel.warning => Colors.orangeAccent.withValues(alpha: 0.25),
_ => context.primaryColor.withValues(alpha: 0.1),
};
return Scaffold(
appBar: AppBar(
title: const Text(
"Logs",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16.0,
),
),
title: const Text("Logs", style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0)),
scrolledUnderElevation: 1,
elevation: 2,
actions: [
@@ -81,12 +68,7 @@ class AppLogPage extends HookConsumerWidget {
Builder(
builder: (BuildContext iconContext) {
return IconButton(
icon: Icon(
Icons.share_rounded,
color: context.primaryColor,
semanticLabel: "Share logs",
size: 20.0,
),
icon: Icon(Icons.share_rounded, color: context.primaryColor, semanticLabel: "Share logs", size: 20.0),
onPressed: () {
ImmichLogger.shareLogs(iconContext);
},
@@ -98,10 +80,7 @@ class AppLogPage extends HookConsumerWidget {
onPressed: () {
context.maybePop();
},
icon: const Icon(
Icons.arrow_back_ios_new_rounded,
size: 20.0,
),
icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20.0),
),
centerTitle: true,
),
@@ -113,11 +92,7 @@ class AppLogPage extends HookConsumerWidget {
itemBuilder: (context, index) {
var logMessage = logMessages.data![index];
return ListTile(
onTap: () => context.pushRoute(
AppLogDetailRoute(
logMessage: logMessage,
),
),
onTap: () => context.pushRoute(AppLogDetailRoute(logMessage: logMessage)),
trailing: const Icon(Icons.arrow_forward_ios_rounded),
visualDensity: VisualDensity.compact,
dense: true,
@@ -125,18 +100,11 @@ class AppLogPage extends HookConsumerWidget {
minLeadingWidth: 10,
title: Text(
truncateLogMessage(logMessage.message, 4),
style: TextStyle(
fontSize: 14.0,
color: context.colorScheme.onSurface,
fontFamily: "Inconsolata",
),
style: TextStyle(fontSize: 14.0, color: context.colorScheme.onSurface, fontFamily: "Inconsolata"),
),
subtitle: Text(
"at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.logger}",
style: TextStyle(
fontSize: 12.0,
color: context.colorScheme.onSurfaceSecondary,
),
style: TextStyle(fontSize: 12.0, color: context.colorScheme.onSurfaceSecondary),
),
leading: buildLeadingIcon(logMessage.level),
);
@@ -27,11 +27,7 @@ class AppLogDetailPage extends HookConsumerWidget {
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
header,
style: TextStyle(
fontSize: 12.0,
color: context.primaryColor,
fontWeight: FontWeight.bold,
),
style: TextStyle(fontSize: 12.0, color: context.primaryColor, fontWeight: FontWeight.bold),
),
),
IconButton(
@@ -41,38 +37,26 @@ class AppLogDetailPage extends HookConsumerWidget {
SnackBar(
content: Text(
"Copied to clipboard",
style: context.textTheme.bodyLarge?.copyWith(
color: context.primaryColor,
),
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
),
),
);
});
},
icon: Icon(
Icons.copy,
size: 16.0,
color: context.primaryColor,
),
icon: Icon(Icons.copy, size: 16.0, color: context.primaryColor),
),
],
),
Container(
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(
Radius.circular(15.0),
),
borderRadius: const BorderRadius.all(Radius.circular(15.0)),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SelectableText(
text,
style: const TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
fontFamily: "Inconsolata",
),
style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "Inconsolata"),
),
),
),
@@ -91,29 +75,19 @@ class AppLogDetailPage extends HookConsumerWidget {
padding: const EdgeInsets.only(bottom: 8.0),
child: Text(
"FROM",
style: TextStyle(
fontSize: 12.0,
color: context.primaryColor,
fontWeight: FontWeight.bold,
),
style: TextStyle(fontSize: 12.0, color: context.primaryColor, fontWeight: FontWeight.bold),
),
),
Container(
decoration: BoxDecoration(
color: context.colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.all(
Radius.circular(15.0),
),
borderRadius: const BorderRadius.all(Radius.circular(15.0)),
),
child: Padding(
padding: const EdgeInsets.all(8.0),
child: SelectableText(
context1.toString(),
style: const TextStyle(
fontSize: 12.0,
fontWeight: FontWeight.bold,
fontFamily: "Inconsolata",
),
style: const TextStyle(fontSize: 12.0, fontWeight: FontWeight.bold, fontFamily: "Inconsolata"),
),
),
),
@@ -123,20 +97,14 @@ class AppLogDetailPage extends HookConsumerWidget {
}
return Scaffold(
appBar: AppBar(
title: const Text("Log Detail"),
),
appBar: AppBar(title: const Text("Log Detail")),
body: SafeArea(
child: ListView(
children: [
buildTextWithCopyButton("MESSAGE", logMessage.message),
if (logMessage.error != null) buildTextWithCopyButton("DETAILS", logMessage.error.toString()),
if (logMessage.logger != null) buildLogContext1(logMessage.logger.toString()),
if (logMessage.stack != null)
buildTextWithCopyButton(
"STACK TRACE",
logMessage.stack.toString(),
),
if (logMessage.stack != null) buildTextWithCopyButton("STACK TRACE", logMessage.stack.toString()),
],
),
),
@@ -49,11 +49,9 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
// Cancel uploads
await Store.put(StoreKey.backgroundBackup, false);
ref.read(backupProvider.notifier).configureBackgroundBackup(
enabled: false,
onBatteryInfo: () {},
onError: (_) {},
);
ref
.read(backupProvider.notifier)
.configureBackgroundBackup(enabled: false, onBatteryInfo: () {}, onError: (_) {});
ref.read(backupProvider.notifier).setAutoBackup(false);
ref.read(backupProvider.notifier).cancelBackup();
ref.read(manualUploadProvider.notifier).cancelBackup();
@@ -65,14 +63,8 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
if (permission.isGranted) {
await ref.read(backgroundSyncProvider).syncLocal(full: true);
await migrateDeviceAssetToSqlite(
ref.read(isarProvider),
ref.read(driftProvider),
);
await migrateBackupAlbumsToSqlite(
ref.read(isarProvider),
ref.read(driftProvider),
);
await migrateDeviceAssetToSqlite(ref.read(isarProvider), ref.read(driftProvider));
await migrateBackupAlbumsToSqlite(ref.read(isarProvider), ref.read(driftProvider));
}
} else {
await ref.read(backgroundSyncProvider).cancel();
@@ -98,16 +90,8 @@ class _ChangeExperiencePageState extends ConsumerState<ChangeExperiencePage> {
AnimatedSwitcher(
duration: Durations.long4,
child: hasMigrated
? const Icon(
Icons.check_circle_rounded,
color: Colors.green,
size: 48.0,
)
: const SizedBox(
width: 50.0,
height: 50.0,
child: CircularProgressIndicator(),
),
? const Icon(Icons.check_circle_rounded, color: Colors.green, size: 48.0)
: const SizedBox(width: 50.0, height: 50.0, child: CircularProgressIndicator()),
),
const SizedBox(height: 16.0),
Center(
+18 -50
View File
@@ -20,10 +20,7 @@ import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart';
class CreateAlbumPage extends HookConsumerWidget {
final List<Asset>? assets;
const CreateAlbumPage({
super.key,
this.assets,
});
const CreateAlbumPage({super.key, this.assets});
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -32,9 +29,7 @@ class CreateAlbumPage extends HookConsumerWidget {
final albumDescriptionTextFieldFocusNode = useFocusNode();
final isAlbumTitleTextFieldFocus = useState(false);
final isAlbumTitleEmpty = useState(true);
final selectedAssets = useState<Set<Asset>>(
assets != null ? Set.from(assets!) : const {},
);
final selectedAssets = useState<Set<Asset>>(assets != null ? Set.from(assets!) : const {});
void onBackgroundTapped() {
albumTitleTextFieldFocusNode.unfocus();
@@ -50,10 +45,7 @@ class CreateAlbumPage extends HookConsumerWidget {
onSelectPhotosButtonPressed() async {
AssetSelectionPageResult? selectedAsset = await context.pushRoute<AssetSelectionPageResult?>(
AlbumAssetSelectionRoute(
existingAssets: selectedAssets.value,
canDeselect: true,
),
AlbumAssetSelectionRoute(existingAssets: selectedAssets.value, canDeselect: true),
);
if (selectedAsset == null) {
selectedAssets.value = const {};
@@ -64,10 +56,7 @@ class CreateAlbumPage extends HookConsumerWidget {
buildTitleInputField() {
return Padding(
padding: const EdgeInsets.only(
right: 10,
left: 10,
),
padding: const EdgeInsets.only(right: 10, left: 10),
child: AlbumTitleTextField(
isAlbumTitleEmpty: isAlbumTitleEmpty,
albumTitleTextFieldFocusNode: albumTitleTextFieldFocusNode,
@@ -79,10 +68,7 @@ class CreateAlbumPage extends HookConsumerWidget {
buildDescriptionInputField() {
return Padding(
padding: const EdgeInsets.only(
right: 10,
left: 10,
),
padding: const EdgeInsets.only(right: 10, left: 10),
child: AlbumViewerEditableDescription(
albumDescription: '',
descriptionFocusNode: albumDescriptionTextFieldFocusNode,
@@ -95,10 +81,7 @@ class CreateAlbumPage extends HookConsumerWidget {
return SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 200, left: 18),
child: Text(
'create_shared_album_page_share_add_assets',
style: context.textTheme.labelLarge,
).tr(),
child: Text('create_shared_album_page_share_add_assets', style: context.textTheme.labelLarge).tr(),
),
);
}
@@ -115,18 +98,11 @@ class CreateAlbumPage extends HookConsumerWidget {
style: FilledButton.styleFrom(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 16),
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10),
),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(10))),
backgroundColor: context.colorScheme.surfaceContainerHigh,
),
onPressed: onSelectPhotosButtonPressed,
icon: Icon(
Icons.add_rounded,
color: context.primaryColor,
),
icon: Icon(Icons.add_rounded, color: context.primaryColor),
label: Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
@@ -174,17 +150,12 @@ class CreateAlbumPage extends HookConsumerWidget {
crossAxisSpacing: 5.0,
mainAxisSpacing: 5,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return GestureDetector(
onTap: onBackgroundTapped,
child: SharedAlbumThumbnailImage(
asset: selectedAssets.value.elementAt(index),
),
);
},
childCount: selectedAssets.value.length,
),
delegate: SliverChildBuilderDelegate((BuildContext context, int index) {
return GestureDetector(
onTap: onBackgroundTapped,
child: SharedAlbumThumbnailImage(asset: selectedAssets.value.elementAt(index)),
);
}, childCount: selectedAssets.value.length),
),
);
}
@@ -194,10 +165,9 @@ class CreateAlbumPage extends HookConsumerWidget {
Future<void> createAlbum() async {
onBackgroundTapped();
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
ref.read(albumTitleProvider),
selectedAssets.value,
);
var newAlbum = await ref
.watch(albumProvider.notifier)
.createAlbum(ref.read(albumTitleProvider), selectedAssets.value);
if (newAlbum != null) {
ref.read(albumProvider.notifier).refreshRemoteAlbums();
@@ -220,9 +190,7 @@ class CreateAlbumPage extends HookConsumerWidget {
},
icon: const Icon(Icons.close_rounded),
),
title: const Text(
'create_album',
).tr(),
title: const Text('create_album').tr(),
actions: [
TextButton(
onPressed: albumTitleController.text.isNotEmpty ? createAlbum : null,
+17 -41
View File
@@ -6,22 +6,13 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
class DownloadPanel extends ConsumerWidget {
const DownloadPanel({
super.key,
});
const DownloadPanel({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final showProgress = ref.watch(
downloadStateProvider.select((state) => state.showProgress),
);
final showProgress = ref.watch(downloadStateProvider.select((state) => state.showProgress));
final tasks = ref
.watch(
downloadStateProvider.select((state) => state.taskProgress),
)
.entries
.toList();
final tasks = ref.watch(downloadStateProvider.select((state) => state.taskProgress)).entries.toList();
onCancelDownload(String id) {
ref.watch(downloadStateProvider.notifier).cancelDownload(id);
@@ -74,47 +65,35 @@ class DownloadTaskTile extends StatelessWidget {
final progressPercent = (progress * 100).round();
String getStatusText() => switch (status) {
TaskStatus.running => 'downloading'.tr(),
TaskStatus.complete => 'download_complete'.tr(),
TaskStatus.failed => 'download_failed'.tr(),
TaskStatus.canceled => 'download_canceled'.tr(),
TaskStatus.paused => 'download_paused'.tr(),
TaskStatus.enqueued => 'download_enqueue'.tr(),
TaskStatus.notFound => 'download_notfound'.tr(),
TaskStatus.waitingToRetry => 'download_waiting_to_retry'.tr(),
};
TaskStatus.running => 'downloading'.tr(),
TaskStatus.complete => 'download_complete'.tr(),
TaskStatus.failed => 'download_failed'.tr(),
TaskStatus.canceled => 'download_canceled'.tr(),
TaskStatus.paused => 'download_paused'.tr(),
TaskStatus.enqueued => 'download_enqueue'.tr(),
TaskStatus.notFound => 'download_notfound'.tr(),
TaskStatus.waitingToRetry => 'download_waiting_to_retry'.tr(),
};
return SizedBox(
key: const ValueKey('download_progress'),
width: context.width - 32,
child: Card(
clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(16),
),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(16))),
child: ListTile(
minVerticalPadding: 18,
leading: const Icon(Icons.video_file_outlined),
title: Text(
getStatusText(),
style: context.textTheme.labelLarge,
),
title: Text(getStatusText(), style: context.textTheme.labelLarge),
trailing: IconButton(
icon: Icon(Icons.close, color: context.colorScheme.onError),
onPressed: onCancelDownload,
style: ElevatedButton.styleFrom(
backgroundColor: context.colorScheme.error.withAlpha(200),
),
style: ElevatedButton.styleFrom(backgroundColor: context.colorScheme.error.withAlpha(200)),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
fileName,
style: context.textTheme.labelMedium,
),
Text(fileName, style: context.textTheme.labelMedium),
Row(
children: [
Expanded(
@@ -125,10 +104,7 @@ class DownloadTaskTile extends StatelessWidget {
),
),
const SizedBox(width: 8),
Text(
'$progressPercent%',
style: context.textTheme.labelSmall,
),
Text('$progressPercent%', style: context.textTheme.labelSmall),
],
),
],
@@ -36,11 +36,7 @@ class GalleryStackedChildren extends HookConsumerWidget {
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemCount: stackElements.length,
padding: const EdgeInsets.only(
left: 5,
right: 5,
bottom: 30,
),
padding: const EdgeInsets.only(left: 5, right: 5, bottom: 30),
itemBuilder: (context, index) {
final currentAsset = stackElements.elementAt(index);
final assetId = currentAsset.remoteId;
@@ -63,9 +59,7 @@ class GalleryStackedChildren extends HookConsumerWidget {
? const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(6)),
border: Border.fromBorderSide(
BorderSide(color: Colors.white, width: 2),
),
border: Border.fromBorderSide(BorderSide(color: Colors.white, width: 2)),
)
: const BoxDecoration(
color: Colors.white,
@@ -87,11 +87,7 @@ class GalleryViewerPage extends HookConsumerWidget {
if (index < totalAssets.value && index >= 0) {
final asset = loadAsset(index);
await precacheImage(
ImmichImage.imageProvider(
asset: asset,
width: context.width,
height: context.height,
),
ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height),
context,
onError: onError,
);
@@ -103,23 +99,20 @@ class GalleryViewerPage extends HookConsumerWidget {
}
}
useEffect(
() {
if (ref.read(showControlsProvider)) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
useEffect(() {
if (ref.read(showControlsProvider)) {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
} else {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
}
// Delay this a bit so we can finish loading the page
Timer(const Duration(milliseconds: 400), () {
precacheNextImage(currentIndex.value + 1);
});
// Delay this a bit so we can finish loading the page
Timer(const Duration(milliseconds: 400), () {
precacheNextImage(currentIndex.value + 1);
});
return null;
},
const [],
);
return null;
}, const []);
useEffect(() {
final asset = loadAsset(currentIndex.value);
@@ -136,9 +129,7 @@ class GalleryViewerPage extends HookConsumerWidget {
duration: const Duration(seconds: 1),
content: Text(
"local_asset_cast_failed".tr(),
style: context.textTheme.bodyLarge?.copyWith(
color: context.primaryColor,
),
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
),
),
);
@@ -147,9 +138,7 @@ class GalleryViewerPage extends HookConsumerWidget {
}
}
return null;
}, [
ref.watch(castProvider).isCasting,
]);
}, [ref.watch(castProvider).isCasting]);
void showInfo() {
final asset = ref.read(currentAssetProvider);
@@ -157,9 +146,7 @@ class GalleryViewerPage extends HookConsumerWidget {
return;
}
showModalBottomSheet(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
),
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(15.0))),
barrierColor: Colors.transparent,
isScrollControlled: true,
showDragHandle: true,
@@ -174,20 +161,10 @@ class GalleryViewerPage extends HookConsumerWidget {
expand: false,
builder: (context, scrollController) {
return Padding(
padding: EdgeInsets.only(
bottom: context.viewInsets.bottom,
),
child: ref.watch(appSettingsServiceProvider).getSetting<bool>(
AppSettingsEnum.advancedTroubleshooting,
)
? AdvancedBottomSheet(
assetDetail: asset,
scrollController: scrollController,
)
: DetailPanel(
asset: asset,
scrollController: scrollController,
),
padding: EdgeInsets.only(bottom: context.viewInsets.bottom),
child: ref.watch(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)
? AdvancedBottomSheet(assetDetail: asset, scrollController: scrollController)
: DetailPanel(asset: asset, scrollController: scrollController),
);
},
);
@@ -258,10 +235,7 @@ class GalleryViewerPage extends HookConsumerWidget {
tightMode: true,
initialScale: PhotoViewComputedScale.contained * 0.99,
minScale: PhotoViewComputedScale.contained * 0.99,
errorBuilder: (context, error, stackTrace) => ImmichImage(
asset,
fit: BoxFit.contain,
),
errorBuilder: (context, error, stackTrace) => ImmichImage(asset, fit: BoxFit.contain),
);
}
@@ -283,11 +257,7 @@ class GalleryViewerPage extends HookConsumerWidget {
asset: asset,
image: Image(
key: ValueKey(asset),
image: ImmichImage.imageProvider(
asset: asset,
width: context.width,
height: context.height,
),
image: ImmichImage.imageProvider(asset: asset, width: context.width, height: context.height),
fit: BoxFit.contain,
height: context.height,
width: context.width,
@@ -342,17 +312,8 @@ class GalleryViewerPage extends HookConsumerWidget {
child: Stack(
fit: StackFit.expand,
children: [
BackdropFilter(
filter: ui.ImageFilter.blur(
sigmaX: 10,
sigmaY: 10,
),
),
ImmichThumbnail(
key: ValueKey(asset),
asset: asset,
fit: BoxFit.contain,
),
BackdropFilter(filter: ui.ImageFilter.blur(sigmaX: 10, sigmaY: 10)),
ImmichThumbnail(key: ValueKey(asset), asset: asset, fit: BoxFit.contain),
],
),
);
@@ -361,9 +322,9 @@ class GalleryViewerPage extends HookConsumerWidget {
scrollPhysics: isZoomed.value
? const NeverScrollableScrollPhysics() // Don't allow paging while scrolled in
: (Platform.isIOS
? const FastScrollPhysics() // Use bouncing physics for iOS
: const FastClampingScrollPhysics() // Use heavy physics for Android
),
? const FastScrollPhysics() // Use bouncing physics for iOS
: const FastClampingScrollPhysics() // Use heavy physics for Android
),
itemCount: totalAssets.value,
scrollDirection: Axis.horizontal,
onPageChanged: (value, _) {
@@ -401,9 +362,7 @@ class GalleryViewerPage extends HookConsumerWidget {
duration: const Duration(seconds: 2),
content: Text(
"local_asset_cast_failed".tr(),
style: context.textTheme.bodyLarge?.copyWith(
color: context.primaryColor,
),
style: context.textTheme.bodyLarge?.copyWith(color: context.primaryColor),
),
),
);
@@ -416,10 +375,7 @@ class GalleryViewerPage extends HookConsumerWidget {
top: 0,
left: 0,
right: 0,
child: GalleryAppBar(
key: const ValueKey('app-bar'),
showInfo: showInfo,
),
child: GalleryAppBar(key: const ValueKey('app-bar'), showInfo: showInfo),
),
Positioned(
bottom: 0,
@@ -79,10 +79,8 @@ class HeaderSettingsPage extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0),
itemCount: list.length,
itemBuilder: (ctx, index) => list[index],
separatorBuilder: (context, index) => const Padding(
padding: EdgeInsets.only(bottom: 16.0, left: 8, right: 8),
child: Divider(),
),
separatorBuilder: (context, index) =>
const Padding(padding: EdgeInsets.only(bottom: 16.0, left: 8, right: 8), child: Divider()),
),
),
);
@@ -109,12 +107,9 @@ class HeaderKeyValueSettings extends StatelessWidget {
final SettingsHeader header;
final Function() onRemove;
HeaderKeyValueSettings({
super.key,
required this.header,
required this.onRemove,
}) : keyController = TextEditingController(text: header.key),
valueController = TextEditingController(text: header.value);
HeaderKeyValueSettings({super.key, required this.header, required this.onRemove})
: keyController = TextEditingController(text: header.key),
valueController = TextEditingController(text: header.value);
String? emptyFieldValidator(String? value) {
if (value == null || value.isEmpty) {
@@ -150,9 +145,7 @@ class HeaderKeyValueSettings extends StatelessWidget {
Padding(
padding: const EdgeInsets.only(left: 8),
child: IconButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
style: ElevatedButton.styleFrom(padding: const EdgeInsets.symmetric(vertical: 12)),
color: Colors.red[400],
onPressed: onRemove,
icon: const Icon(Icons.delete_outline),
@@ -8,10 +8,7 @@ class LargeLeadingTile extends StatelessWidget {
required this.onTap,
required this.title,
this.subtitle,
this.leadingPadding = const EdgeInsets.symmetric(
vertical: 8,
horizontal: 16.0,
),
this.leadingPadding = const EdgeInsets.symmetric(vertical: 8, horizontal: 16.0),
this.borderRadius = 20.0,
this.trailing,
this.selected = false,
@@ -47,18 +44,12 @@ class LargeLeadingTile extends StatelessWidget {
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Padding(
padding: leadingPadding,
child: leading,
),
Padding(padding: leadingPadding, child: leading),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: context.width * 0.6,
child: title,
),
SizedBox(width: context.width * 0.6, child: title),
subtitle ?? const SizedBox.shrink(),
],
),
@@ -78,17 +78,15 @@ class NativeVideoViewerPage extends HookConsumerWidget {
throw Exception('No file found for the video');
}
final source = await VideoSource.init(
path: file.path,
type: VideoSourceType.file,
);
final source = await VideoSource.init(path: file.path, type: VideoSourceType.file);
return source;
}
// Use a network URL for the video player controller
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final isOriginalVideo =
ref.read(appSettingsServiceProvider).getSetting<bool>(AppSettingsEnum.loadOriginalVideo);
final isOriginalVideo = ref
.read(appSettingsServiceProvider)
.getSetting<bool>(AppSettingsEnum.loadOriginalVideo);
final String postfixUrl = isOriginalVideo ? 'original' : 'video/playback';
final String videoUrl = asset.livePhotoVideoId != null
? '$serverEndpoint/assets/${asset.livePhotoVideoId}/$postfixUrl'
@@ -101,30 +99,24 @@ class NativeVideoViewerPage extends HookConsumerWidget {
);
return source;
} catch (error) {
log.severe(
'Error creating video source for asset ${asset.fileName}: $error',
);
log.severe('Error creating video source for asset ${asset.fileName}: $error');
return null;
}
}
final videoSource = useMemoized<Future<VideoSource?>>(() => createSource());
final aspectRatio = useState<double?>(asset.aspectRatio);
useMemoized(
() async {
if (!context.mounted || aspectRatio.value != null) {
return null;
}
useMemoized(() async {
if (!context.mounted || aspectRatio.value != null) {
return null;
}
try {
aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset);
} catch (error) {
log.severe(
'Error getting aspect ratio for asset ${asset.fileName}: $error',
);
}
},
);
try {
aspectRatio.value = await ref.read(assetServiceProvider).getAspectRatio(asset);
} catch (error) {
log.severe('Error getting aspect ratio for asset ${asset.fileName}: $error');
}
});
void checkIfBuffering() {
if (!context.mounted) {
@@ -134,8 +126,9 @@ class NativeVideoViewerPage extends HookConsumerWidget {
final videoPlayback = ref.read(videoPlaybackValueProvider);
if ((isBuffering.value || videoPlayback.state == VideoPlaybackState.initializing) &&
videoPlayback.state != VideoPlaybackState.buffering) {
ref.read(videoPlaybackValueProvider.notifier).value =
videoPlayback.copyWith(state: VideoPlaybackState.buffering);
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback.copyWith(
state: VideoPlaybackState.buffering,
);
}
}
@@ -322,48 +315,42 @@ class NativeVideoViewerPage extends HookConsumerWidget {
// This delay seems like a hacky way to resolve underlying bugs in video
// playback, but other resolutions failed thus far
Timer(
Platform.isIOS
? Duration(milliseconds: 300 * playbackDelayFactor)
: imageToVideo
? Duration(milliseconds: 200 * playbackDelayFactor)
: Duration(milliseconds: 400 * playbackDelayFactor), () {
if (!context.mounted) {
return;
}
currentAsset.value = value;
if (currentAsset.value == asset) {
onPlaybackReady();
}
});
});
useEffect(
() {
// If opening a remote video from a hero animation, delay visibility to avoid a stutter
final timer = isVisible.value
? null
: Timer(
const Duration(milliseconds: 300),
() => isVisible.value = true,
);
return () {
timer?.cancel();
final playerController = controller.value;
if (playerController == null) {
Platform.isIOS
? Duration(milliseconds: 300 * playbackDelayFactor)
: imageToVideo
? Duration(milliseconds: 200 * playbackDelayFactor)
: Duration(milliseconds: 400 * playbackDelayFactor),
() {
if (!context.mounted) {
return;
}
removeListeners(playerController);
playerController.stop().catchError((error) {
log.fine('Error stopping video: $error');
});
WakelockPlus.disable();
};
},
const [],
);
currentAsset.value = value;
if (currentAsset.value == asset) {
onPlaybackReady();
}
},
);
});
useEffect(() {
// If opening a remote video from a hero animation, delay visibility to avoid a stutter
final timer = isVisible.value ? null : Timer(const Duration(milliseconds: 300), () => isVisible.value = true);
return () {
timer?.cancel();
final playerController = controller.value;
if (playerController == null) {
return;
}
removeListeners(playerController);
playerController.stop().catchError((error) {
log.fine('Error stopping video: $error');
});
WakelockPlus.disable();
};
}, const []);
useOnAppLifecycleStateChange((_, state) async {
if (state == AppLifecycleState.resumed && shouldPlayOnForeground.value) {
@@ -393,12 +380,7 @@ class NativeVideoViewerPage extends HookConsumerWidget {
child: AspectRatio(
key: ValueKey(asset),
aspectRatio: aspectRatio.value!,
child: isCurrent
? NativeVideoPlayerView(
key: ValueKey(asset),
onViewReady: initController,
)
: null,
child: isCurrent ? NativeVideoPlayerView(key: ValueKey(asset), onViewReady: initController) : null,
),
),
),
+22 -67
View File
@@ -18,67 +18,31 @@ import 'package:immich_mobile/widgets/settings/preference_settings/preference_se
import 'package:immich_mobile/widgets/settings/settings_card.dart';
enum SettingSection {
beta(
'beta_sync',
Icons.sync_outlined,
"beta_sync_subtitle",
),
advanced(
'advanced',
Icons.build_outlined,
"advanced_settings_tile_subtitle",
),
assetViewer(
'asset_viewer_settings_title',
Icons.image_outlined,
"asset_viewer_settings_subtitle",
),
backup(
'backup',
Icons.cloud_upload_outlined,
"backup_setting_subtitle",
),
languages(
'language',
Icons.language,
"setting_languages_subtitle",
),
networking(
'networking_settings',
Icons.wifi,
"networking_subtitle",
),
notifications(
'notifications',
Icons.notifications_none_rounded,
"setting_notifications_subtitle",
),
preferences(
'preferences_settings_title',
Icons.interests_outlined,
"preferences_settings_subtitle",
),
timeline(
'asset_list_settings_title',
Icons.auto_awesome_mosaic_outlined,
"asset_list_settings_subtitle",
);
beta('beta_sync', Icons.sync_outlined, "beta_sync_subtitle"),
advanced('advanced', Icons.build_outlined, "advanced_settings_tile_subtitle"),
assetViewer('asset_viewer_settings_title', Icons.image_outlined, "asset_viewer_settings_subtitle"),
backup('backup', Icons.cloud_upload_outlined, "backup_setting_subtitle"),
languages('language', Icons.language, "setting_languages_subtitle"),
networking('networking_settings', Icons.wifi, "networking_subtitle"),
notifications('notifications', Icons.notifications_none_rounded, "setting_notifications_subtitle"),
preferences('preferences_settings_title', Icons.interests_outlined, "preferences_settings_subtitle"),
timeline('asset_list_settings_title', Icons.auto_awesome_mosaic_outlined, "asset_list_settings_subtitle");
final String title;
final String subtitle;
final IconData icon;
Widget get widget => switch (this) {
SettingSection.beta => const _BetaLandscapeToggle(),
SettingSection.advanced => const AdvancedSettings(),
SettingSection.assetViewer => const AssetViewerSettings(),
SettingSection.backup => const BackupSettings(),
SettingSection.languages => const LanguageSettings(),
SettingSection.networking => const NetworkingSettings(),
SettingSection.notifications => const NotificationSetting(),
SettingSection.preferences => const PreferenceSetting(),
SettingSection.timeline => const AssetListSettings(),
};
SettingSection.beta => const _BetaLandscapeToggle(),
SettingSection.advanced => const AdvancedSettings(),
SettingSection.assetViewer => const AssetViewerSettings(),
SettingSection.backup => const BackupSettings(),
SettingSection.languages => const LanguageSettings(),
SettingSection.networking => const NetworkingSettings(),
SettingSection.notifications => const NotificationSetting(),
SettingSection.preferences => const PreferenceSetting(),
SettingSection.timeline => const AssetListSettings(),
};
const SettingSection(this.title, this.icon, this.subtitle);
}
@@ -91,10 +55,7 @@ class SettingsPage extends StatelessWidget {
Widget build(BuildContext context) {
context.locale;
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('settings').tr(),
),
appBar: AppBar(centerTitle: false, title: const Text('settings').tr()),
body: context.isMobile ? const _MobileLayout() : const _TabletLayout(),
);
}
@@ -164,10 +125,7 @@ class _TabletLayout extends HookWidget {
),
),
const VerticalDivider(width: 1),
Expanded(
flex: 4,
child: selectedSection.value.widget,
),
Expanded(flex: 4, child: selectedSection.value.widget),
],
);
}
@@ -198,10 +156,7 @@ class SettingsSubPage extends StatelessWidget {
Widget build(BuildContext context) {
context.locale;
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: Text(section.title).tr(),
),
appBar: AppBar(centerTitle: false, title: Text(section.title).tr()),
body: section.widget,
);
}
@@ -43,31 +43,26 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
final accessToken = Store.tryGet(StoreKey.accessToken);
if (accessToken != null && serverUrl != null && endpoint != null) {
ref.read(authProvider.notifier).saveAuthInfo(accessToken: accessToken).then(
(a) => {
log.info('Successfully updated auth info with access token: $accessToken'),
},
ref
.read(authProvider.notifier)
.saveAuthInfo(accessToken: accessToken)
.then(
(a) => {log.info('Successfully updated auth info with access token: $accessToken')},
onError: (exception) => {
log.severe(
'Failed to update auth info with access token: $accessToken',
),
log.severe('Failed to update auth info with access token: $accessToken'),
ref.read(authProvider.notifier).logout(),
context.replaceRoute(const LoginRoute()),
},
);
} else {
log.severe(
'Missing crucial offline login info - Logging out completely',
);
log.severe('Missing crucial offline login info - Logging out completely');
ref.read(authProvider.notifier).logout();
context.replaceRoute(const LoginRoute());
return;
}
if (context.router.current.name == SplashScreenRoute.name) {
context.replaceRoute(
Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute(),
);
context.replaceRoute(Store.isBetaTimelineEnabled ? const TabShellRoute() : const TabControllerRoute());
}
if (Store.isBetaTimelineEnabled) {
@@ -85,11 +80,7 @@ class SplashScreenPageState extends ConsumerState<SplashScreenPage> {
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Image(
image: AssetImage('assets/immich-logo.png'),
width: 80,
filterQuality: FilterQuality.high,
),
child: Image(image: AssetImage('assets/immich-logo.png'), width: 80, filterQuality: FilterQuality.high),
),
);
}
@@ -36,9 +36,7 @@ class TabControllerPage extends HookConsumerWidget {
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
context.primaryColor,
),
valueColor: AlwaysStoppedAnimation<Color>(context.primaryColor),
),
),
),
@@ -65,51 +63,31 @@ class TabControllerPage extends HookConsumerWidget {
final navigationDestinations = [
NavigationDestination(
label: 'photos'.tr(),
icon: const Icon(
Icons.photo_library_outlined,
),
icon: const Icon(Icons.photo_library_outlined),
selectedIcon: buildIcon(
isProcessing: isRefreshingAssets,
icon: Icon(
Icons.photo_library,
color: context.primaryColor,
),
icon: Icon(Icons.photo_library, color: context.primaryColor),
),
),
NavigationDestination(
label: 'search'.tr(),
icon: const Icon(
Icons.search_rounded,
),
selectedIcon: Icon(
Icons.search,
color: context.primaryColor,
),
icon: const Icon(Icons.search_rounded),
selectedIcon: Icon(Icons.search, color: context.primaryColor),
),
NavigationDestination(
label: 'albums'.tr(),
icon: const Icon(
Icons.photo_album_outlined,
),
icon: const Icon(Icons.photo_album_outlined),
selectedIcon: buildIcon(
isProcessing: isRefreshingRemoteAlbums,
icon: Icon(
Icons.photo_album_rounded,
color: context.primaryColor,
),
icon: Icon(Icons.photo_album_rounded, color: context.primaryColor),
),
),
NavigationDestination(
label: 'library'.tr(),
icon: const Icon(
Icons.space_dashboard_outlined,
),
icon: const Icon(Icons.space_dashboard_outlined),
selectedIcon: buildIcon(
isProcessing: isRefreshingAssets,
icon: Icon(
Icons.space_dashboard_rounded,
color: context.primaryColor,
),
icon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor),
),
),
];
@@ -125,13 +103,7 @@ class TabControllerPage extends HookConsumerWidget {
Widget navigationRail(TabsRouter tabsRouter) {
return NavigationRail(
destinations: navigationDestinations
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
selectedIcon: e.selectedIcon,
),
)
.map((e) => NavigationRailDestination(icon: e.icon, label: Text(e.label), selectedIcon: e.selectedIcon))
.toList(),
onDestinationSelected: (index) => onNavigationSelected(tabsRouter, index),
selectedIndex: tabsRouter.activeIndex,
@@ -142,17 +114,9 @@ class TabControllerPage extends HookConsumerWidget {
final multiselectEnabled = ref.watch(multiselectProvider);
return AutoTabsRouter(
routes: [
const PhotosRoute(),
SearchRoute(),
const AlbumsRoute(),
const LibraryRoute(),
],
routes: [const PhotosRoute(), SearchRoute(), const AlbumsRoute(), const LibraryRoute()],
duration: const Duration(milliseconds: 600),
transitionBuilder: (context, child, animation) => FadeTransition(
opacity: animation,
child: child,
),
transitionBuilder: (context, child, animation) => FadeTransition(opacity: animation, child: child),
builder: (context, child) {
final tabsRouter = AutoTabsRouter.of(context);
return PopScope(
+13 -53
View File
@@ -56,56 +56,30 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
final navigationDestinations = [
NavigationDestination(
label: 'photos'.tr(),
icon: const Icon(
Icons.photo_library_outlined,
),
selectedIcon: Icon(
Icons.photo_library,
color: context.primaryColor,
),
icon: const Icon(Icons.photo_library_outlined),
selectedIcon: Icon(Icons.photo_library, color: context.primaryColor),
),
NavigationDestination(
label: 'search'.tr(),
icon: const Icon(
Icons.search_rounded,
),
selectedIcon: Icon(
Icons.search,
color: context.primaryColor,
),
icon: const Icon(Icons.search_rounded),
selectedIcon: Icon(Icons.search, color: context.primaryColor),
),
NavigationDestination(
label: 'albums'.tr(),
icon: const Icon(
Icons.photo_album_outlined,
),
selectedIcon: Icon(
Icons.photo_album_rounded,
color: context.primaryColor,
),
icon: const Icon(Icons.photo_album_outlined),
selectedIcon: Icon(Icons.photo_album_rounded, color: context.primaryColor),
),
NavigationDestination(
label: 'library'.tr(),
icon: const Icon(
Icons.space_dashboard_outlined,
),
selectedIcon: Icon(
Icons.space_dashboard_rounded,
color: context.primaryColor,
),
icon: const Icon(Icons.space_dashboard_outlined),
selectedIcon: Icon(Icons.space_dashboard_rounded, color: context.primaryColor),
),
];
Widget navigationRail(TabsRouter tabsRouter) {
return NavigationRail(
destinations: navigationDestinations
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(e.label),
selectedIcon: e.selectedIcon,
),
)
.map((e) => NavigationRailDestination(icon: e.icon, label: Text(e.label), selectedIcon: e.selectedIcon))
.toList(),
onDestinationSelected: (index) => _onNavigationSelected(tabsRouter, index, ref),
selectedIndex: tabsRouter.activeIndex,
@@ -115,17 +89,9 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
}
return AutoTabsRouter(
routes: [
const MainTimelineRoute(),
DriftSearchRoute(),
const DriftAlbumsRoute(),
const DriftLibraryRoute(),
],
routes: [const MainTimelineRoute(), DriftSearchRoute(), const DriftAlbumsRoute(), const DriftLibraryRoute()],
duration: const Duration(milliseconds: 600),
transitionBuilder: (context, child, animation) => FadeTransition(
opacity: animation,
child: child,
),
transitionBuilder: (context, child, animation) => FadeTransition(opacity: animation, child: child),
builder: (context, child) {
final tabsRouter = AutoTabsRouter.of(context);
return PopScope(
@@ -142,10 +108,7 @@ class _TabShellPageState extends ConsumerState<TabShellPage> {
],
)
: child,
bottomNavigationBar: _BottomNavigationBar(
tabsRouter: tabsRouter,
destinations: navigationDestinations,
),
bottomNavigationBar: _BottomNavigationBar(tabsRouter: tabsRouter, destinations: navigationDestinations),
),
);
},
@@ -175,10 +138,7 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
}
class _BottomNavigationBar extends ConsumerWidget {
const _BottomNavigationBar({
required this.tabsRouter,
required this.destinations,
});
const _BottomNavigationBar({required this.tabsRouter, required this.destinations});
final List<Widget> destinations;
final TabsRouter tabsRouter;