mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 15:08:44 -04:00
feat(mobile): beta sync stats page (#19950)
* show beta sync stats * show status next to jobs * use drift devtools reset database impl * dcm fixes * fix: hash count * styling --------- Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
97daf42fd5
commit
2efca67217
19
i18n/en.json
19
i18n/en.json
@ -573,6 +573,8 @@
|
|||||||
"backup_options_page_title": "Backup options",
|
"backup_options_page_title": "Backup options",
|
||||||
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
||||||
"backward": "Backward",
|
"backward": "Backward",
|
||||||
|
"beta_sync": "Beta Sync Status",
|
||||||
|
"beta_sync_subtitle": "Manage the new sync system",
|
||||||
"biometric_auth_enabled": "Biometric authentication enabled",
|
"biometric_auth_enabled": "Biometric authentication enabled",
|
||||||
"biometric_locked_out": "You are locked out of biometric authentication",
|
"biometric_locked_out": "You are locked out of biometric authentication",
|
||||||
"biometric_no_options": "No biometric options available",
|
"biometric_no_options": "No biometric options available",
|
||||||
@ -1051,6 +1053,9 @@
|
|||||||
"haptic_feedback_switch": "Enable haptic feedback",
|
"haptic_feedback_switch": "Enable haptic feedback",
|
||||||
"haptic_feedback_title": "Haptic Feedback",
|
"haptic_feedback_title": "Haptic Feedback",
|
||||||
"has_quota": "Has quota",
|
"has_quota": "Has quota",
|
||||||
|
"hash_asset": "Hash asset",
|
||||||
|
"hashed_assets": "Hashed assets",
|
||||||
|
"hashing": "Hashing",
|
||||||
"header_settings_add_header_tip": "Add Header",
|
"header_settings_add_header_tip": "Add Header",
|
||||||
"header_settings_field_validator_msg": "Value cannot be empty",
|
"header_settings_field_validator_msg": "Value cannot be empty",
|
||||||
"header_settings_header_name_input": "Header name",
|
"header_settings_header_name_input": "Header name",
|
||||||
@ -1083,6 +1088,7 @@
|
|||||||
"host": "Host",
|
"host": "Host",
|
||||||
"hour": "Hour",
|
"hour": "Hour",
|
||||||
"id": "ID",
|
"id": "ID",
|
||||||
|
"idle": "Idle",
|
||||||
"ignore_icloud_photos": "Ignore iCloud photos",
|
"ignore_icloud_photos": "Ignore iCloud photos",
|
||||||
"ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
|
"ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
|
||||||
"image": "Image",
|
"image": "Image",
|
||||||
@ -1165,7 +1171,9 @@
|
|||||||
"list": "List",
|
"list": "List",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"loading_search_results_failed": "Loading search results failed",
|
"loading_search_results_failed": "Loading search results failed",
|
||||||
|
"local": "Local",
|
||||||
"local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server",
|
"local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server",
|
||||||
|
"local_assets": "Local Assets",
|
||||||
"local_network": "Local network",
|
"local_network": "Local network",
|
||||||
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
|
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
|
||||||
"location_permission": "Location permission",
|
"location_permission": "Location permission",
|
||||||
@ -1359,6 +1367,7 @@
|
|||||||
"original": "original",
|
"original": "original",
|
||||||
"other": "Other",
|
"other": "Other",
|
||||||
"other_devices": "Other devices",
|
"other_devices": "Other devices",
|
||||||
|
"other_entities": "Other entities",
|
||||||
"other_variables": "Other variables",
|
"other_variables": "Other variables",
|
||||||
"owned": "Owned",
|
"owned": "Owned",
|
||||||
"owner": "Owner",
|
"owner": "Owner",
|
||||||
@ -1519,6 +1528,8 @@
|
|||||||
"refreshing_faces": "Refreshing faces",
|
"refreshing_faces": "Refreshing faces",
|
||||||
"refreshing_metadata": "Refreshing metadata",
|
"refreshing_metadata": "Refreshing metadata",
|
||||||
"regenerating_thumbnails": "Regenerating thumbnails",
|
"regenerating_thumbnails": "Regenerating thumbnails",
|
||||||
|
"remote": "Remote",
|
||||||
|
"remote_assets": "Remote Assets",
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"remove_assets_album_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from the album?",
|
"remove_assets_album_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from the album?",
|
||||||
"remove_assets_shared_link_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from this shared link?",
|
"remove_assets_shared_link_confirmation": "Are you sure you want to remove {count, plural, one {# asset} other {# assets}} from this shared link?",
|
||||||
@ -1556,6 +1567,9 @@
|
|||||||
"reset_password": "Reset password",
|
"reset_password": "Reset password",
|
||||||
"reset_people_visibility": "Reset people visibility",
|
"reset_people_visibility": "Reset people visibility",
|
||||||
"reset_pin_code": "Reset PIN code",
|
"reset_pin_code": "Reset PIN code",
|
||||||
|
"reset_sqlite": "Reset SQLite Database",
|
||||||
|
"reset_sqlite_confirmation": "Are you sure you want to reset the SQLite database? You will need to log out and log in again to resync the data",
|
||||||
|
"reset_sqlite_success": "Successfully reset the SQLite database",
|
||||||
"reset_to_default": "Reset to default",
|
"reset_to_default": "Reset to default",
|
||||||
"resolve_duplicates": "Resolve duplicates",
|
"resolve_duplicates": "Resolve duplicates",
|
||||||
"resolved_all_duplicates": "Resolved all duplicates",
|
"resolved_all_duplicates": "Resolved all duplicates",
|
||||||
@ -1569,6 +1583,7 @@
|
|||||||
"role": "Role",
|
"role": "Role",
|
||||||
"role_editor": "Editor",
|
"role_editor": "Editor",
|
||||||
"role_viewer": "Viewer",
|
"role_viewer": "Viewer",
|
||||||
|
"running": "Running",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"save_to_gallery": "Save to gallery",
|
"save_to_gallery": "Save to gallery",
|
||||||
"saved_api_key": "Saved API Key",
|
"saved_api_key": "Saved API Key",
|
||||||
@ -1822,6 +1837,7 @@
|
|||||||
"storage_quota": "Storage Quota",
|
"storage_quota": "Storage Quota",
|
||||||
"storage_usage": "{used} of {available} used",
|
"storage_usage": "{used} of {available} used",
|
||||||
"submit": "Submit",
|
"submit": "Submit",
|
||||||
|
"success": "Success",
|
||||||
"suggestions": "Suggestions",
|
"suggestions": "Suggestions",
|
||||||
"sunrise_on_the_beach": "Sunrise on the beach",
|
"sunrise_on_the_beach": "Sunrise on the beach",
|
||||||
"support": "Support",
|
"support": "Support",
|
||||||
@ -1831,6 +1847,8 @@
|
|||||||
"sync": "Sync",
|
"sync": "Sync",
|
||||||
"sync_albums": "Sync albums",
|
"sync_albums": "Sync albums",
|
||||||
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
||||||
|
"sync_local": "Sync Local",
|
||||||
|
"sync_remote": "Sync Remote",
|
||||||
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
||||||
"tag": "Tag",
|
"tag": "Tag",
|
||||||
"tag_assets": "Tag assets",
|
"tag_assets": "Tag assets",
|
||||||
@ -1841,6 +1859,7 @@
|
|||||||
"tag_updated": "Updated tag: {tag}",
|
"tag_updated": "Updated tag: {tag}",
|
||||||
"tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}",
|
"tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
|
"tap_to_run_job": "Tap to run job",
|
||||||
"template": "Template",
|
"template": "Template",
|
||||||
"theme": "Theme",
|
"theme": "Theme",
|
||||||
"theme_selection": "Theme selection",
|
"theme_selection": "Theme selection",
|
||||||
|
@ -76,4 +76,15 @@ class AssetService {
|
|||||||
Future<List<(String, String)>> getPlaces() {
|
Future<List<(String, String)>> getPlaces() {
|
||||||
return _remoteAssetRepository.getPlaces();
|
return _remoteAssetRepository.getPlaces();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<(int local, int remote)> getAssetCounts() async {
|
||||||
|
return (
|
||||||
|
await _localAssetRepository.getCount(),
|
||||||
|
await _remoteAssetRepository.getCount()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getLocalHashedCount() {
|
||||||
|
return _localAssetRepository.getHashedCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,4 +18,8 @@ class LocalAlbumService {
|
|||||||
Future<void> update(LocalAlbum album) {
|
Future<void> update(LocalAlbum album) {
|
||||||
return _repository.upsert(album);
|
return _repository.upsert(album);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getCount() {
|
||||||
|
return _repository.getCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,4 +12,8 @@ class DriftMemoryService {
|
|||||||
Future<List<DriftMemory>> getMemoryLane(String ownerId) {
|
Future<List<DriftMemory>> getMemoryLane(String ownerId) {
|
||||||
return _repository.getAll(ownerId);
|
return _repository.getAll(ownerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getCount() {
|
||||||
|
return _repository.getCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,4 +147,8 @@ class RemoteAlbumService {
|
|||||||
|
|
||||||
return _repository.addUsers(albumId, userIds);
|
return _repository.addUsers(albumId, userIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getCount() {
|
||||||
|
return _repository.getCount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,14 @@ class BackgroundSyncManager {
|
|||||||
final SyncCallback? onRemoteSyncComplete;
|
final SyncCallback? onRemoteSyncComplete;
|
||||||
final SyncErrorCallback? onRemoteSyncError;
|
final SyncErrorCallback? onRemoteSyncError;
|
||||||
|
|
||||||
|
final SyncCallback? onLocalSyncStart;
|
||||||
|
final SyncCallback? onLocalSyncComplete;
|
||||||
|
final SyncErrorCallback? onLocalSyncError;
|
||||||
|
|
||||||
|
final SyncCallback? onHashingStart;
|
||||||
|
final SyncCallback? onHashingComplete;
|
||||||
|
final SyncErrorCallback? onHashingError;
|
||||||
|
|
||||||
Cancelable<void>? _syncTask;
|
Cancelable<void>? _syncTask;
|
||||||
Cancelable<void>? _syncWebsocketTask;
|
Cancelable<void>? _syncWebsocketTask;
|
||||||
Cancelable<void>? _deviceAlbumSyncTask;
|
Cancelable<void>? _deviceAlbumSyncTask;
|
||||||
@ -21,6 +29,12 @@ class BackgroundSyncManager {
|
|||||||
this.onRemoteSyncStart,
|
this.onRemoteSyncStart,
|
||||||
this.onRemoteSyncComplete,
|
this.onRemoteSyncComplete,
|
||||||
this.onRemoteSyncError,
|
this.onRemoteSyncError,
|
||||||
|
this.onLocalSyncStart,
|
||||||
|
this.onLocalSyncComplete,
|
||||||
|
this.onLocalSyncError,
|
||||||
|
this.onHashingStart,
|
||||||
|
this.onHashingComplete,
|
||||||
|
this.onHashingError,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> cancel() {
|
Future<void> cancel() {
|
||||||
@ -47,6 +61,8 @@ class BackgroundSyncManager {
|
|||||||
return _deviceAlbumSyncTask!.future;
|
return _deviceAlbumSyncTask!.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onLocalSyncStart?.call();
|
||||||
|
|
||||||
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
|
// We use a ternary operator to avoid [_deviceAlbumSyncTask] from being
|
||||||
// captured by the closure passed to [runInIsolateGentle].
|
// captured by the closure passed to [runInIsolateGentle].
|
||||||
_deviceAlbumSyncTask = full
|
_deviceAlbumSyncTask = full
|
||||||
@ -61,6 +77,10 @@ class BackgroundSyncManager {
|
|||||||
|
|
||||||
return _deviceAlbumSyncTask!.whenComplete(() {
|
return _deviceAlbumSyncTask!.whenComplete(() {
|
||||||
_deviceAlbumSyncTask = null;
|
_deviceAlbumSyncTask = null;
|
||||||
|
onLocalSyncComplete?.call();
|
||||||
|
}).catchError((error) {
|
||||||
|
onLocalSyncError?.call(error.toString());
|
||||||
|
_deviceAlbumSyncTask = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,10 +90,17 @@ class BackgroundSyncManager {
|
|||||||
return _hashTask!.future;
|
return _hashTask!.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onHashingStart?.call();
|
||||||
|
|
||||||
_hashTask = runInIsolateGentle(
|
_hashTask = runInIsolateGentle(
|
||||||
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
|
computation: (ref) => ref.read(hashServiceProvider).hashAssets(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return _hashTask!.whenComplete(() {
|
return _hashTask!.whenComplete(() {
|
||||||
|
onHashingComplete?.call();
|
||||||
|
_hashTask = null;
|
||||||
|
}).catchError((error) {
|
||||||
|
onHashingError?.call(error.toString());
|
||||||
_hashTask = null;
|
_hashTask = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -398,4 +398,8 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
return results.isNotEmpty ? results.first : null;
|
return results.isNotEmpty ? results.first : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getCount() {
|
||||||
|
return _db.managers.localAlbumEntity.count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,4 +63,14 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
return query.map((row) => row.toDto()).getSingleOrNull();
|
return query.map((row) => row.toDto()).getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getCount() {
|
||||||
|
return _db.managers.localAssetEntity.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> getHashedCount() {
|
||||||
|
return _db.managers.localAssetEntity
|
||||||
|
.filter((e) => e.checksum.isNull().not())
|
||||||
|
.count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,6 +58,10 @@ class DriftMemoryRepository extends DriftDatabaseRepository {
|
|||||||
|
|
||||||
return memoriesMap.values.toList();
|
return memoriesMap.values.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getCount() {
|
||||||
|
return _db.managers.memoryEntity.count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on MemoryEntityData {
|
extension on MemoryEntityData {
|
||||||
|
@ -268,6 +268,10 @@ class DriftRemoteAlbumRepository extends DriftDatabaseRepository {
|
|||||||
return album;
|
return album;
|
||||||
}).watchSingleOrNull();
|
}).watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getCount() {
|
||||||
|
return _db.managers.remoteAlbumEntity.count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on RemoteAlbumEntityData {
|
extension on RemoteAlbumEntityData {
|
||||||
|
@ -238,4 +238,8 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<int> getCount() {
|
||||||
|
return _db.managers.remoteAssetEntity.count();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,8 @@ import 'package:immich_mobile/widgets/settings/language_settings.dart';
|
|||||||
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
|
import 'package:immich_mobile/widgets/settings/networking_settings/networking_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
import 'package:immich_mobile/widgets/settings/notification_setting.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart';
|
import 'package:immich_mobile/widgets/settings/preference_settings/preference_setting.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart' as app_store;
|
||||||
|
import 'package:immich_mobile/widgets/settings/settings_card.dart';
|
||||||
|
|
||||||
enum SettingSection {
|
enum SettingSection {
|
||||||
advanced(
|
advanced(
|
||||||
@ -97,47 +99,11 @@ class _MobileLayout extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final List<Widget> settings = SettingSection.values
|
final List<Widget> settings = SettingSection.values
|
||||||
.map(
|
.map(
|
||||||
(setting) => Padding(
|
(setting) => SettingsCard(
|
||||||
padding: const EdgeInsets.symmetric(
|
title: setting.title.tr(),
|
||||||
horizontal: 16.0,
|
subtitle: setting.subtitle.tr(),
|
||||||
),
|
icon: setting.icon,
|
||||||
child: Card(
|
settingRoute: SettingsSubRoute(section: setting),
|
||||||
elevation: 0,
|
|
||||||
clipBehavior: Clip.antiAlias,
|
|
||||||
color: context.colorScheme.surfaceContainer,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
|
||||||
child: ListTile(
|
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16.0,
|
|
||||||
),
|
|
||||||
leading: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
|
||||||
color: context.isDarkTheme
|
|
||||||
? Colors.black26
|
|
||||||
: Colors.white.withAlpha(100),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Icon(setting.icon, color: context.primaryColor),
|
|
||||||
),
|
|
||||||
title: Text(
|
|
||||||
setting.title,
|
|
||||||
style: context.textTheme.titleMedium!.copyWith(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: context.primaryColor,
|
|
||||||
),
|
|
||||||
).tr(),
|
|
||||||
subtitle: Text(
|
|
||||||
setting.subtitle,
|
|
||||||
style: context.textTheme.labelLarge,
|
|
||||||
).tr(),
|
|
||||||
onTap: () =>
|
|
||||||
context.pushRoute(SettingsSubRoute(section: setting)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
@ -146,6 +112,13 @@ class _MobileLayout extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.only(top: 10.0, bottom: 56),
|
padding: const EdgeInsets.only(top: 10.0, bottom: 56),
|
||||||
children: [
|
children: [
|
||||||
const BetaTimelineListTile(),
|
const BetaTimelineListTile(),
|
||||||
|
if (app_store.Store.isBetaTimelineEnabled)
|
||||||
|
SettingsCard(
|
||||||
|
icon: Icons.sync_outlined,
|
||||||
|
title: 'beta_sync'.tr(),
|
||||||
|
subtitle: 'beta_sync_subtitle'.tr(),
|
||||||
|
settingRoute: const BetaSyncSettingsRoute(),
|
||||||
|
),
|
||||||
...settings,
|
...settings,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
29
mobile/lib/pages/settings/beta_sync_settings.page.dart
Normal file
29
mobile/lib/pages/settings/beta_sync_settings.page.dart
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/beta_sync_settings/beta_sync_settings.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class BetaSyncSettingsPage extends StatelessWidget {
|
||||||
|
const BetaSyncSettingsPage({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
title: const Text("beta_sync").t(context: context),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () => context.maybePop(true),
|
||||||
|
splashRadius: 24,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.arrow_back_ios_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: const BetaSyncSettings(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,12 @@ final backgroundSyncProvider = Provider<BackgroundSyncManager>((ref) {
|
|||||||
onRemoteSyncStart: syncStatusNotifier.startRemoteSync,
|
onRemoteSyncStart: syncStatusNotifier.startRemoteSync,
|
||||||
onRemoteSyncComplete: syncStatusNotifier.completeRemoteSync,
|
onRemoteSyncComplete: syncStatusNotifier.completeRemoteSync,
|
||||||
onRemoteSyncError: syncStatusNotifier.errorRemoteSync,
|
onRemoteSyncError: syncStatusNotifier.errorRemoteSync,
|
||||||
|
onLocalSyncStart: syncStatusNotifier.startLocalSync,
|
||||||
|
onLocalSyncComplete: syncStatusNotifier.completeLocalSync,
|
||||||
|
onLocalSyncError: syncStatusNotifier.errorLocalSync,
|
||||||
|
onHashingStart: syncStatusNotifier.startHashJob,
|
||||||
|
onHashingComplete: syncStatusNotifier.completeHashJob,
|
||||||
|
onHashingError: syncStatusNotifier.errorHashJob,
|
||||||
);
|
);
|
||||||
ref.onDispose(manager.cancel);
|
ref.onDispose(manager.cancel);
|
||||||
return manager;
|
return manager;
|
||||||
|
@ -1,43 +1,71 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
enum SyncStatus {
|
enum SyncStatus {
|
||||||
idle,
|
idle,
|
||||||
syncing,
|
syncing,
|
||||||
success,
|
success,
|
||||||
error,
|
error;
|
||||||
|
|
||||||
|
localized() {
|
||||||
|
return switch (this) {
|
||||||
|
SyncStatus.idle => "idle".tr(),
|
||||||
|
SyncStatus.syncing => "running".tr(),
|
||||||
|
SyncStatus.success => "success".tr(),
|
||||||
|
SyncStatus.error => "error".tr()
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SyncStatusState {
|
class SyncStatusState {
|
||||||
final SyncStatus remoteSyncStatus;
|
final SyncStatus remoteSyncStatus;
|
||||||
|
final SyncStatus localSyncStatus;
|
||||||
|
final SyncStatus hashJobStatus;
|
||||||
|
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
const SyncStatusState({
|
const SyncStatusState({
|
||||||
this.remoteSyncStatus = SyncStatus.idle,
|
this.remoteSyncStatus = SyncStatus.idle,
|
||||||
|
this.localSyncStatus = SyncStatus.idle,
|
||||||
|
this.hashJobStatus = SyncStatus.idle,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
SyncStatusState copyWith({
|
SyncStatusState copyWith({
|
||||||
SyncStatus? remoteSyncStatus,
|
SyncStatus? remoteSyncStatus,
|
||||||
|
SyncStatus? localSyncStatus,
|
||||||
|
SyncStatus? hashJobStatus,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
}) {
|
}) {
|
||||||
return SyncStatusState(
|
return SyncStatusState(
|
||||||
remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus,
|
remoteSyncStatus: remoteSyncStatus ?? this.remoteSyncStatus,
|
||||||
|
localSyncStatus: localSyncStatus ?? this.localSyncStatus,
|
||||||
|
hashJobStatus: hashJobStatus ?? this.hashJobStatus,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing;
|
bool get isRemoteSyncing => remoteSyncStatus == SyncStatus.syncing;
|
||||||
|
bool get isLocalSyncing => localSyncStatus == SyncStatus.syncing;
|
||||||
|
bool get isHashing => hashJobStatus == SyncStatus.syncing;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
if (identical(this, other)) return true;
|
if (identical(this, other)) return true;
|
||||||
return other is SyncStatusState &&
|
return other is SyncStatusState &&
|
||||||
other.remoteSyncStatus == remoteSyncStatus &&
|
other.remoteSyncStatus == remoteSyncStatus &&
|
||||||
|
other.localSyncStatus == localSyncStatus &&
|
||||||
|
other.hashJobStatus == hashJobStatus &&
|
||||||
other.errorMessage == errorMessage;
|
other.errorMessage == errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(remoteSyncStatus, errorMessage);
|
int get hashCode => Object.hash(
|
||||||
|
remoteSyncStatus,
|
||||||
|
localSyncStatus,
|
||||||
|
hashJobStatus,
|
||||||
|
errorMessage,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class SyncStatusNotifier extends Notifier<SyncStatusState> {
|
class SyncStatusNotifier extends Notifier<SyncStatusState> {
|
||||||
@ -46,9 +74,15 @@ class SyncStatusNotifier extends Notifier<SyncStatusState> {
|
|||||||
return const SyncStatusState(
|
return const SyncStatusState(
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
remoteSyncStatus: SyncStatus.idle,
|
remoteSyncStatus: SyncStatus.idle,
|
||||||
|
localSyncStatus: SyncStatus.idle,
|
||||||
|
hashJobStatus: SyncStatus.idle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Remote Sync
|
||||||
|
///
|
||||||
|
|
||||||
void setRemoteSyncStatus(SyncStatus status, [String? errorMessage]) {
|
void setRemoteSyncStatus(SyncStatus status, [String? errorMessage]) {
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
remoteSyncStatus: status,
|
remoteSyncStatus: status,
|
||||||
@ -60,6 +94,37 @@ class SyncStatusNotifier extends Notifier<SyncStatusState> {
|
|||||||
void completeRemoteSync() => setRemoteSyncStatus(SyncStatus.success);
|
void completeRemoteSync() => setRemoteSyncStatus(SyncStatus.success);
|
||||||
void errorRemoteSync(String error) =>
|
void errorRemoteSync(String error) =>
|
||||||
setRemoteSyncStatus(SyncStatus.error, error);
|
setRemoteSyncStatus(SyncStatus.error, error);
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Local Sync
|
||||||
|
///
|
||||||
|
|
||||||
|
void setLocalSyncStatus(SyncStatus status, [String? errorMessage]) {
|
||||||
|
state = state.copyWith(
|
||||||
|
localSyncStatus: status,
|
||||||
|
errorMessage: status == SyncStatus.error ? errorMessage : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startLocalSync() => setLocalSyncStatus(SyncStatus.syncing);
|
||||||
|
void completeLocalSync() => setLocalSyncStatus(SyncStatus.success);
|
||||||
|
void errorLocalSync(String error) =>
|
||||||
|
setLocalSyncStatus(SyncStatus.error, error);
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Hash Job
|
||||||
|
///
|
||||||
|
|
||||||
|
void setHashJobStatus(SyncStatus status, [String? errorMessage]) {
|
||||||
|
state = state.copyWith(
|
||||||
|
hashJobStatus: status,
|
||||||
|
errorMessage: status == SyncStatus.error ? errorMessage : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void startHashJob() => setHashJobStatus(SyncStatus.syncing);
|
||||||
|
void completeHashJob() => setHashJobStatus(SyncStatus.success);
|
||||||
|
void errorHashJob(String error) => setHashJobStatus(SyncStatus.error, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
final syncStatusProvider =
|
final syncStatusProvider =
|
||||||
|
@ -73,6 +73,7 @@ import 'package:immich_mobile/pages/search/map/map_location_picker.page.dart';
|
|||||||
import 'package:immich_mobile/pages/search/person_result.page.dart';
|
import 'package:immich_mobile/pages/search/person_result.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/settings/beta_sync_settings.page.dart';
|
||||||
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
||||||
@ -481,7 +482,6 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: DriftUserSelectionRoute.page,
|
page: DriftUserSelectionRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
|
||||||
AutoRoute(
|
AutoRoute(
|
||||||
page: ChangeExperienceRoute.page,
|
page: ChangeExperienceRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
@ -495,6 +495,10 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: DriftUploadDetailRoute.page,
|
page: DriftUploadDetailRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: BetaSyncSettingsRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
@ -503,6 +503,22 @@ class BackupOptionsRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [BetaSyncSettingsPage]
|
||||||
|
class BetaSyncSettingsRoute extends PageRouteInfo<void> {
|
||||||
|
const BetaSyncSettingsRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(BetaSyncSettingsRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'BetaSyncSettingsRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const BetaSyncSettingsPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [ChangeExperiencePage]
|
/// [ChangeExperiencePage]
|
||||||
class ChangeExperienceRoute extends PageRouteInfo<ChangeExperienceRouteArgs> {
|
class ChangeExperienceRoute extends PageRouteInfo<ChangeExperienceRouteArgs> {
|
||||||
|
@ -0,0 +1,348 @@
|
|||||||
|
import 'package:drift/drift.dart' as drift_db;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/sync_status.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/settings/beta_sync_settings/entity_count_tile.dart';
|
||||||
|
|
||||||
|
class BetaSyncSettings extends HookConsumerWidget {
|
||||||
|
const BetaSyncSettings({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final assetService = ref.watch(assetServiceProvider);
|
||||||
|
final localAlbumService = ref.watch(localAlbumServiceProvider);
|
||||||
|
final remoteAlbumService = ref.watch(remoteAlbumServiceProvider);
|
||||||
|
final memoryService = ref.watch(driftMemoryServiceProvider);
|
||||||
|
|
||||||
|
Future<List<dynamic>> loadCounts() async {
|
||||||
|
final assetCounts = assetService.getAssetCounts();
|
||||||
|
final localAlbumCounts = localAlbumService.getCount();
|
||||||
|
final remoteAlbumCounts = remoteAlbumService.getCount();
|
||||||
|
final memoryCount = memoryService.getCount();
|
||||||
|
final getLocalHashedCount = assetService.getLocalHashedCount();
|
||||||
|
|
||||||
|
return await Future.wait([
|
||||||
|
assetCounts,
|
||||||
|
localAlbumCounts,
|
||||||
|
remoteAlbumCounts,
|
||||||
|
memoryCount,
|
||||||
|
getLocalHashedCount,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> resetDatabase() async {
|
||||||
|
// https://github.com/simolus3/drift/commit/bd80a46264b6dd833ef4fd87fffc03f5a832ab41#diff-3f879e03b4a35779344ef16170b9353608dd9c42385f5402ec6035aac4dd8a04R76-R94
|
||||||
|
final drift = ref.read(driftProvider);
|
||||||
|
final database = drift.attachedDatabase;
|
||||||
|
await database.exclusively(() async {
|
||||||
|
// https://stackoverflow.com/a/65743498/25690041
|
||||||
|
await database.customStatement('PRAGMA writable_schema = 1;');
|
||||||
|
await database.customStatement('DELETE FROM sqlite_master;');
|
||||||
|
await database.customStatement('VACUUM;');
|
||||||
|
await database.customStatement('PRAGMA writable_schema = 0;');
|
||||||
|
await database.customStatement('PRAGMA integrity_check');
|
||||||
|
|
||||||
|
await database.customStatement('PRAGMA user_version = 0');
|
||||||
|
await database.beforeOpen(
|
||||||
|
// ignore: invalid_use_of_internal_member
|
||||||
|
database.resolvedEngine.executor,
|
||||||
|
drift_db.OpeningDetails(null, database.schemaVersion),
|
||||||
|
);
|
||||||
|
await database.customStatement(
|
||||||
|
'PRAGMA user_version = ${database.schemaVersion}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Refresh all stream queries
|
||||||
|
database.notifyUpdates({
|
||||||
|
for (final table in database.allTables)
|
||||||
|
drift_db.TableUpdate.onTable(table),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return FutureBuilder<List<dynamic>>(
|
||||||
|
future: loadCounts(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return const CircularProgressIndicator();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCounts = snapshot.data![0]! as (int, int);
|
||||||
|
final localAssetCount = assetCounts.$1;
|
||||||
|
final remoteAssetCount = assetCounts.$2;
|
||||||
|
|
||||||
|
final localAlbumCount = snapshot.data![1]! as int;
|
||||||
|
final remoteAlbumCount = snapshot.data![2]! as int;
|
||||||
|
final memoryCount = snapshot.data![3]! as int;
|
||||||
|
final localHashedCount = snapshot.data![4]! as int;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16, bottom: 32),
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
_SectionHeaderText(text: "assets".t(context: context)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: Flex(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
spacing: 8.0,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: EntitiyCountTile(
|
||||||
|
label: "local".t(context: context),
|
||||||
|
count: localAssetCount,
|
||||||
|
icon: Icons.smartphone,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: EntitiyCountTile(
|
||||||
|
label: "remote".t(context: context),
|
||||||
|
count: remoteAssetCount,
|
||||||
|
icon: Icons.cloud,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_SectionHeaderText(text: "albums".t(context: context)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: Flex(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
spacing: 8.0,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: EntitiyCountTile(
|
||||||
|
label: "local".t(context: context),
|
||||||
|
count: localAlbumCount,
|
||||||
|
icon: Icons.smartphone,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: EntitiyCountTile(
|
||||||
|
label: "remote".t(context: context),
|
||||||
|
count: remoteAlbumCount,
|
||||||
|
icon: Icons.cloud,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_SectionHeaderText(text: "other".t(context: context)),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: Flex(
|
||||||
|
direction: Axis.horizontal,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
spacing: 8.0,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: EntitiyCountTile(
|
||||||
|
label: "memories".t(context: context),
|
||||||
|
count: memoryCount,
|
||||||
|
icon: Icons.calendar_today,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: EntitiyCountTile(
|
||||||
|
label: "hashed_assets".t(context: context),
|
||||||
|
count: localHashedCount,
|
||||||
|
icon: Icons.tag,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(
|
||||||
|
height: 1,
|
||||||
|
indent: 16,
|
||||||
|
endIndent: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_SectionHeaderText(text: "jobs".t(context: context)),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"sync_local".t(context: context),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"tap_to_run_job".t(context: context),
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.sync),
|
||||||
|
trailing: _SyncStatusIcon(
|
||||||
|
status: ref.watch(syncStatusProvider).localSyncStatus,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
ref.read(backgroundSyncProvider).syncLocal(full: true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"sync_remote".t(context: context),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
"tap_to_run_job".t(context: context),
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.cloud_sync),
|
||||||
|
trailing: _SyncStatusIcon(
|
||||||
|
status: ref.watch(syncStatusProvider).remoteSyncStatus,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
ref.read(backgroundSyncProvider).syncRemote();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"hash_asset".t(context: context),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: const Icon(Icons.tag),
|
||||||
|
subtitle: Text(
|
||||||
|
"tap_to_run_job".t(context: context),
|
||||||
|
),
|
||||||
|
trailing: _SyncStatusIcon(
|
||||||
|
status: ref.watch(syncStatusProvider).hashJobStatus,
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
ref.read(backgroundSyncProvider).hashAssets();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const Divider(
|
||||||
|
height: 1,
|
||||||
|
indent: 16,
|
||||||
|
endIndent: 16,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
_SectionHeaderText(text: "actions".t(context: context)),
|
||||||
|
ListTile(
|
||||||
|
title: Text(
|
||||||
|
"reset_sqlite".t(context: context),
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.colorScheme.error,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
leading: Icon(
|
||||||
|
Icons.settings_backup_restore_rounded,
|
||||||
|
color: context.colorScheme.error,
|
||||||
|
),
|
||||||
|
onTap: () async {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(
|
||||||
|
"reset_sqlite".t(context: context),
|
||||||
|
),
|
||||||
|
content: Text(
|
||||||
|
"reset_sqlite_confirmation".t(context: context),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => context.pop(),
|
||||||
|
child: Text("cancel".t(context: context)),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
await resetDatabase();
|
||||||
|
context.pop();
|
||||||
|
context.scaffoldMessenger.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"reset_sqlite_success".t(context: context),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
"confirm".t(context: context),
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.colorScheme.error,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SyncStatusIcon extends StatelessWidget {
|
||||||
|
final SyncStatus status;
|
||||||
|
|
||||||
|
const _SyncStatusIcon({
|
||||||
|
required this.status,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return switch (status) {
|
||||||
|
SyncStatus.idle => const Icon(
|
||||||
|
Icons.pause_circle_outline_rounded,
|
||||||
|
),
|
||||||
|
SyncStatus.syncing => const SizedBox(
|
||||||
|
height: 24,
|
||||||
|
width: 24,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SyncStatus.success => const Icon(
|
||||||
|
Icons.check_circle_outline,
|
||||||
|
color: Colors.green,
|
||||||
|
),
|
||||||
|
SyncStatus.error => Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
color: context.colorScheme.error,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SectionHeaderText extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const _SectionHeaderText({
|
||||||
|
required this.text,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0),
|
||||||
|
child: Text(
|
||||||
|
text.toUpperCase(),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: context.colorScheme.onSurface.withAlpha(200),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,99 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
|
||||||
|
class EntitiyCountTile extends StatelessWidget {
|
||||||
|
final int count;
|
||||||
|
final String label;
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
const EntitiyCountTile({
|
||||||
|
super.key,
|
||||||
|
required this.count,
|
||||||
|
required this.label,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
String zeroPadding(int number, int targetWidth) {
|
||||||
|
final numStr = number.toString();
|
||||||
|
return numStr.length < targetWidth
|
||||||
|
? "0" * (targetWidth - numStr.length)
|
||||||
|
: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
int calculateMaxDigits(double availableWidth) {
|
||||||
|
const double charWidth = 11.0;
|
||||||
|
return (availableWidth / charWidth).floor().clamp(1, 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.surfaceContainerLow,
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
|
border: Border.all(
|
||||||
|
width: 0.5,
|
||||||
|
color: context.colorScheme.outline.withAlpha(25),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Icon and Label
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.primaryColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
// Number
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final maxDigits = calculateMaxDigits(constraints.maxWidth);
|
||||||
|
return RichText(
|
||||||
|
text: TextSpan(
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontFamily: 'OverpassMono',
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
TextSpan(
|
||||||
|
text: zeroPadding(count, maxDigits),
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.colorScheme.onSurfaceSecondary
|
||||||
|
.withAlpha(75),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: count.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
mobile/lib/widgets/settings/settings_card.dart
Normal file
63
mobile/lib/widgets/settings/settings_card.dart
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
|
class SettingsCard extends StatelessWidget {
|
||||||
|
const SettingsCard({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.settingRoute,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final PageRouteInfo settingRoute;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
child: Card(
|
||||||
|
elevation: 0,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
color: context.colorScheme.surfaceContainer,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
),
|
||||||
|
leading: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(16)),
|
||||||
|
color: context.isDarkTheme
|
||||||
|
? Colors.black26
|
||||||
|
: Colors.white.withAlpha(100),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Icon(icon, color: context.primaryColor),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: context.textTheme.titleMedium!.copyWith(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
subtitle,
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
onTap: () => context.pushRoute(settingRoute),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user