From ab2a7006f9f8a43554e1fde64a5f1d655baaa138 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 13 Apr 2025 08:01:32 -0500 Subject: [PATCH] chore(mobile): small visual fix and update (#17547) * chore(mobile): small visual fix and update * update * update * remove design placeholder --- mobile/assets/i18n/en-US.json | 5 +- mobile/ios/Podfile.lock | 25 ++++ .../backup/backup_album_selection.page.dart | 5 +- mobile/lib/pages/photos/photos.page.dart | 31 ++--- mobile/lib/pages/search/search.page.dart | 2 +- mobile/lib/theme/theme_data.dart | 7 ++ .../widgets/asset_grid/multiselect_grid.dart | 3 +- .../common/immich_loading_indicator.dart | 112 ++++++++++++++++-- .../lib/widgets/forms/login/login_form.dart | 2 +- .../backup_settings/backup_settings.dart | 7 +- mobile/pubspec.lock | 8 ++ 11 files changed, 168 insertions(+), 39 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 3aa2f1b475..a17226b9cb 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -1,4 +1,5 @@ { + "open": "Open", "action_common_back": "Back", "action_common_cancel": "Cancel", "action_common_clear": "Clear", @@ -312,7 +313,7 @@ "home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping", "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", - "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", + "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album so that the timeline can populate photos and videos in it", "home_page_share_err_local": "Can not share local assets via link, skipping", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", "ignore_icloud_photos": "Ignore iCloud photos", @@ -693,4 +694,4 @@ "viewer_unstack": "Un-Stack", "wifi_name": "WiFi Name", "your_wifi_name": "Your WiFi name" -} \ No newline at end of file +} diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 28ce59feb1..908ee84aed 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -97,6 +97,25 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS + - sqlite3 (3.49.1): + - sqlite3/common (= 3.49.1) + - sqlite3/common (3.49.1) + - sqlite3/dbstatvtab (3.49.1): + - sqlite3/common + - sqlite3/fts5 (3.49.1): + - sqlite3/common + - sqlite3/perf-threadsafe (3.49.1): + - sqlite3/common + - sqlite3/rtree (3.49.1): + - sqlite3/common + - sqlite3_flutter_libs (0.0.1): + - Flutter + - FlutterMacOS + - sqlite3 (~> 3.49.1) + - sqlite3/dbstatvtab + - sqlite3/fts5 + - sqlite3/perf-threadsafe + - sqlite3/rtree - SwiftyGif (5.4.5) - url_launcher_ios (0.0.1): - Flutter @@ -130,6 +149,7 @@ DEPENDENCIES: - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) + - sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) @@ -140,6 +160,7 @@ SPEC REPOS: - MapLibre - SAMKeychain - SDWebImage + - sqlite3 - SwiftyGif EXTERNAL SOURCES: @@ -195,6 +216,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite_darwin: :path: ".symlinks/plugins/sqflite_darwin/darwin" + sqlite3_flutter_libs: + :path: ".symlinks/plugins/sqlite3_flutter_libs/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: @@ -232,6 +255,8 @@ SPEC CHECKSUMS: share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983 + sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241 SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 url_launcher_ios: 694010445543906933d732453a59da0a173ae33d wakelock_plus: 04623e3f525556020ebd4034310f20fe7fda8b49 diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 0869e75e9f..671a9bfe16 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -10,7 +10,6 @@ import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/backup/album_info_card.dart'; import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; -import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @RoutePage() @@ -37,7 +36,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { if (albums.isEmpty) { return const SliverToBoxAdapter( child: Center( - child: ImmichLoadingIndicator(), + child: CircularProgressIndicator(), ), ); } @@ -61,7 +60,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { if (albums.isEmpty) { return const SliverToBoxAdapter( child: Center( - child: ImmichLoadingIndicator(), + child: CircularProgressIndicator(), ), ); } diff --git a/mobile/lib/pages/photos/photos.page.dart b/mobile/lib/pages/photos/photos.page.dart index c9211e984d..62ac96c8aa 100644 --- a/mobile/lib/pages/photos/photos.page.dart +++ b/mobile/lib/pages/photos/photos.page.dart @@ -53,28 +53,29 @@ class PhotosPage extends HookConsumerWidget { padding: const EdgeInsets.only(top: 16.0), child: Text( 'home_page_building_timeline', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 16, + style: context.textTheme.titleMedium?.copyWith( color: context.primaryColor, ), ).tr(), ), + const SizedBox(height: 8), AnimatedOpacity( - duration: const Duration(milliseconds: 500), + duration: const Duration(milliseconds: 1000), opacity: tipOneOpacity.value, - child: SizedBox( - width: 250, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: const Text( - 'home_page_first_time_notice', - textAlign: TextAlign.justify, - style: TextStyle( - fontSize: 12, + child: Column( + children: [ + SizedBox( + width: 320, + child: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + 'home_page_first_time_notice', + textAlign: TextAlign.center, + style: context.textTheme.bodyMedium, + ).tr(), ), - ).tr(), - ), + ), + ], ), ), ], diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index b2bed73c6a..3f94f6b347 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -715,7 +715,7 @@ class SearchPage extends HookConsumerWidget { ), if (isSearching.value) const Expanded( - child: Center(child: CircularProgressIndicator.adaptive()), + child: Center(child: CircularProgressIndicator()), ) else SearchResultGrid( diff --git a/mobile/lib/theme/theme_data.dart b/mobile/lib/theme/theme_data.dart index 33dc5dff54..2a593ffb38 100644 --- a/mobile/lib/theme/theme_data.dart +++ b/mobile/lib/theme/theme_data.dart @@ -163,6 +163,13 @@ ThemeData getThemeData({ ), ), dialogTheme: DialogThemeData(backgroundColor: colorScheme.surfaceContainer), + progressIndicatorTheme: const ProgressIndicatorThemeData( + // ignore: deprecated_member_use + year2023: false, + // TODO: Uncommented after upgrade to version later than 3.29.2 + // circularTrackColor: Colors.black12, + trackGap: 3, + ), ); } diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index 03d04b682f..e6a6ff7233 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -22,7 +22,6 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; -import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/utils/selection_handlers.dart'; @@ -59,7 +58,7 @@ class MultiselectGrid extends HookConsumerWidget { final bool editEnabled; final Widget? emptyIndicator; Widget buildDefaultLoadingIndicator() => - const Center(child: ImmichLoadingIndicator()); + const Center(child: CircularProgressIndicator()); Widget buildEmptyIndicator() => emptyIndicator ?? Center(child: const Text("no_assets_to_show").tr()); diff --git a/mobile/lib/widgets/common/immich_loading_indicator.dart b/mobile/lib/widgets/common/immich_loading_indicator.dart index 67a2ce7baa..8f9eaeaa99 100644 --- a/mobile/lib/widgets/common/immich_loading_indicator.dart +++ b/mobile/lib/widgets/common/immich_loading_indicator.dart @@ -1,7 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/widgets/common/immich_logo.dart'; -class ImmichLoadingIndicator extends StatelessWidget { +class ImmichLoadingIndicator extends HookWidget { final double? borderRadius; const ImmichLoadingIndicator({ @@ -11,18 +12,109 @@ class ImmichLoadingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { + final logoAnimationController = useAnimationController( + duration: const Duration(seconds: 6), + ) + ..reverse() + ..repeat(); + + final borderAnimationController = useAnimationController( + duration: const Duration(seconds: 6), + )..repeat(); + return Container( - height: 60, - width: 60, + height: 80, + width: 80, decoration: BoxDecoration( - color: context.primaryColor.withAlpha(200), - borderRadius: BorderRadius.circular(borderRadius ?? 10), + color: Colors.transparent, + borderRadius: BorderRadius.circular(borderRadius ?? 50), + backgroundBlendMode: BlendMode.luminosity, ), - padding: const EdgeInsets.all(15), - child: const CircularProgressIndicator( - color: Colors.white, - strokeWidth: 3, + child: AnimatedBuilder( + animation: borderAnimationController, + builder: (context, child) { + return CustomPaint( + painter: GradientBorderPainter( + animation: borderAnimationController.value, + strokeWidth: 3, + ), + child: child, + ); + }, + child: Padding( + padding: const EdgeInsets.all(15), + child: RotationTransition( + turns: logoAnimationController, + child: const ImmichLogo( + heroTag: 'logo', + ), + ), + ), ), ); } } + +class GradientBorderPainter extends CustomPainter { + final double animation; + final double strokeWidth; + final double opacity = 0.7; + final colors = [ + const Color(0xFFFA2921), + const Color(0xFFED79B5), + const Color(0xFFFFB400), + const Color(0xFF1E83F7), + const Color(0xFF18C249), + ]; + + GradientBorderPainter({ + required this.animation, + required this.strokeWidth, + }); + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + final radius = min(size.width, size.height) / 2 - strokeWidth / 2; + + // Create a sweep gradient that covers the entire circle + final Rect rect = Rect.fromCircle(center: center, radius: radius); + + // Create a paint with the gradient + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth; + + // Create a gradient that smoothly transitions between colors + final shader = SweepGradient( + // Use a fixed starting point and let matrix transformation handle rotation + startAngle: 0, + endAngle: 2 * 3.14159, + colors: [ + // Repeat colors to ensure smooth transitions + ...colors.map((c) => c.withValues(alpha: opacity)), + colors.first.withValues(alpha: opacity), + ], + // Add evenly distributed stops + stops: List.generate( + colors.length + 1, + (index) => index / colors.length, + ), + tileMode: TileMode.clamp, + // Use transformations to rotate the gradient + transform: GradientRotation(-animation * 2 * 3.14159), + ).createShader(rect); + + paint.shader = shader; + + // Draw the circular border + canvas.drawCircle(center, radius, paint); + } + + @override + bool shouldRepaint(GradientBorderPainter oldDelegate) { + return animation != oldDelegate.animation; + } + + double min(double a, double b) => a < b ? a : b; +} diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart index ab532987a7..f9af31d6a4 100644 --- a/mobile/lib/widgets/forms/login/login_form.dart +++ b/mobile/lib/widgets/forms/login/login_form.dart @@ -303,7 +303,7 @@ class LoginForm extends HookConsumerWidget { ), onPressed: () => context.pushRoute(const SettingsRoute()), icon: const Icon(Icons.settings_rounded), - label: const SizedBox.shrink(), + label: const Text(""), ), ), const SizedBox(width: 1), diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart index 6c681e01df..20f172cb28 100644 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart @@ -13,7 +13,6 @@ import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; class BackupSettings extends HookConsumerWidget { const BackupSettings({ @@ -59,7 +58,7 @@ class BackupSettings extends HookConsumerWidget { ? const Column( children: [ SizedBox(height: 20), - Center(child: ImmichLoadingIndicator()), + Center(child: CircularProgressIndicator()), SizedBox(height: 20), ], ) @@ -83,9 +82,7 @@ class BackupSettings extends HookConsumerWidget { ), buttonText: 'sync_albums'.tr(), child: isAlbumSyncInProgress.value - ? const CircularProgressIndicator.adaptive( - strokeWidth: 2, - ) + ? const CircularProgressIndicator() : ElevatedButton( onPressed: syncAlbums, child: Text('sync'.tr()), diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 7c8348726f..9112ccc7ee 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1320,6 +1320,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + punycode: + dependency: "direct main" + description: + name: punycode + sha256: "39b874cc1f78b94e57db17e74b3f2ba2a96e25c0bebdcc8a571614dccda0ff0c" + url: "https://pub.dev" + source: hosted + version: "1.0.0" recase: dependency: transitive description: