mirror of
https://github.com/immich-app/immich.git
synced 2025-05-24 01:12:58 -04:00
feat: locked view mobile (#18316)
* feat: locked/private view * feat: locked/private view * feat: mobile lock/private view * feat: mobile lock/private view * merge main * pr feedback * pr feedback * bottom sheet sizing * always lock when navigating away
This commit is contained in:
parent
397808dd1a
commit
fe71894308
13
i18n/en.json
13
i18n/en.json
@ -563,6 +563,10 @@
|
|||||||
"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",
|
||||||
|
"biometric_auth_enabled": "Biometric authentication enabled",
|
||||||
|
"biometric_locked_out": "You are locked out of biometric authentication",
|
||||||
|
"biometric_no_options": "No biometric options available",
|
||||||
|
"biometric_not_available": "Biometric authentication is not available on this device",
|
||||||
"birthdate_saved": "Date of birth saved successfully",
|
"birthdate_saved": "Date of birth saved successfully",
|
||||||
"birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
|
"birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
|
||||||
"blurred_background": "Blurred background",
|
"blurred_background": "Blurred background",
|
||||||
@ -822,6 +826,7 @@
|
|||||||
"empty_trash": "Empty trash",
|
"empty_trash": "Empty trash",
|
||||||
"empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!",
|
"empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!",
|
||||||
"enable": "Enable",
|
"enable": "Enable",
|
||||||
|
"enable_biometric_auth_description": "Enter your PIN code to enable biometric authentication",
|
||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"end_date": "End date",
|
"end_date": "End date",
|
||||||
"enqueued": "Enqueued",
|
"enqueued": "Enqueued",
|
||||||
@ -995,6 +1000,7 @@
|
|||||||
"external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
"external_network_sheet_info": "When not on the preferred Wi-Fi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
||||||
"face_unassigned": "Unassigned",
|
"face_unassigned": "Unassigned",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
|
"failed_to_authenticate": "Failed to authenticate",
|
||||||
"failed_to_load_assets": "Failed to load assets",
|
"failed_to_load_assets": "Failed to load assets",
|
||||||
"failed_to_load_folder": "Failed to load folder",
|
"failed_to_load_folder": "Failed to load folder",
|
||||||
"favorite": "Favorite",
|
"favorite": "Favorite",
|
||||||
@ -1060,6 +1066,8 @@
|
|||||||
"home_page_favorite_err_local": "Can not favorite local assets yet, 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_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 so that the timeline can populate photos and videos in it",
|
"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_locked_error_local": "Can not move local assets to locked folder, skipping",
|
||||||
|
"home_page_locked_error_partner": "Can not move partner assets to locked folder, skipping",
|
||||||
"home_page_share_err_local": "Can not share local assets via link, skipping",
|
"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",
|
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
|
||||||
"host": "Host",
|
"host": "Host",
|
||||||
@ -1227,8 +1235,6 @@
|
|||||||
"memories_setting_description": "Manage what you see in your memories",
|
"memories_setting_description": "Manage what you see in your memories",
|
||||||
"memories_start_over": "Start Over",
|
"memories_start_over": "Start Over",
|
||||||
"memories_swipe_to_close": "Swipe up to close",
|
"memories_swipe_to_close": "Swipe up to close",
|
||||||
"memories_year_ago": "A year ago",
|
|
||||||
"memories_years_ago": "{years, plural, other {# years}} ago",
|
|
||||||
"memory": "Memory",
|
"memory": "Memory",
|
||||||
"memory_lane_title": "Memory Lane {title}",
|
"memory_lane_title": "Memory Lane {title}",
|
||||||
"menu": "Menu",
|
"menu": "Menu",
|
||||||
@ -1400,6 +1406,7 @@
|
|||||||
"play_memories": "Play memories",
|
"play_memories": "Play memories",
|
||||||
"play_motion_photo": "Play Motion Photo",
|
"play_motion_photo": "Play Motion Photo",
|
||||||
"play_or_pause_video": "Play or pause video",
|
"play_or_pause_video": "Play or pause video",
|
||||||
|
"please_auth_to_access": "Please authenticate to access",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"preferences_settings_subtitle": "Manage the app's preferences",
|
"preferences_settings_subtitle": "Manage the app's preferences",
|
||||||
"preferences_settings_title": "Preferences",
|
"preferences_settings_title": "Preferences",
|
||||||
@ -1661,6 +1668,7 @@
|
|||||||
"share_add_photos": "Add photos",
|
"share_add_photos": "Add photos",
|
||||||
"share_assets_selected": "{count} selected",
|
"share_assets_selected": "{count} selected",
|
||||||
"share_dialog_preparing": "Preparing...",
|
"share_dialog_preparing": "Preparing...",
|
||||||
|
"share_link": "Share Link",
|
||||||
"shared": "Shared",
|
"shared": "Shared",
|
||||||
"shared_album_activities_input_disable": "Comment is disabled",
|
"shared_album_activities_input_disable": "Comment is disabled",
|
||||||
"shared_album_activity_remove_content": "Do you want to delete this activity?",
|
"shared_album_activity_remove_content": "Do you want to delete this activity?",
|
||||||
@ -1884,6 +1892,7 @@
|
|||||||
"uploading": "Uploading",
|
"uploading": "Uploading",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"usage": "Usage",
|
"usage": "Usage",
|
||||||
|
"use_biometric": "Use biometric",
|
||||||
"use_current_connection": "use current connection",
|
"use_current_connection": "use current connection",
|
||||||
"use_custom_date_range": "Use custom date range instead",
|
"use_custom_date_range": "Use custom date range instead",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||||
|
|
||||||
<!-- Foreground service permission -->
|
<!-- Foreground service permission -->
|
||||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
package app.alextran.immich
|
package app.alextran.immich
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
|
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterFragmentActivity() {
|
||||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
flutterEngine.plugins.add(BackgroundServicePlugin())
|
flutterEngine.plugins.add(BackgroundServicePlugin())
|
||||||
flutterEngine.plugins.add(HttpSSLOptionsPlugin())
|
flutterEngine.plugins.add(HttpSSLOptionsPlugin())
|
||||||
// No need to set up method channel here as it's now handled in the plugin
|
// No need to set up method channel here as it's now handled in the plugin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode
|
||||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
setting is off -->
|
||||||
<!-- Show a splash screen on the activity. Automatically removed when
|
<style name="LaunchTheme" parent="Theme.AppCompat.DayNight">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
Flutter draws its first frame -->
|
Flutter draws its first frame -->
|
||||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
<item name="android:forceDarkAllowed">false</item>
|
<item name="android:forceDarkAllowed">false</item>
|
||||||
<item name="android:windowFullscreen">false</item>
|
<item name="android:windowFullscreen">false</item>
|
||||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||||
</style>
|
</style>
|
||||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
This theme determines the color of the Android Window while your
|
This theme determines the color of the Android Window while your
|
||||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
running.
|
running.
|
||||||
|
|
||||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
<item name="android:windowBackground">?android:colorBackground</item>
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
@ -44,6 +44,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- flutter_native_splash (2.4.3):
|
- flutter_native_splash (2.4.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- flutter_secure_storage (6.0.0):
|
||||||
|
- Flutter
|
||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
@ -59,6 +61,9 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- isar_flutter_libs (1.0.0):
|
- isar_flutter_libs (1.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- local_auth_darwin (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
- FlutterMacOS
|
||||||
- MapLibre (6.5.0)
|
- MapLibre (6.5.0)
|
||||||
- maplibre_gl (0.0.1):
|
- maplibre_gl (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
@ -130,6 +135,7 @@ DEPENDENCIES:
|
|||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||||
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
|
||||||
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
|
||||||
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
|
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
|
||||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||||
@ -137,6 +143,7 @@ DEPENDENCIES:
|
|||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||||
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
|
- isar_flutter_libs (from `.symlinks/plugins/isar_flutter_libs/ios`)
|
||||||
|
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
|
||||||
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
|
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
|
||||||
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
|
||||||
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
|
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
|
||||||
@ -178,6 +185,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||||
flutter_native_splash:
|
flutter_native_splash:
|
||||||
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
:path: ".symlinks/plugins/flutter_native_splash/ios"
|
||||||
|
flutter_secure_storage:
|
||||||
|
:path: ".symlinks/plugins/flutter_secure_storage/ios"
|
||||||
flutter_udid:
|
flutter_udid:
|
||||||
:path: ".symlinks/plugins/flutter_udid/ios"
|
:path: ".symlinks/plugins/flutter_udid/ios"
|
||||||
flutter_web_auth_2:
|
flutter_web_auth_2:
|
||||||
@ -192,6 +201,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/integration_test/ios"
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
isar_flutter_libs:
|
isar_flutter_libs:
|
||||||
:path: ".symlinks/plugins/isar_flutter_libs/ios"
|
:path: ".symlinks/plugins/isar_flutter_libs/ios"
|
||||||
|
local_auth_darwin:
|
||||||
|
:path: ".symlinks/plugins/local_auth_darwin/darwin"
|
||||||
maplibre_gl:
|
maplibre_gl:
|
||||||
:path: ".symlinks/plugins/maplibre_gl/ios"
|
:path: ".symlinks/plugins/maplibre_gl/ios"
|
||||||
native_video_player:
|
native_video_player:
|
||||||
@ -233,6 +244,7 @@ SPEC CHECKSUMS:
|
|||||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||||
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
|
||||||
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
|
||||||
|
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
|
||||||
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
|
||||||
flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80
|
flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80
|
||||||
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
|
||||||
@ -240,6 +252,7 @@ SPEC CHECKSUMS:
|
|||||||
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
|
||||||
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
|
||||||
isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26
|
isar_flutter_libs: bc909e72c3d756c2759f14c8776c13b5b0556e26
|
||||||
|
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
|
||||||
MapLibre: 0ebfa9329d313cec8bf0a5ba5a336a1dc903785e
|
MapLibre: 0ebfa9329d313cec8bf0a5ba5a336a1dc903785e
|
||||||
maplibre_gl: eab61cca6e1cfa9187249bacd3f08b51e8cd8ae9
|
maplibre_gl: eab61cca6e1cfa9187249bacd3f08b51e8cd8ae9
|
||||||
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
|
||||||
|
@ -1,165 +1,167 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>AppGroupId</key>
|
<key>AppGroupId</key>
|
||||||
<string>$(CUSTOM_GROUP_ID)</string>
|
<string>$(CUSTOM_GROUP_ID)</string>
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>app.alextran.immich.backgroundFetch</string>
|
<string>app.alextran.immich.backgroundFetch</string>
|
||||||
<string>app.alextran.immich.backgroundProcessing</string>
|
<string>app.alextran.immich.backgroundProcessing</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>${PRODUCT_NAME}</string>
|
<string>${PRODUCT_NAME}</string>
|
||||||
<key>CFBundleDocumentTypes</key>
|
<key>CFBundleDocumentTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeName</key>
|
<key>CFBundleTypeName</key>
|
||||||
<string>ShareHandler</string>
|
<string>ShareHandler</string>
|
||||||
<key>LSHandlerRank</key>
|
<key>LSHandlerRank</key>
|
||||||
<string>Alternate</string>
|
<string>Alternate</string>
|
||||||
<key>LSItemContentTypes</key>
|
<key>LSItemContentTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>public.file-url</string>
|
<string>public.file-url</string>
|
||||||
<string>public.image</string>
|
<string>public.image</string>
|
||||||
<string>public.text</string>
|
<string>public.text</string>
|
||||||
<string>public.movie</string>
|
<string>public.movie</string>
|
||||||
<string>public.url</string>
|
<string>public.url</string>
|
||||||
<string>public.data</string>
|
<string>public.data</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<string>ar</string>
|
<string>ar</string>
|
||||||
<string>ca</string>
|
<string>ca</string>
|
||||||
<string>cs</string>
|
<string>cs</string>
|
||||||
<string>da</string>
|
<string>da</string>
|
||||||
<string>de</string>
|
<string>de</string>
|
||||||
<string>es</string>
|
<string>es</string>
|
||||||
<string>fi</string>
|
<string>fi</string>
|
||||||
<string>fr</string>
|
<string>fr</string>
|
||||||
<string>he</string>
|
<string>he</string>
|
||||||
<string>hi</string>
|
<string>hi</string>
|
||||||
<string>hu</string>
|
<string>hu</string>
|
||||||
<string>it</string>
|
<string>it</string>
|
||||||
<string>ja</string>
|
<string>ja</string>
|
||||||
<string>ko</string>
|
<string>ko</string>
|
||||||
<string>lv</string>
|
<string>lv</string>
|
||||||
<string>mn</string>
|
<string>mn</string>
|
||||||
<string>nb</string>
|
<string>nb</string>
|
||||||
<string>nl</string>
|
<string>nl</string>
|
||||||
<string>pl</string>
|
<string>pl</string>
|
||||||
<string>pt</string>
|
<string>pt</string>
|
||||||
<string>ro</string>
|
<string>ro</string>
|
||||||
<string>ru</string>
|
<string>ru</string>
|
||||||
<string>sk</string>
|
<string>sk</string>
|
||||||
<string>sl</string>
|
<string>sl</string>
|
||||||
<string>sr</string>
|
<string>sr</string>
|
||||||
<string>sv</string>
|
<string>sv</string>
|
||||||
<string>th</string>
|
<string>th</string>
|
||||||
<string>uk</string>
|
<string>uk</string>
|
||||||
<string>vi</string>
|
<string>vi</string>
|
||||||
<string>zh</string>
|
<string>zh</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>immich_mobile</string>
|
<string>immich_mobile</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.132.3</string>
|
<string>1.132.3</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleTypeRole</key>
|
<key>CFBundleTypeRole</key>
|
||||||
<string>Editor</string>
|
<string>Editor</string>
|
||||||
<key>CFBundleURLSchemes</key>
|
<key>CFBundleURLSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>205</string>
|
<string>205</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false />
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>https</string>
|
<string>https</string>
|
||||||
</array>
|
</array>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||||
<string>No</string>
|
<string>No</string>
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>NSAppTransportSecurity</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true />
|
||||||
</dict>
|
</dict>
|
||||||
<key>NSCameraUsageDescription</key>
|
<key>NSCameraUsageDescription</key>
|
||||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
<string>We require this permission to access the local WiFi name for background upload mechanism</string>
|
||||||
<key>NSLocationUsageDescription</key>
|
<key>NSLocationUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name</string>
|
<string>We require this permission to access the local WiFi name</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>We require this permission to access the local WiFi name</string>
|
<string>We require this permission to access the local WiFi name</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>NSUserActivityTypes</key>
|
<key>NSUserActivityTypes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>INSendMessageIntent</string>
|
<string>INSendMessageIntent</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>fetch</string>
|
<string>fetch</string>
|
||||||
<string>processing</string>
|
<string>processing</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>Main</string>
|
<string>Main</string>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIStatusBarHidden</key>
|
||||||
<false/>
|
<false />
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
<key>io.flutter.embedded_views_preview</key>
|
||||||
<true/>
|
<true />
|
||||||
</dict>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
|
<string>We need to use FaceID to allow access to your locked folder</string>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
@ -11,3 +11,6 @@ const int kSyncEventBatchSize = 5000;
|
|||||||
// Hash batch limits
|
// Hash batch limits
|
||||||
const int kBatchHashFileLimit = 128;
|
const int kBatchHashFileLimit = 128;
|
||||||
const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
|
const int kBatchHashSizeLimit = 1024 * 1024 * 1024; // 1GB
|
||||||
|
|
||||||
|
// Secure storage keys
|
||||||
|
const String kSecuredPinCode = "secured_pin_code";
|
||||||
|
@ -8,3 +8,5 @@ enum TextSearchType {
|
|||||||
filename,
|
filename,
|
||||||
description,
|
description,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
||||||
@ -45,7 +46,8 @@ class Asset {
|
|||||||
: remote.stack?.primaryAssetId,
|
: remote.stack?.primaryAssetId,
|
||||||
stackCount = remote.stack?.assetCount ?? 0,
|
stackCount = remote.stack?.assetCount ?? 0,
|
||||||
stackId = remote.stack?.id,
|
stackId = remote.stack?.id,
|
||||||
thumbhash = remote.thumbhash;
|
thumbhash = remote.thumbhash,
|
||||||
|
visibility = getVisibility(remote.visibility);
|
||||||
|
|
||||||
Asset({
|
Asset({
|
||||||
this.id = Isar.autoIncrement,
|
this.id = Isar.autoIncrement,
|
||||||
@ -71,6 +73,7 @@ class Asset {
|
|||||||
this.stackCount = 0,
|
this.stackCount = 0,
|
||||||
this.isOffline = false,
|
this.isOffline = false,
|
||||||
this.thumbhash,
|
this.thumbhash,
|
||||||
|
this.visibility = AssetVisibilityEnum.timeline,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ignore
|
@ignore
|
||||||
@ -173,6 +176,9 @@ class Asset {
|
|||||||
|
|
||||||
int stackCount;
|
int stackCount;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.ordinal)
|
||||||
|
AssetVisibilityEnum visibility;
|
||||||
|
|
||||||
/// Returns null if the asset has no sync access to the exif info
|
/// Returns null if the asset has no sync access to the exif info
|
||||||
@ignore
|
@ignore
|
||||||
double? get aspectRatio {
|
double? get aspectRatio {
|
||||||
@ -349,7 +355,8 @@ class Asset {
|
|||||||
a.thumbhash != thumbhash ||
|
a.thumbhash != thumbhash ||
|
||||||
stackId != a.stackId ||
|
stackId != a.stackId ||
|
||||||
stackCount != a.stackCount ||
|
stackCount != a.stackCount ||
|
||||||
stackPrimaryAssetId == null && a.stackPrimaryAssetId != null;
|
stackPrimaryAssetId == null && a.stackPrimaryAssetId != null ||
|
||||||
|
visibility != a.visibility;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a new [Asset] with values from this and merged & updated with [a]
|
/// Returns a new [Asset] with values from this and merged & updated with [a]
|
||||||
@ -452,6 +459,7 @@ class Asset {
|
|||||||
String? stackPrimaryAssetId,
|
String? stackPrimaryAssetId,
|
||||||
int? stackCount,
|
int? stackCount,
|
||||||
String? thumbhash,
|
String? thumbhash,
|
||||||
|
AssetVisibilityEnum? visibility,
|
||||||
}) =>
|
}) =>
|
||||||
Asset(
|
Asset(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@ -477,6 +485,7 @@ class Asset {
|
|||||||
stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId,
|
stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId,
|
||||||
stackCount: stackCount ?? this.stackCount,
|
stackCount: stackCount ?? this.stackCount,
|
||||||
thumbhash: thumbhash ?? this.thumbhash,
|
thumbhash: thumbhash ?? this.thumbhash,
|
||||||
|
visibility: visibility ?? this.visibility,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> put(Isar db) async {
|
Future<void> put(Isar db) async {
|
||||||
@ -541,8 +550,22 @@ class Asset {
|
|||||||
"isArchived": $isArchived,
|
"isArchived": $isArchived,
|
||||||
"isTrashed": $isTrashed,
|
"isTrashed": $isTrashed,
|
||||||
"isOffline": $isOffline,
|
"isOffline": $isOffline,
|
||||||
|
"visibility": "$visibility",
|
||||||
}""";
|
}""";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getVisibility(AssetResponseDtoVisibilityEnum visibility) {
|
||||||
|
switch (visibility) {
|
||||||
|
case AssetResponseDtoVisibilityEnum.timeline:
|
||||||
|
return AssetVisibilityEnum.timeline;
|
||||||
|
case AssetResponseDtoVisibilityEnum.archive:
|
||||||
|
return AssetVisibilityEnum.archive;
|
||||||
|
case AssetResponseDtoVisibilityEnum.hidden:
|
||||||
|
return AssetVisibilityEnum.hidden;
|
||||||
|
case AssetResponseDtoVisibilityEnum.locked:
|
||||||
|
return AssetVisibilityEnum.locked;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum AssetType {
|
enum AssetType {
|
||||||
|
121
mobile/lib/entities/asset.entity.g.dart
generated
121
mobile/lib/entities/asset.entity.g.dart
generated
@ -118,8 +118,14 @@ const AssetSchema = CollectionSchema(
|
|||||||
name: r'updatedAt',
|
name: r'updatedAt',
|
||||||
type: IsarType.dateTime,
|
type: IsarType.dateTime,
|
||||||
),
|
),
|
||||||
r'width': PropertySchema(
|
r'visibility': PropertySchema(
|
||||||
id: 20,
|
id: 20,
|
||||||
|
name: r'visibility',
|
||||||
|
type: IsarType.byte,
|
||||||
|
enumMap: _AssetvisibilityEnumValueMap,
|
||||||
|
),
|
||||||
|
r'width': PropertySchema(
|
||||||
|
id: 21,
|
||||||
name: r'width',
|
name: r'width',
|
||||||
type: IsarType.int,
|
type: IsarType.int,
|
||||||
)
|
)
|
||||||
@ -256,7 +262,8 @@ void _assetSerialize(
|
|||||||
writer.writeString(offsets[17], object.thumbhash);
|
writer.writeString(offsets[17], object.thumbhash);
|
||||||
writer.writeByte(offsets[18], object.type.index);
|
writer.writeByte(offsets[18], object.type.index);
|
||||||
writer.writeDateTime(offsets[19], object.updatedAt);
|
writer.writeDateTime(offsets[19], object.updatedAt);
|
||||||
writer.writeInt(offsets[20], object.width);
|
writer.writeByte(offsets[20], object.visibility.index);
|
||||||
|
writer.writeInt(offsets[21], object.width);
|
||||||
}
|
}
|
||||||
|
|
||||||
Asset _assetDeserialize(
|
Asset _assetDeserialize(
|
||||||
@ -288,7 +295,10 @@ Asset _assetDeserialize(
|
|||||||
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ??
|
type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ??
|
||||||
AssetType.other,
|
AssetType.other,
|
||||||
updatedAt: reader.readDateTime(offsets[19]),
|
updatedAt: reader.readDateTime(offsets[19]),
|
||||||
width: reader.readIntOrNull(offsets[20]),
|
visibility:
|
||||||
|
_AssetvisibilityValueEnumMap[reader.readByteOrNull(offsets[20])] ??
|
||||||
|
AssetVisibilityEnum.timeline,
|
||||||
|
width: reader.readIntOrNull(offsets[21]),
|
||||||
);
|
);
|
||||||
return object;
|
return object;
|
||||||
}
|
}
|
||||||
@ -342,6 +352,9 @@ P _assetDeserializeProp<P>(
|
|||||||
case 19:
|
case 19:
|
||||||
return (reader.readDateTime(offset)) as P;
|
return (reader.readDateTime(offset)) as P;
|
||||||
case 20:
|
case 20:
|
||||||
|
return (_AssetvisibilityValueEnumMap[reader.readByteOrNull(offset)] ??
|
||||||
|
AssetVisibilityEnum.timeline) as P;
|
||||||
|
case 21:
|
||||||
return (reader.readIntOrNull(offset)) as P;
|
return (reader.readIntOrNull(offset)) as P;
|
||||||
default:
|
default:
|
||||||
throw IsarError('Unknown property with id $propertyId');
|
throw IsarError('Unknown property with id $propertyId');
|
||||||
@ -360,6 +373,18 @@ const _AssettypeValueEnumMap = {
|
|||||||
2: AssetType.video,
|
2: AssetType.video,
|
||||||
3: AssetType.audio,
|
3: AssetType.audio,
|
||||||
};
|
};
|
||||||
|
const _AssetvisibilityEnumValueMap = {
|
||||||
|
'timeline': 0,
|
||||||
|
'hidden': 1,
|
||||||
|
'archive': 2,
|
||||||
|
'locked': 3,
|
||||||
|
};
|
||||||
|
const _AssetvisibilityValueEnumMap = {
|
||||||
|
0: AssetVisibilityEnum.timeline,
|
||||||
|
1: AssetVisibilityEnum.hidden,
|
||||||
|
2: AssetVisibilityEnum.archive,
|
||||||
|
3: AssetVisibilityEnum.locked,
|
||||||
|
};
|
||||||
|
|
||||||
Id _assetGetId(Asset object) {
|
Id _assetGetId(Asset object) {
|
||||||
return object.id;
|
return object.id;
|
||||||
@ -2477,6 +2502,59 @@ extension AssetQueryFilter on QueryBuilder<Asset, Asset, QFilterCondition> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> visibilityEqualTo(
|
||||||
|
AssetVisibilityEnum value) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.equalTo(
|
||||||
|
property: r'visibility',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> visibilityGreaterThan(
|
||||||
|
AssetVisibilityEnum value, {
|
||||||
|
bool include = false,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||||
|
include: include,
|
||||||
|
property: r'visibility',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> visibilityLessThan(
|
||||||
|
AssetVisibilityEnum value, {
|
||||||
|
bool include = false,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.lessThan(
|
||||||
|
include: include,
|
||||||
|
property: r'visibility',
|
||||||
|
value: value,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> visibilityBetween(
|
||||||
|
AssetVisibilityEnum lower,
|
||||||
|
AssetVisibilityEnum upper, {
|
||||||
|
bool includeLower = true,
|
||||||
|
bool includeUpper = true,
|
||||||
|
}) {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addFilterCondition(FilterCondition.between(
|
||||||
|
property: r'visibility',
|
||||||
|
lower: lower,
|
||||||
|
includeLower: includeLower,
|
||||||
|
upper: upper,
|
||||||
|
includeUpper: includeUpper,
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QAfterFilterCondition> widthIsNull() {
|
QueryBuilder<Asset, Asset, QAfterFilterCondition> widthIsNull() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addFilterCondition(const FilterCondition.isNull(
|
return query.addFilterCondition(const FilterCondition.isNull(
|
||||||
@ -2791,6 +2869,18 @@ extension AssetQuerySortBy on QueryBuilder<Asset, Asset, QSortBy> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByVisibility() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'visibility', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByVisibilityDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'visibility', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QAfterSortBy> sortByWidth() {
|
QueryBuilder<Asset, Asset, QAfterSortBy> sortByWidth() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'width', Sort.asc);
|
return query.addSortBy(r'width', Sort.asc);
|
||||||
@ -3057,6 +3147,18 @@ extension AssetQuerySortThenBy on QueryBuilder<Asset, Asset, QSortThenBy> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByVisibility() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'visibility', Sort.asc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByVisibilityDesc() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addSortBy(r'visibility', Sort.desc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QAfterSortBy> thenByWidth() {
|
QueryBuilder<Asset, Asset, QAfterSortBy> thenByWidth() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addSortBy(r'width', Sort.asc);
|
return query.addSortBy(r'width', Sort.asc);
|
||||||
@ -3201,6 +3303,12 @@ extension AssetQueryWhereDistinct on QueryBuilder<Asset, Asset, QDistinct> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, Asset, QDistinct> distinctByVisibility() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addDistinctBy(r'visibility');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, Asset, QDistinct> distinctByWidth() {
|
QueryBuilder<Asset, Asset, QDistinct> distinctByWidth() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addDistinctBy(r'width');
|
return query.addDistinctBy(r'width');
|
||||||
@ -3335,6 +3443,13 @@ extension AssetQueryProperty on QueryBuilder<Asset, Asset, QQueryProperty> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueryBuilder<Asset, AssetVisibilityEnum, QQueryOperations>
|
||||||
|
visibilityProperty() {
|
||||||
|
return QueryBuilder.apply(this, (query) {
|
||||||
|
return query.addPropertyName(r'visibility');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
QueryBuilder<Asset, int?, QQueryOperations> widthProperty() {
|
QueryBuilder<Asset, int?, QQueryOperations> widthProperty() {
|
||||||
return QueryBuilder.apply(this, (query) {
|
return QueryBuilder.apply(this, (query) {
|
||||||
return query.addPropertyName(r'width');
|
return query.addPropertyName(r'width');
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
|
|
||||||
abstract interface class IAssetApiRepository {
|
abstract interface class IAssetApiRepository {
|
||||||
@ -15,4 +16,9 @@ abstract interface class IAssetApiRepository {
|
|||||||
// Future<void> delete(String id);
|
// Future<void> delete(String id);
|
||||||
|
|
||||||
Future<List<Asset>> search({List<String> personIds = const []});
|
Future<List<Asset>> search({List<String> personIds = const []});
|
||||||
|
|
||||||
|
Future<void> updateVisibility(
|
||||||
|
List<String> list,
|
||||||
|
AssetVisibilityEnum visibility,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,4 +6,9 @@ abstract interface class IAuthApiRepository {
|
|||||||
Future<void> logout();
|
Future<void> logout();
|
||||||
|
|
||||||
Future<void> changePassword(String newPassword);
|
Future<void> changePassword(String newPassword);
|
||||||
|
|
||||||
|
Future<bool> unlockPinCode(String pinCode);
|
||||||
|
Future<void> lockPinCode();
|
||||||
|
|
||||||
|
Future<void> setupPinCode(String pinCode);
|
||||||
}
|
}
|
||||||
|
6
mobile/lib/interfaces/biometric.interface.dart
Normal file
6
mobile/lib/interfaces/biometric.interface.dart
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
|
||||||
|
|
||||||
|
abstract interface class IBiometricRepository {
|
||||||
|
Future<BiometricStatus> getStatus();
|
||||||
|
Future<bool> authenticate(String? message);
|
||||||
|
}
|
5
mobile/lib/interfaces/secure_storage.interface.dart
Normal file
5
mobile/lib/interfaces/secure_storage.interface.dart
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
abstract interface class ISecureStorageRepository {
|
||||||
|
Future<String?> read(String key);
|
||||||
|
Future<void> write(String key, String value);
|
||||||
|
Future<void> delete(String key);
|
||||||
|
}
|
@ -31,4 +31,9 @@ abstract class ITimelineRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
Stream<RenderList> watchAssetSelectionTimeline(String userId);
|
Stream<RenderList> watchAssetSelectionTimeline(String userId);
|
||||||
|
|
||||||
|
Stream<RenderList> watchLockedTimeline(
|
||||||
|
String userId,
|
||||||
|
GroupAssetsBy groupAssetsBy,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||||
import 'package:immich_mobile/providers/theme.provider.dart';
|
import 'package:immich_mobile/providers/theme.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/routing/tab_navigation_observer.dart';
|
import 'package:immich_mobile/routing/app_navigation_observer.dart';
|
||||||
import 'package:immich_mobile/services/background.service.dart';
|
import 'package:immich_mobile/services/background.service.dart';
|
||||||
import 'package:immich_mobile/services/local_notification.service.dart';
|
import 'package:immich_mobile/services/local_notification.service.dart';
|
||||||
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
import 'package:immich_mobile/theme/dynamic_theme.dart';
|
||||||
@ -219,7 +219,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
),
|
),
|
||||||
routeInformationParser: router.defaultRouteParser(),
|
routeInformationParser: router.defaultRouteParser(),
|
||||||
routerDelegate: router.delegate(
|
routerDelegate: router.delegate(
|
||||||
navigatorObservers: () => [TabNavigationObserver(ref: ref)],
|
navigatorObservers: () => [AppNavigationObserver(ref: ref)],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
38
mobile/lib/models/auth/biometric_status.model.dart
Normal file
38
mobile/lib/models/auth/biometric_status.model.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:local_auth/local_auth.dart';
|
||||||
|
|
||||||
|
class BiometricStatus {
|
||||||
|
final List<BiometricType> availableBiometrics;
|
||||||
|
final bool canAuthenticate;
|
||||||
|
|
||||||
|
const BiometricStatus({
|
||||||
|
required this.availableBiometrics,
|
||||||
|
required this.canAuthenticate,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() =>
|
||||||
|
'BiometricStatus(availableBiometrics: $availableBiometrics, canAuthenticate: $canAuthenticate)';
|
||||||
|
|
||||||
|
BiometricStatus copyWith({
|
||||||
|
List<BiometricType>? availableBiometrics,
|
||||||
|
bool? canAuthenticate,
|
||||||
|
}) {
|
||||||
|
return BiometricStatus(
|
||||||
|
availableBiometrics: availableBiometrics ?? this.availableBiometrics,
|
||||||
|
canAuthenticate: canAuthenticate ?? this.canAuthenticate,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant BiometricStatus other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
return listEquals(other.availableBiometrics, availableBiometrics) &&
|
||||||
|
other.canAuthenticate == canAuthenticate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => availableBiometrics.hashCode ^ canAuthenticate.hashCode;
|
||||||
|
}
|
@ -140,6 +140,19 @@ class QuickAccessButtons extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
onTap: () => context.pushRoute(FolderRoute()),
|
onTap: () => context.pushRoute(FolderRoute()),
|
||||||
),
|
),
|
||||||
|
ListTile(
|
||||||
|
leading: const Icon(
|
||||||
|
Icons.lock_outline_rounded,
|
||||||
|
size: 26,
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
'locked_folder'.tr(),
|
||||||
|
style: context.textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () => context.pushRoute(const LockedRoute()),
|
||||||
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(
|
leading: const Icon(
|
||||||
Icons.group_outlined,
|
Icons.group_outlined,
|
||||||
|
95
mobile/lib/pages/library/locked/locked.page.dart
Normal file
95
mobile/lib/pages/library/locked/locked.page.dart
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class LockedPage extends HookConsumerWidget {
|
||||||
|
const LockedPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final appLifeCycle = useAppLifecycleState();
|
||||||
|
final showOverlay = useState(false);
|
||||||
|
final authProviderNotifier = ref.read(authProvider.notifier);
|
||||||
|
// lock the page when it is destroyed
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
return () {
|
||||||
|
authProviderNotifier.lockPinCode();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() {
|
||||||
|
if (context.mounted) {
|
||||||
|
if (appLifeCycle == AppLifecycleState.resumed) {
|
||||||
|
showOverlay.value = false;
|
||||||
|
} else {
|
||||||
|
showOverlay.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[appLifeCycle],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: ref.watch(multiselectProvider) ? null : const LockPageAppBar(),
|
||||||
|
body: showOverlay.value
|
||||||
|
? const SizedBox()
|
||||||
|
: MultiselectGrid(
|
||||||
|
renderListProvider: lockedTimelineProvider,
|
||||||
|
topWidget: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'no_locked_photos_message'.tr(),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
editEnabled: false,
|
||||||
|
favoriteEnabled: false,
|
||||||
|
unfavorite: false,
|
||||||
|
archiveEnabled: false,
|
||||||
|
stackEnabled: false,
|
||||||
|
unarchive: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class LockPageAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
|
const LockPageAppBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(authProvider.notifier).lockPinCode();
|
||||||
|
context.maybePop();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.arrow_back_ios_rounded),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
automaticallyImplyLeading: false,
|
||||||
|
title: const Text(
|
||||||
|
'locked_folder',
|
||||||
|
).tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => const Size.fromHeight(kToolbarHeight);
|
||||||
|
}
|
127
mobile/lib/pages/library/locked/pin_auth.page.dart
Normal file
127
mobile/lib/pages/library/locked/pin_auth.page.dart
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/local_auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
import 'package:immich_mobile/widgets/forms/pin_registration_form.dart';
|
||||||
|
import 'package:immich_mobile/widgets/forms/pin_verification_form.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class PinAuthPage extends HookConsumerWidget {
|
||||||
|
final bool createPinCode;
|
||||||
|
|
||||||
|
const PinAuthPage({super.key, this.createPinCode = false});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final localAuthState = ref.watch(localAuthProvider);
|
||||||
|
final showPinRegistrationForm = useState(createPinCode);
|
||||||
|
|
||||||
|
Future<void> registerBiometric(String pinCode) async {
|
||||||
|
final isRegistered =
|
||||||
|
await ref.read(localAuthProvider.notifier).registerBiometric(
|
||||||
|
context,
|
||||||
|
pinCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isRegistered) {
|
||||||
|
context.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'biometric_auth_enabled'.tr(),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
backgroundColor: context.colorScheme.primaryContainer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
context.replaceRoute(const LockedRoute());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enableBiometricAuth() {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (buildContext) {
|
||||||
|
return SimpleDialog(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
PinVerificationForm(
|
||||||
|
description: 'enable_biometric_auth_description'.tr(),
|
||||||
|
onSuccess: (pinCode) {
|
||||||
|
Navigator.pop(buildContext);
|
||||||
|
registerBiometric(pinCode);
|
||||||
|
},
|
||||||
|
autoFocus: true,
|
||||||
|
icon: Icons.fingerprint_rounded,
|
||||||
|
successIcon: Icons.fingerprint_rounded,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('locked_folder'.tr()),
|
||||||
|
),
|
||||||
|
body: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 36.0),
|
||||||
|
child: showPinRegistrationForm.value
|
||||||
|
? Center(
|
||||||
|
child: PinRegistrationForm(
|
||||||
|
onDone: () => showPinRegistrationForm.value = false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: PinVerificationForm(
|
||||||
|
autoFocus: true,
|
||||||
|
onSuccess: (_) =>
|
||||||
|
context.replaceRoute(const LockedRoute()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (localAuthState.canAuthenticate) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
|
child: TextButton.icon(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.fingerprint,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
onPressed: enableBiometricAuth,
|
||||||
|
label: Text(
|
||||||
|
'use_biometric'.tr(),
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.primaryColor,
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/user.service.dart';
|
import 'package:immich_mobile/domain/services/user.service.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
@ -170,6 +171,13 @@ class AssetNotifier extends StateNotifier<bool> {
|
|||||||
status ??= !assets.every((a) => a.isArchived);
|
status ??= !assets.every((a) => a.isArchived);
|
||||||
return _assetService.changeArchiveStatus(assets, status);
|
return _assetService.changeArchiveStatus(assets, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setLockedView(
|
||||||
|
List<Asset> selection,
|
||||||
|
AssetVisibilityEnum visibility,
|
||||||
|
) {
|
||||||
|
return _assetService.setVisibility(selection, visibility);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final assetDetailProvider =
|
final assetDetailProvider =
|
||||||
|
@ -188,4 +188,16 @@ class AuthNotifier extends StateNotifier<AuthState> {
|
|||||||
Future<String?> setOpenApiServiceEndpoint() {
|
Future<String?> setOpenApiServiceEndpoint() {
|
||||||
return _authService.setOpenApiServiceEndpoint();
|
return _authService.setOpenApiServiceEndpoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> unlockPinCode(String pinCode) {
|
||||||
|
return _authService.unlockPinCode(pinCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> lockPinCode() {
|
||||||
|
return _authService.lockPinCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setupPinCode(String pinCode) {
|
||||||
|
return _authService.setupPinCode(pinCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
97
mobile/lib/providers/local_auth.provider.dart
Normal file
97
mobile/lib/providers/local_auth.provider.dart
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
|
||||||
|
import 'package:immich_mobile/services/local_auth.service.dart';
|
||||||
|
import 'package:immich_mobile/services/secure_storage.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
final localAuthProvider =
|
||||||
|
StateNotifierProvider<LocalAuthNotifier, BiometricStatus>((ref) {
|
||||||
|
return LocalAuthNotifier(
|
||||||
|
ref.watch(localAuthServiceProvider),
|
||||||
|
ref.watch(secureStorageServiceProvider),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
class LocalAuthNotifier extends StateNotifier<BiometricStatus> {
|
||||||
|
final LocalAuthService _localAuthService;
|
||||||
|
final SecureStorageService _secureStorageService;
|
||||||
|
|
||||||
|
final _log = Logger("LocalAuthNotifier");
|
||||||
|
|
||||||
|
LocalAuthNotifier(this._localAuthService, this._secureStorageService)
|
||||||
|
: super(
|
||||||
|
const BiometricStatus(
|
||||||
|
availableBiometrics: [],
|
||||||
|
canAuthenticate: false,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
_localAuthService.getStatus().then((value) {
|
||||||
|
state = state.copyWith(
|
||||||
|
canAuthenticate: value.canAuthenticate,
|
||||||
|
availableBiometrics: value.availableBiometrics,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> registerBiometric(BuildContext context, String pinCode) async {
|
||||||
|
final isAuthenticated =
|
||||||
|
await authenticate(context, 'Authenticate to enable biometrics');
|
||||||
|
|
||||||
|
if (!isAuthenticated) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _secureStorageService.write(kSecuredPinCode, pinCode);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> authenticate(BuildContext context, String? message) async {
|
||||||
|
String errorMessage = "";
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await _localAuthService.authenticate(message);
|
||||||
|
} on PlatformException catch (error) {
|
||||||
|
switch (error.code) {
|
||||||
|
case "NotEnrolled":
|
||||||
|
_log.warning("User is not enrolled in biometrics");
|
||||||
|
errorMessage = "biometric_no_options".tr();
|
||||||
|
break;
|
||||||
|
case "NotAvailable":
|
||||||
|
_log.warning("Biometric authentication is not available");
|
||||||
|
errorMessage = "biometric_not_available".tr();
|
||||||
|
break;
|
||||||
|
case "LockedOut":
|
||||||
|
_log.warning("User is locked out of biometric authentication");
|
||||||
|
errorMessage = "biometric_locked_out".tr();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_log.warning("Failed to authenticate with unknown reason");
|
||||||
|
errorMessage = 'failed_to_authenticate'.tr();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
_log.warning("Error during authentication: $error");
|
||||||
|
errorMessage = 'failed_to_authenticate'.tr();
|
||||||
|
} finally {
|
||||||
|
if (errorMessage.isNotEmpty) {
|
||||||
|
context.showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
errorMessage,
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
backgroundColor: context.colorScheme.errorContainer,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
3
mobile/lib/providers/routes.provider.dart
Normal file
3
mobile/lib/providers/routes.provider.dart
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
final inLockedViewProvider = StateProvider<bool>((ref) => false);
|
10
mobile/lib/providers/secure_storage.provider.dart
Normal file
10
mobile/lib/providers/secure_storage.provider.dart
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
|
final secureStorageProvider =
|
||||||
|
StateNotifierProvider<SecureStorageProvider, void>((ref) {
|
||||||
|
return SecureStorageProvider();
|
||||||
|
});
|
||||||
|
|
||||||
|
class SecureStorageProvider extends StateNotifier<void> {
|
||||||
|
SecureStorageProvider() : super(null);
|
||||||
|
}
|
@ -73,3 +73,8 @@ final assetsTimelineProvider =
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final lockedTimelineProvider = StreamProvider<RenderList>((ref) {
|
||||||
|
final timelineService = ref.watch(timelineServiceProvider);
|
||||||
|
return timelineService.watchLockedTimelineProvider();
|
||||||
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
@ -48,4 +49,27 @@ class AssetApiRepository extends ApiRepository implements IAssetApiRepository {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> updateVisibility(
|
||||||
|
List<String> ids,
|
||||||
|
AssetVisibilityEnum visibility,
|
||||||
|
) async {
|
||||||
|
return _api.updateAssets(
|
||||||
|
AssetBulkUpdateDto(ids: ids, visibility: _mapVisibility(visibility)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
_mapVisibility(AssetVisibilityEnum visibility) {
|
||||||
|
switch (visibility) {
|
||||||
|
case AssetVisibilityEnum.timeline:
|
||||||
|
return AssetVisibility.timeline;
|
||||||
|
case AssetVisibilityEnum.hidden:
|
||||||
|
return AssetVisibility.hidden;
|
||||||
|
case AssetVisibilityEnum.locked:
|
||||||
|
return AssetVisibility.locked;
|
||||||
|
case AssetVisibilityEnum.archive:
|
||||||
|
return AssetVisibility.archive;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,4 +55,26 @@ class AuthApiRepository extends ApiRepository implements IAuthApiRepository {
|
|||||||
userId: dto.userId,
|
userId: dto.userId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> unlockPinCode(String pinCode) async {
|
||||||
|
try {
|
||||||
|
await _apiService.authenticationApi
|
||||||
|
.unlockAuthSession(SessionUnlockDto(pinCode: pinCode));
|
||||||
|
return true;
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setupPinCode(String pinCode) {
|
||||||
|
return _apiService.authenticationApi
|
||||||
|
.setupPinCode(PinCodeSetupDto(pinCode: pinCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> lockPinCode() {
|
||||||
|
return _apiService.authenticationApi.lockAuthSession();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
35
mobile/lib/repositories/biometric.repository.dart
Normal file
35
mobile/lib/repositories/biometric.repository.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/biometric.interface.dart';
|
||||||
|
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
|
||||||
|
import 'package:local_auth/local_auth.dart';
|
||||||
|
|
||||||
|
final biometricRepositoryProvider =
|
||||||
|
Provider((ref) => BiometricRepository(LocalAuthentication()));
|
||||||
|
|
||||||
|
class BiometricRepository implements IBiometricRepository {
|
||||||
|
final LocalAuthentication _localAuth;
|
||||||
|
|
||||||
|
BiometricRepository(this._localAuth);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<BiometricStatus> getStatus() async {
|
||||||
|
final bool canAuthenticateWithBiometrics =
|
||||||
|
await _localAuth.canCheckBiometrics;
|
||||||
|
final bool canAuthenticate =
|
||||||
|
canAuthenticateWithBiometrics || await _localAuth.isDeviceSupported();
|
||||||
|
final availableBiometric = await _localAuth.getAvailableBiometrics();
|
||||||
|
|
||||||
|
return BiometricStatus(
|
||||||
|
canAuthenticate: canAuthenticate,
|
||||||
|
availableBiometrics: availableBiometric,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> authenticate(String? message) async {
|
||||||
|
return _localAuth.authenticate(
|
||||||
|
localizedReason: message ?? 'please_auth_to_access'.tr(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
27
mobile/lib/repositories/secure_storage.repository.dart
Normal file
27
mobile/lib/repositories/secure_storage.repository.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/secure_storage.interface.dart';
|
||||||
|
|
||||||
|
final secureStorageRepositoryProvider =
|
||||||
|
Provider((ref) => SecureStorageRepository(const FlutterSecureStorage()));
|
||||||
|
|
||||||
|
class SecureStorageRepository implements ISecureStorageRepository {
|
||||||
|
final FlutterSecureStorage _secureStorage;
|
||||||
|
|
||||||
|
SecureStorageRepository(this._secureStorage);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> read(String key) {
|
||||||
|
return _secureStorage.read(key: key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> write(String key, String value) {
|
||||||
|
return _secureStorage.write(key: key, value: value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> delete(String key) {
|
||||||
|
return _secureStorage.delete(key: key);
|
||||||
|
}
|
||||||
|
}
|
@ -45,8 +45,8 @@ class TimelineRepository extends DatabaseRepository
|
|||||||
.where()
|
.where()
|
||||||
.ownerIdEqualToAnyChecksum(fastHash(userId))
|
.ownerIdEqualToAnyChecksum(fastHash(userId))
|
||||||
.filter()
|
.filter()
|
||||||
.isArchivedEqualTo(true)
|
|
||||||
.isTrashedEqualTo(false)
|
.isTrashedEqualTo(false)
|
||||||
|
.visibilityEqualTo(AssetVisibilityEnum.archive)
|
||||||
.sortByFileCreatedAtDesc();
|
.sortByFileCreatedAtDesc();
|
||||||
|
|
||||||
return _watchRenderList(query, GroupAssetsBy.none);
|
return _watchRenderList(query, GroupAssetsBy.none);
|
||||||
@ -59,6 +59,8 @@ class TimelineRepository extends DatabaseRepository
|
|||||||
.ownerIdEqualToAnyChecksum(fastHash(userId))
|
.ownerIdEqualToAnyChecksum(fastHash(userId))
|
||||||
.filter()
|
.filter()
|
||||||
.isFavoriteEqualTo(true)
|
.isFavoriteEqualTo(true)
|
||||||
|
.not()
|
||||||
|
.visibilityEqualTo(AssetVisibilityEnum.locked)
|
||||||
.isTrashedEqualTo(false)
|
.isTrashedEqualTo(false)
|
||||||
.sortByFileCreatedAtDesc();
|
.sortByFileCreatedAtDesc();
|
||||||
|
|
||||||
@ -94,8 +96,8 @@ class TimelineRepository extends DatabaseRepository
|
|||||||
Stream<RenderList> watchAllVideosTimeline() {
|
Stream<RenderList> watchAllVideosTimeline() {
|
||||||
final query = db.assets
|
final query = db.assets
|
||||||
.filter()
|
.filter()
|
||||||
.isArchivedEqualTo(false)
|
|
||||||
.isTrashedEqualTo(false)
|
.isTrashedEqualTo(false)
|
||||||
|
.visibilityEqualTo(AssetVisibilityEnum.timeline)
|
||||||
.typeEqualTo(AssetType.video)
|
.typeEqualTo(AssetType.video)
|
||||||
.sortByFileCreatedAtDesc();
|
.sortByFileCreatedAtDesc();
|
||||||
|
|
||||||
@ -111,9 +113,9 @@ class TimelineRepository extends DatabaseRepository
|
|||||||
.where()
|
.where()
|
||||||
.ownerIdEqualToAnyChecksum(fastHash(userId))
|
.ownerIdEqualToAnyChecksum(fastHash(userId))
|
||||||
.filter()
|
.filter()
|
||||||
.isArchivedEqualTo(false)
|
|
||||||
.isTrashedEqualTo(false)
|
.isTrashedEqualTo(false)
|
||||||
.stackPrimaryAssetIdIsNull()
|
.stackPrimaryAssetIdIsNull()
|
||||||
|
.visibilityEqualTo(AssetVisibilityEnum.timeline)
|
||||||
.sortByFileCreatedAtDesc();
|
.sortByFileCreatedAtDesc();
|
||||||
|
|
||||||
return _watchRenderList(query, groupAssetByOption);
|
return _watchRenderList(query, groupAssetByOption);
|
||||||
@ -129,8 +131,8 @@ class TimelineRepository extends DatabaseRepository
|
|||||||
.where()
|
.where()
|
||||||
.anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id))
|
.anyOf(isarUserIds, (qb, id) => qb.ownerIdEqualToAnyChecksum(id))
|
||||||
.filter()
|
.filter()
|
||||||
.isArchivedEqualTo(false)
|
|
||||||
.isTrashedEqualTo(false)
|
.isTrashedEqualTo(false)
|
||||||
|
.visibilityEqualTo(AssetVisibilityEnum.timeline)
|
||||||
.stackPrimaryAssetIdIsNull()
|
.stackPrimaryAssetIdIsNull()
|
||||||
.sortByFileCreatedAtDesc();
|
.sortByFileCreatedAtDesc();
|
||||||
return _watchRenderList(query, groupAssetByOption);
|
return _watchRenderList(query, groupAssetByOption);
|
||||||
@ -151,6 +153,7 @@ class TimelineRepository extends DatabaseRepository
|
|||||||
.remoteIdIsNotNull()
|
.remoteIdIsNotNull()
|
||||||
.filter()
|
.filter()
|
||||||
.ownerIdEqualTo(fastHash(userId))
|
.ownerIdEqualTo(fastHash(userId))
|
||||||
|
.visibilityEqualTo(AssetVisibilityEnum.timeline)
|
||||||
.isTrashedEqualTo(false)
|
.isTrashedEqualTo(false)
|
||||||
.stackPrimaryAssetIdIsNull()
|
.stackPrimaryAssetIdIsNull()
|
||||||
.sortByFileCreatedAtDesc();
|
.sortByFileCreatedAtDesc();
|
||||||
@ -158,6 +161,22 @@ class TimelineRepository extends DatabaseRepository
|
|||||||
return _watchRenderList(query, GroupAssetsBy.none);
|
return _watchRenderList(query, GroupAssetsBy.none);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<RenderList> watchLockedTimeline(
|
||||||
|
String userId,
|
||||||
|
GroupAssetsBy getGroupByOption,
|
||||||
|
) {
|
||||||
|
final query = db.assets
|
||||||
|
.where()
|
||||||
|
.ownerIdEqualToAnyChecksum(fastHash(userId))
|
||||||
|
.filter()
|
||||||
|
.visibilityEqualTo(AssetVisibilityEnum.locked)
|
||||||
|
.isTrashedEqualTo(false)
|
||||||
|
.sortByFileCreatedAtDesc();
|
||||||
|
|
||||||
|
return _watchRenderList(query, getGroupByOption);
|
||||||
|
}
|
||||||
|
|
||||||
Stream<RenderList> _watchRenderList(
|
Stream<RenderList> _watchRenderList(
|
||||||
QueryBuilder<Asset, Asset, QAfterSortBy> query,
|
QueryBuilder<Asset, Asset, QAfterSortBy> query,
|
||||||
GroupAssetsBy groupAssetsBy,
|
GroupAssetsBy groupAssetsBy,
|
||||||
|
52
mobile/lib/routing/app_navigation_observer.dart
Normal file
52
mobile/lib/routing/app_navigation_observer.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
class AppNavigationObserver extends AutoRouterObserver {
|
||||||
|
/// Riverpod Instance
|
||||||
|
final WidgetRef ref;
|
||||||
|
|
||||||
|
AppNavigationObserver({
|
||||||
|
required this.ref,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> didChangeTabRoute(
|
||||||
|
TabPageRoute route,
|
||||||
|
TabPageRoute previousRoute,
|
||||||
|
) async {
|
||||||
|
Future(
|
||||||
|
() => ref.read(inLockedViewProvider.notifier).state = false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPush(Route route, Route? previousRoute) {
|
||||||
|
_handleLockedViewState(route, previousRoute);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleLockedViewState(Route route, Route? previousRoute) {
|
||||||
|
final isInLockedView = ref.read(inLockedViewProvider);
|
||||||
|
final isFromLockedViewToDetailView =
|
||||||
|
route.settings.name == GalleryViewerRoute.name &&
|
||||||
|
previousRoute?.settings.name == LockedRoute.name;
|
||||||
|
|
||||||
|
final isFromDetailViewToInfoPanelView = route.settings.name == null &&
|
||||||
|
previousRoute?.settings.name == GalleryViewerRoute.name &&
|
||||||
|
isInLockedView;
|
||||||
|
|
||||||
|
if (route.settings.name == LockedRoute.name ||
|
||||||
|
isFromLockedViewToDetailView ||
|
||||||
|
isFromDetailViewToInfoPanelView) {
|
||||||
|
Future(
|
||||||
|
() => ref.read(inLockedViewProvider.notifier).state = true,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Future(
|
||||||
|
() => ref.read(inLockedViewProvider.notifier).state = false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
89
mobile/lib/routing/locked_guard.dart
Normal file
89
mobile/lib/routing/locked_guard.dart
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:immich_mobile/constants/constants.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/services/local_auth.service.dart';
|
||||||
|
import 'package:immich_mobile/services/secure_storage.service.dart';
|
||||||
|
import 'package:local_auth/error_codes.dart' as auth_error;
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
// ignore: import_rule_openapi
|
||||||
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
|
class LockedGuard extends AutoRouteGuard {
|
||||||
|
final ApiService _apiService;
|
||||||
|
final SecureStorageService _secureStorageService;
|
||||||
|
final LocalAuthService _localAuth;
|
||||||
|
final _log = Logger("AuthGuard");
|
||||||
|
|
||||||
|
LockedGuard(
|
||||||
|
this._apiService,
|
||||||
|
this._secureStorageService,
|
||||||
|
this._localAuth,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onNavigation(NavigationResolver resolver, StackRouter router) async {
|
||||||
|
final authStatus = await _apiService.authenticationApi.getAuthStatus();
|
||||||
|
|
||||||
|
if (authStatus == null) {
|
||||||
|
resolver.next(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a pincode has been created but this user. Show the form to create if not exist
|
||||||
|
if (!authStatus.pinCode) {
|
||||||
|
router.push(PinAuthRoute(createPinCode: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authStatus.isElevated) {
|
||||||
|
resolver.next(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the user has the pincode saved in secure storage, meaning
|
||||||
|
/// the user has enabled the biometric authentication
|
||||||
|
final securePinCode = await _secureStorageService.read(kSecuredPinCode);
|
||||||
|
if (securePinCode == null) {
|
||||||
|
router.push(PinAuthRoute());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final bool isAuth = await _localAuth.authenticate();
|
||||||
|
|
||||||
|
if (!isAuth) {
|
||||||
|
resolver.next(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _apiService.authenticationApi.unlockAuthSession(
|
||||||
|
SessionUnlockDto(pinCode: securePinCode),
|
||||||
|
);
|
||||||
|
|
||||||
|
resolver.next(true);
|
||||||
|
} on PlatformException catch (error) {
|
||||||
|
switch (error.code) {
|
||||||
|
case auth_error.notAvailable:
|
||||||
|
_log.severe("notAvailable: $error");
|
||||||
|
break;
|
||||||
|
case auth_error.notEnrolled:
|
||||||
|
_log.severe("not enrolled");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
_log.severe("error");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver.next(false);
|
||||||
|
} on ApiException {
|
||||||
|
// PIN code has changed, need to re-enter to access
|
||||||
|
await _secureStorageService.delete(kSecuredPinCode);
|
||||||
|
router.push(PinAuthRoute());
|
||||||
|
} catch (error) {
|
||||||
|
_log.severe("Failed to access locked page", error);
|
||||||
|
resolver.next(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -39,6 +39,8 @@ import 'package:immich_mobile/pages/library/favorite.page.dart';
|
|||||||
import 'package:immich_mobile/pages/library/folder/folder.page.dart';
|
import 'package:immich_mobile/pages/library/folder/folder.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/library.page.dart';
|
import 'package:immich_mobile/pages/library/library.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
import 'package:immich_mobile/pages/library/local_albums.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/locked/locked.page.dart';
|
||||||
|
import 'package:immich_mobile/pages/library/locked/pin_auth.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
import 'package:immich_mobile/pages/library/partner/partner.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
import 'package:immich_mobile/pages/library/partner/partner_detail.page.dart';
|
||||||
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
import 'package:immich_mobile/pages/library/people/people_collection.page.dart';
|
||||||
@ -67,24 +69,41 @@ import 'package:immich_mobile/routing/auth_guard.dart';
|
|||||||
import 'package:immich_mobile/routing/backup_permission_guard.dart';
|
import 'package:immich_mobile/routing/backup_permission_guard.dart';
|
||||||
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
import 'package:immich_mobile/routing/custom_transition_builders.dart';
|
||||||
import 'package:immich_mobile/routing/duplicate_guard.dart';
|
import 'package:immich_mobile/routing/duplicate_guard.dart';
|
||||||
|
import 'package:immich_mobile/routing/locked_guard.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/services/local_auth.service.dart';
|
||||||
|
import 'package:immich_mobile/services/secure_storage.service.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
part 'router.gr.dart';
|
part 'router.gr.dart';
|
||||||
|
|
||||||
|
final appRouterProvider = Provider(
|
||||||
|
(ref) => AppRouter(
|
||||||
|
ref.watch(apiServiceProvider),
|
||||||
|
ref.watch(galleryPermissionNotifier.notifier),
|
||||||
|
ref.watch(secureStorageServiceProvider),
|
||||||
|
ref.watch(localAuthServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
@AutoRouterConfig(replaceInRouteName: 'Page,Route')
|
@AutoRouterConfig(replaceInRouteName: 'Page,Route')
|
||||||
class AppRouter extends RootStackRouter {
|
class AppRouter extends RootStackRouter {
|
||||||
late final AuthGuard _authGuard;
|
late final AuthGuard _authGuard;
|
||||||
late final DuplicateGuard _duplicateGuard;
|
late final DuplicateGuard _duplicateGuard;
|
||||||
late final BackupPermissionGuard _backupPermissionGuard;
|
late final BackupPermissionGuard _backupPermissionGuard;
|
||||||
|
late final LockedGuard _lockedGuard;
|
||||||
|
|
||||||
AppRouter(
|
AppRouter(
|
||||||
ApiService apiService,
|
ApiService apiService,
|
||||||
GalleryPermissionNotifier galleryPermissionNotifier,
|
GalleryPermissionNotifier galleryPermissionNotifier,
|
||||||
|
SecureStorageService secureStorageService,
|
||||||
|
LocalAuthService localAuthService,
|
||||||
) {
|
) {
|
||||||
_authGuard = AuthGuard(apiService);
|
_authGuard = AuthGuard(apiService);
|
||||||
_duplicateGuard = DuplicateGuard();
|
_duplicateGuard = DuplicateGuard();
|
||||||
|
_lockedGuard =
|
||||||
|
LockedGuard(apiService, secureStorageService, localAuthService);
|
||||||
_backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier);
|
_backupPermissionGuard = BackupPermissionGuard(galleryPermissionNotifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,12 +308,13 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: ShareIntentRoute.page,
|
page: ShareIntentRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: LockedRoute.page,
|
||||||
|
guards: [_authGuard, _lockedGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: PinAuthRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
final appRouterProvider = Provider(
|
|
||||||
(ref) => AppRouter(
|
|
||||||
ref.watch(apiServiceProvider),
|
|
||||||
ref.watch(galleryPermissionNotifier.notifier),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
@ -956,6 +956,25 @@ class LocalAlbumsRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [LockedPage]
|
||||||
|
class LockedRoute extends PageRouteInfo<void> {
|
||||||
|
const LockedRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(
|
||||||
|
LockedRoute.name,
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'LockedRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const LockedPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [LoginPage]
|
/// [LoginPage]
|
||||||
class LoginRoute extends PageRouteInfo<void> {
|
class LoginRoute extends PageRouteInfo<void> {
|
||||||
@ -1359,6 +1378,53 @@ class PhotosRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [PinAuthPage]
|
||||||
|
class PinAuthRoute extends PageRouteInfo<PinAuthRouteArgs> {
|
||||||
|
PinAuthRoute({
|
||||||
|
Key? key,
|
||||||
|
bool createPinCode = false,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
PinAuthRoute.name,
|
||||||
|
args: PinAuthRouteArgs(
|
||||||
|
key: key,
|
||||||
|
createPinCode: createPinCode,
|
||||||
|
),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'PinAuthRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
final args =
|
||||||
|
data.argsAs<PinAuthRouteArgs>(orElse: () => const PinAuthRouteArgs());
|
||||||
|
return PinAuthPage(
|
||||||
|
key: args.key,
|
||||||
|
createPinCode: args.createPinCode,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PinAuthRouteArgs {
|
||||||
|
const PinAuthRouteArgs({
|
||||||
|
this.key,
|
||||||
|
this.createPinCode = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final bool createPinCode;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PinAuthRouteArgs{key: $key, createPinCode: $createPinCode}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [PlacesCollectionPage]
|
/// [PlacesCollectionPage]
|
||||||
class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
|
class PlacesCollectionRoute extends PageRouteInfo<PlacesCollectionRouteArgs> {
|
||||||
|
@ -1,35 +0,0 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/user.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/memory.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
|
||||||
|
|
||||||
class TabNavigationObserver extends AutoRouterObserver {
|
|
||||||
/// Riverpod Instance
|
|
||||||
final WidgetRef ref;
|
|
||||||
|
|
||||||
TabNavigationObserver({
|
|
||||||
required this.ref,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<void> didChangeTabRoute(
|
|
||||||
TabPageRoute route,
|
|
||||||
TabPageRoute previousRoute,
|
|
||||||
) async {
|
|
||||||
if (route.name == 'HomeRoute') {
|
|
||||||
ref.invalidate(memoryFutureProvider);
|
|
||||||
Future(() => ref.read(assetProvider.notifier).getAllAsset());
|
|
||||||
|
|
||||||
// Update user info
|
|
||||||
try {
|
|
||||||
ref.read(userServiceProvider).refreshMyUser();
|
|
||||||
ref.read(serverInfoProvider.notifier).getServerVersion();
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint("Error refreshing user info $e");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/exif.interface.dart';
|
||||||
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
import 'package:immich_mobile/domain/interfaces/user.interface.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
@ -239,6 +240,9 @@ class AssetService {
|
|||||||
|
|
||||||
for (var element in assets) {
|
for (var element in assets) {
|
||||||
element.isArchived = isArchived;
|
element.isArchived = isArchived;
|
||||||
|
element.visibility = isArchived
|
||||||
|
? AssetVisibilityEnum.archive
|
||||||
|
: AssetVisibilityEnum.timeline;
|
||||||
}
|
}
|
||||||
|
|
||||||
await _syncService.upsertAssetsWithExif(assets);
|
await _syncService.upsertAssetsWithExif(assets);
|
||||||
@ -458,6 +462,7 @@ class AssetService {
|
|||||||
bool shouldDeletePermanently = false,
|
bool shouldDeletePermanently = false,
|
||||||
}) async {
|
}) async {
|
||||||
final candidates = assets.where((a) => a.isRemote);
|
final candidates = assets.where((a) => a.isRemote);
|
||||||
|
|
||||||
if (candidates.isEmpty) {
|
if (candidates.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -475,6 +480,7 @@ class AssetService {
|
|||||||
.where((asset) => asset.storage == AssetState.merged)
|
.where((asset) => asset.storage == AssetState.merged)
|
||||||
.map((asset) {
|
.map((asset) {
|
||||||
asset.remoteId = null;
|
asset.remoteId = null;
|
||||||
|
asset.visibility = AssetVisibilityEnum.timeline;
|
||||||
return asset;
|
return asset;
|
||||||
})
|
})
|
||||||
: assets.where((asset) => asset.isRemote).map((asset) {
|
: assets.where((asset) => asset.isRemote).map((asset) {
|
||||||
@ -529,4 +535,21 @@ class AssetService {
|
|||||||
final me = _userService.getMyUser();
|
final me = _userService.getMyUser();
|
||||||
return _assetRepository.getMotionAssets(me.id);
|
return _assetRepository.getMotionAssets(me.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setVisibility(
|
||||||
|
List<Asset> assets,
|
||||||
|
AssetVisibilityEnum visibility,
|
||||||
|
) async {
|
||||||
|
await _assetApiRepository.updateVisibility(
|
||||||
|
assets.map((asset) => asset.remoteId!).toList(),
|
||||||
|
visibility,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedAssets = assets.map((asset) {
|
||||||
|
asset.visibility = visibility;
|
||||||
|
return asset;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
await _assetRepository.updateAll(updatedAssets);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -201,4 +201,16 @@ class AuthService {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<bool> unlockPinCode(String pinCode) {
|
||||||
|
return _authApiRepository.unlockPinCode(pinCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> lockPinCode() {
|
||||||
|
return _authApiRepository.lockPinCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setupPinCode(String pinCode) {
|
||||||
|
return _authApiRepository.setupPinCode(pinCode);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
26
mobile/lib/services/local_auth.service.dart
Normal file
26
mobile/lib/services/local_auth.service.dart
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/biometric.interface.dart';
|
||||||
|
import 'package:immich_mobile/models/auth/biometric_status.model.dart';
|
||||||
|
import 'package:immich_mobile/repositories/biometric.repository.dart';
|
||||||
|
|
||||||
|
final localAuthServiceProvider = Provider(
|
||||||
|
(ref) => LocalAuthService(
|
||||||
|
ref.watch(biometricRepositoryProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
class LocalAuthService {
|
||||||
|
// final _log = Logger("LocalAuthService");
|
||||||
|
|
||||||
|
final IBiometricRepository _biometricRepository;
|
||||||
|
|
||||||
|
LocalAuthService(this._biometricRepository);
|
||||||
|
|
||||||
|
Future<BiometricStatus> getStatus() {
|
||||||
|
return _biometricRepository.getStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> authenticate([String? message]) async {
|
||||||
|
return _biometricRepository.authenticate(message);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,10 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
import 'package:immich_mobile/interfaces/asset.interface.dart';
|
||||||
import 'package:immich_mobile/models/memories/memory.model.dart';
|
import 'package:immich_mobile/models/memories/memory.model.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/translation.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
|
final memoryServiceProvider = StateProvider<MemoryService>((ref) {
|
||||||
@ -40,10 +40,7 @@ class MemoryService {
|
|||||||
.getAllByRemoteId(memory.assets.map((e) => e.id));
|
.getAllByRemoteId(memory.assets.map((e) => e.id));
|
||||||
final yearsAgo = now.year - memory.data.year;
|
final yearsAgo = now.year - memory.data.year;
|
||||||
if (dbAssets.isNotEmpty) {
|
if (dbAssets.isNotEmpty) {
|
||||||
final String title = yearsAgo <= 1
|
final String title = t('years_ago', {'years': yearsAgo.toString()});
|
||||||
? 'memories_year_ago'.tr()
|
|
||||||
: 'memories_years_ago'
|
|
||||||
.tr(namedArgs: {'years': yearsAgo.toString()});
|
|
||||||
memories.add(
|
memories.add(
|
||||||
Memory(
|
Memory(
|
||||||
title: title,
|
title: title,
|
||||||
|
29
mobile/lib/services/secure_storage.service.dart
Normal file
29
mobile/lib/services/secure_storage.service.dart
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/interfaces/secure_storage.interface.dart';
|
||||||
|
import 'package:immich_mobile/repositories/secure_storage.repository.dart';
|
||||||
|
|
||||||
|
final secureStorageServiceProvider = Provider(
|
||||||
|
(ref) => SecureStorageService(
|
||||||
|
ref.watch(secureStorageRepositoryProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
class SecureStorageService {
|
||||||
|
// final _log = Logger("LocalAuthService");
|
||||||
|
|
||||||
|
final ISecureStorageRepository _secureStorageRepository;
|
||||||
|
|
||||||
|
SecureStorageService(this._secureStorageRepository);
|
||||||
|
|
||||||
|
Future<void> write(String key, String value) async {
|
||||||
|
await _secureStorageRepository.write(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(String key) async {
|
||||||
|
await _secureStorageRepository.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> read(String key) async {
|
||||||
|
return _secureStorageRepository.read(key);
|
||||||
|
}
|
||||||
|
}
|
@ -105,4 +105,13 @@ class TimelineService {
|
|||||||
return GroupAssetsBy
|
return GroupAssetsBy
|
||||||
.values[_appSettingsService.getSetting(AppSettingsEnum.groupAssetsBy)];
|
.values[_appSettingsService.getSetting(AppSettingsEnum.groupAssetsBy)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<RenderList> watchLockedTimelineProvider() async* {
|
||||||
|
final user = _userService.getMyUser();
|
||||||
|
|
||||||
|
yield* _timelineRepository.watchLockedTimeline(
|
||||||
|
user.id,
|
||||||
|
_getGroupByOption(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,7 +42,7 @@ ThemeData getThemeData({
|
|||||||
titleTextStyle: TextStyle(
|
titleTextStyle: TextStyle(
|
||||||
color: colorScheme.primary,
|
color: colorScheme.primary,
|
||||||
fontFamily: _getFontFamilyFromLocale(locale),
|
fontFamily: _getFontFamilyFromLocale(locale),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
),
|
),
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
@ -54,28 +54,28 @@ ThemeData getThemeData({
|
|||||||
),
|
),
|
||||||
textTheme: const TextTheme(
|
textTheme: const TextTheme(
|
||||||
displayLarge: TextStyle(
|
displayLarge: TextStyle(
|
||||||
fontSize: 26,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
displayMedium: TextStyle(
|
displayMedium: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
displaySmall: TextStyle(
|
displaySmall: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
titleSmall: TextStyle(
|
titleSmall: TextStyle(
|
||||||
fontSize: 16.0,
|
fontSize: 16.0,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
titleMedium: TextStyle(
|
titleMedium: TextStyle(
|
||||||
fontSize: 18.0,
|
fontSize: 18.0,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
titleLarge: TextStyle(
|
titleLarge: TextStyle(
|
||||||
fontSize: 26.0,
|
fontSize: 26.0,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
|
@ -20,7 +20,7 @@ import 'package:isar/isar.dart';
|
|||||||
// ignore: import_rule_photo_manager
|
// ignore: import_rule_photo_manager
|
||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
const int targetVersion = 10;
|
const int targetVersion = 11;
|
||||||
|
|
||||||
Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
Future<void> migrateDatabaseIfNeeded(Isar db) async {
|
||||||
final int version = Store.get(StoreKey.version, targetVersion);
|
final int version = Store.get(StoreKey.version, targetVersion);
|
||||||
|
@ -32,6 +32,11 @@ dynamic upgradeDto(dynamic value, String targetType) {
|
|||||||
addDefault(value, 'visibility', AssetVisibility.timeline);
|
addDefault(value, 'visibility', AssetVisibility.timeline);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'AssetResponseDto':
|
||||||
|
if (value is Map) {
|
||||||
|
addDefault(value, 'visibility', 'timeline');
|
||||||
|
}
|
||||||
|
break;
|
||||||
case 'UserAdminResponseDto':
|
case 'UserAdminResponseDto':
|
||||||
if (value is Map) {
|
if (value is Map) {
|
||||||
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
addDefault(value, 'profileChangedAt', DateTime.now().toIso8601String());
|
||||||
|
@ -2,6 +2,7 @@ import 'package:easy_localization/easy_localization.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/asset_extensions.dart';
|
import 'package:immich_mobile/extensions/asset_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
@ -157,3 +158,29 @@ Future<void> handleEditLocation(
|
|||||||
|
|
||||||
ref.read(assetServiceProvider).changeLocation(selection.toList(), location);
|
ref.read(assetServiceProvider).changeLocation(selection.toList(), location);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> handleSetAssetsVisibility(
|
||||||
|
WidgetRef ref,
|
||||||
|
BuildContext context,
|
||||||
|
AssetVisibilityEnum visibility,
|
||||||
|
List<Asset> selection, {
|
||||||
|
ToastGravity toastGravity = ToastGravity.BOTTOM,
|
||||||
|
}) async {
|
||||||
|
if (selection.isNotEmpty) {
|
||||||
|
await ref
|
||||||
|
.watch(assetProvider.notifier)
|
||||||
|
.setLockedView(selection, visibility);
|
||||||
|
|
||||||
|
final assetOrAssets = selection.length > 1 ? 'assets' : 'asset';
|
||||||
|
final toastMessage = visibility == AssetVisibilityEnum.locked
|
||||||
|
? 'Added ${selection.length} $assetOrAssets to locked folder'
|
||||||
|
: 'Removed ${selection.length} $assetOrAssets from locked folder';
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: toastMessage,
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/providers/album/album.provider.dart';
|
import 'package:immich_mobile/providers/album/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
import 'package:immich_mobile/widgets/album/add_to_album_sliverlist.dart';
|
||||||
import 'package:immich_mobile/models/asset_selection_state.dart';
|
import 'package:immich_mobile/models/asset_selection_state.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
|
||||||
@ -37,6 +38,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
final void Function()? onEditTime;
|
final void Function()? onEditTime;
|
||||||
final void Function()? onEditLocation;
|
final void Function()? onEditLocation;
|
||||||
final void Function()? onRemoveFromAlbum;
|
final void Function()? onRemoveFromAlbum;
|
||||||
|
final void Function()? onToggleLocked;
|
||||||
|
|
||||||
final bool enabled;
|
final bool enabled;
|
||||||
final bool unfavorite;
|
final bool unfavorite;
|
||||||
@ -58,6 +60,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
this.onEditTime,
|
this.onEditTime,
|
||||||
this.onEditLocation,
|
this.onEditLocation,
|
||||||
this.onRemoveFromAlbum,
|
this.onRemoveFromAlbum,
|
||||||
|
this.onToggleLocked,
|
||||||
this.selectionAssetState = const AssetSelectionState(),
|
this.selectionAssetState = const AssetSelectionState(),
|
||||||
this.enabled = true,
|
this.enabled = true,
|
||||||
this.unarchive = false,
|
this.unarchive = false,
|
||||||
@ -77,6 +80,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
ref.watch(albumProvider).where((a) => a.shared).toList();
|
ref.watch(albumProvider).where((a) => a.shared).toList();
|
||||||
const bottomPadding = 0.20;
|
const bottomPadding = 0.20;
|
||||||
final scrollController = useDraggableScrollController();
|
final scrollController = useDraggableScrollController();
|
||||||
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
|
|
||||||
void minimize() {
|
void minimize() {
|
||||||
scrollController.animateTo(
|
scrollController.animateTo(
|
||||||
@ -133,11 +137,12 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
label: "share".tr(),
|
label: "share".tr(),
|
||||||
onPressed: enabled ? () => onShare(true) : null,
|
onPressed: enabled ? () => onShare(true) : null,
|
||||||
),
|
),
|
||||||
ControlBoxButton(
|
if (!isInLockedView)
|
||||||
iconData: Icons.link_rounded,
|
ControlBoxButton(
|
||||||
label: "control_bottom_app_bar_share_link".tr(),
|
iconData: Icons.link_rounded,
|
||||||
onPressed: enabled ? () => onShare(false) : null,
|
label: "share_link".tr(),
|
||||||
),
|
onPressed: enabled ? () => onShare(false) : null,
|
||||||
|
),
|
||||||
if (hasRemote && onArchive != null)
|
if (hasRemote && onArchive != null)
|
||||||
ControlBoxButton(
|
ControlBoxButton(
|
||||||
iconData:
|
iconData:
|
||||||
@ -153,7 +158,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
label: (unfavorite ? "unfavorite" : "favorite").tr(),
|
label: (unfavorite ? "unfavorite" : "favorite").tr(),
|
||||||
onPressed: enabled ? onFavorite : null,
|
onPressed: enabled ? onFavorite : null,
|
||||||
),
|
),
|
||||||
if (hasLocal && hasRemote && onDelete != null)
|
if (hasLocal && hasRemote && onDelete != null && !isInLockedView)
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 90),
|
constraints: const BoxConstraints(maxWidth: 90),
|
||||||
child: ControlBoxButton(
|
child: ControlBoxButton(
|
||||||
@ -166,7 +171,7 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
enabled ? () => showForceDeleteDialog(onDelete!) : null,
|
enabled ? () => showForceDeleteDialog(onDelete!) : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasRemote && onDeleteServer != null)
|
if (hasRemote && onDeleteServer != null && !isInLockedView)
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 85),
|
constraints: const BoxConstraints(maxWidth: 85),
|
||||||
child: ControlBoxButton(
|
child: ControlBoxButton(
|
||||||
@ -189,9 +194,23 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasLocal && onDeleteLocal != null)
|
if (isInLockedView)
|
||||||
ConstrainedBox(
|
ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 85),
|
constraints: const BoxConstraints(maxWidth: 110),
|
||||||
|
child: ControlBoxButton(
|
||||||
|
iconData: Icons.delete_forever,
|
||||||
|
label: "delete_dialog_title".tr(),
|
||||||
|
onPressed: enabled
|
||||||
|
? () => showForceDeleteDialog(
|
||||||
|
onDeleteServer!,
|
||||||
|
alertMsg: "delete_dialog_alert_remote",
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasLocal && onDeleteLocal != null && !isInLockedView)
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 95),
|
||||||
child: ControlBoxButton(
|
child: ControlBoxButton(
|
||||||
iconData: Icons.no_cell_outlined,
|
iconData: Icons.no_cell_outlined,
|
||||||
label: "control_bottom_app_bar_delete_from_local".tr(),
|
label: "control_bottom_app_bar_delete_from_local".tr(),
|
||||||
@ -231,6 +250,19 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
onPressed: enabled ? onEditLocation : null,
|
onPressed: enabled ? onEditLocation : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (hasRemote)
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 100),
|
||||||
|
child: ControlBoxButton(
|
||||||
|
iconData: isInLockedView
|
||||||
|
? Icons.lock_open_rounded
|
||||||
|
: Icons.lock_outline_rounded,
|
||||||
|
label: isInLockedView
|
||||||
|
? "remove_from_locked_folder".tr()
|
||||||
|
: "move_to_locked_folder".tr(),
|
||||||
|
onPressed: enabled ? onToggleLocked : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
if (!selectionAssetState.hasLocal &&
|
if (!selectionAssetState.hasLocal &&
|
||||||
selectionAssetState.selectedCount > 1 &&
|
selectionAssetState.selectedCount > 1 &&
|
||||||
onStack != null)
|
onStack != null)
|
||||||
@ -269,20 +301,40 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getInitialSize() {
|
||||||
|
if (isInLockedView) {
|
||||||
|
return 0.20;
|
||||||
|
}
|
||||||
|
if (hasRemote) {
|
||||||
|
return 0.35;
|
||||||
|
}
|
||||||
|
return bottomPadding;
|
||||||
|
}
|
||||||
|
|
||||||
|
getMaxChildSize() {
|
||||||
|
if (isInLockedView) {
|
||||||
|
return 0.20;
|
||||||
|
}
|
||||||
|
if (hasRemote) {
|
||||||
|
return 0.65;
|
||||||
|
}
|
||||||
|
return bottomPadding;
|
||||||
|
}
|
||||||
|
|
||||||
return DraggableScrollableSheet(
|
return DraggableScrollableSheet(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
initialChildSize: hasRemote ? 0.35 : bottomPadding,
|
initialChildSize: getInitialSize(),
|
||||||
minChildSize: bottomPadding,
|
minChildSize: bottomPadding,
|
||||||
maxChildSize: hasRemote ? 0.65 : bottomPadding,
|
maxChildSize: getMaxChildSize(),
|
||||||
snap: true,
|
snap: true,
|
||||||
builder: (
|
builder: (
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
ScrollController scrollController,
|
ScrollController scrollController,
|
||||||
) {
|
) {
|
||||||
return Card(
|
return Card(
|
||||||
color: context.colorScheme.surfaceContainerLow,
|
color: context.colorScheme.surfaceContainerHigh,
|
||||||
surfaceTintColor: Colors.transparent,
|
surfaceTintColor: context.colorScheme.surfaceContainerHigh,
|
||||||
elevation: 18.0,
|
elevation: 6.0,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.only(
|
||||||
topLeft: Radius.circular(12),
|
topLeft: Radius.circular(12),
|
||||||
@ -300,27 +352,27 @@ class ControlBottomAppBar extends HookConsumerWidget {
|
|||||||
const CustomDraggingHandle(),
|
const CustomDraggingHandle(),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 100,
|
height: 120,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
children: renderActionButtons(),
|
children: renderActionButtons(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasRemote)
|
if (hasRemote && !isInLockedView) ...[
|
||||||
const Divider(
|
const Divider(
|
||||||
indent: 16,
|
indent: 16,
|
||||||
endIndent: 16,
|
endIndent: 16,
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
),
|
),
|
||||||
if (hasRemote)
|
|
||||||
_AddToAlbumTitleRow(
|
_AddToAlbumTitleRow(
|
||||||
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
|
onCreateNewAlbum: enabled ? onCreateNewAlbum : null,
|
||||||
),
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (hasRemote)
|
if (hasRemote && !isInLockedView)
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
sliver: AddToAlbumSliverList(
|
sliver: AddToAlbumSliverList(
|
||||||
@ -352,12 +404,9 @@ class _AddToAlbumTitleRow extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
"add_to_album",
|
"add_to_album",
|
||||||
style: TextStyle(
|
style: context.textTheme.titleSmall,
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
).tr(),
|
).tr(),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: onCreateNewAlbum,
|
onPressed: onCreateNewAlbum,
|
||||||
|
@ -7,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||||
@ -15,6 +16,7 @@ import 'package:immich_mobile/providers/album/album.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
import 'package:immich_mobile/providers/backup/manual_upload.provider.dart';
|
||||||
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/services/album.service.dart';
|
import 'package:immich_mobile/services/album.service.dart';
|
||||||
@ -395,6 +397,32 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onToggleLockedVisibility() async {
|
||||||
|
processing.value = true;
|
||||||
|
try {
|
||||||
|
final remoteAssets = ownedRemoteSelection(
|
||||||
|
localErrorMessage: 'home_page_locked_error_local'.tr(),
|
||||||
|
ownerErrorMessage: 'home_page_locked_error_partner'.tr(),
|
||||||
|
);
|
||||||
|
if (remoteAssets.isNotEmpty) {
|
||||||
|
final isInLockedView = ref.read(inLockedViewProvider);
|
||||||
|
final visibility = isInLockedView
|
||||||
|
? AssetVisibilityEnum.timeline
|
||||||
|
: AssetVisibilityEnum.locked;
|
||||||
|
|
||||||
|
await handleSetAssetsVisibility(
|
||||||
|
ref,
|
||||||
|
context,
|
||||||
|
visibility,
|
||||||
|
remoteAssets.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
processing.value = false;
|
||||||
|
selectionEnabledHook.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<T> Function() wrapLongRunningFun<T>(
|
Future<T> Function() wrapLongRunningFun<T>(
|
||||||
Future<T> Function() fun, {
|
Future<T> Function() fun, {
|
||||||
bool showOverlay = true,
|
bool showOverlay = true,
|
||||||
@ -460,6 +488,7 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
onEditLocation: editEnabled ? onEditLocation : null,
|
onEditLocation: editEnabled ? onEditLocation : null,
|
||||||
unfavorite: unfavorite,
|
unfavorite: unfavorite,
|
||||||
unarchive: unarchive,
|
unarchive: unarchive,
|
||||||
|
onToggleLocked: onToggleLockedVisibility,
|
||||||
onRemoveFromAlbum: onRemoveFromAlbum != null
|
onRemoveFromAlbum: onRemoveFromAlbum != null
|
||||||
? wrapLongRunningFun(
|
? wrapLongRunningFun(
|
||||||
() => onRemoveFromAlbum!(selection.value),
|
() => onRemoveFromAlbum!(selection.value),
|
||||||
|
@ -15,6 +15,7 @@ import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/download.provider.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@ -46,6 +47,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
final asset = ref.watch(currentAssetProvider);
|
final asset = ref.watch(currentAssetProvider);
|
||||||
if (asset == null) {
|
if (asset == null) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
@ -277,7 +279,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
|||||||
tooltip: 'share'.tr(),
|
tooltip: 'share'.tr(),
|
||||||
): (_) => shareAsset(),
|
): (_) => shareAsset(),
|
||||||
},
|
},
|
||||||
if (asset.isImage)
|
if (asset.isImage && !isInLockedView)
|
||||||
{
|
{
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: const Icon(Icons.tune_outlined),
|
icon: const Icon(Icons.tune_outlined),
|
||||||
@ -285,7 +287,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
|||||||
tooltip: 'edit'.tr(),
|
tooltip: 'edit'.tr(),
|
||||||
): (_) => handleEdit(),
|
): (_) => handleEdit(),
|
||||||
},
|
},
|
||||||
if (isOwner)
|
if (isOwner && !isInLockedView)
|
||||||
{
|
{
|
||||||
asset.isArchived
|
asset.isArchived
|
||||||
? BottomNavigationBarItem(
|
? BottomNavigationBarItem(
|
||||||
@ -299,7 +301,7 @@ class BottomGalleryBar extends ConsumerWidget {
|
|||||||
tooltip: 'archive'.tr(),
|
tooltip: 'archive'.tr(),
|
||||||
): (_) => handleArchive(),
|
): (_) => handleArchive(),
|
||||||
},
|
},
|
||||||
if (isOwner && asset.stackCount > 0)
|
if (isOwner && asset.stackCount > 0 && !isInLockedView)
|
||||||
{
|
{
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: const Icon(Icons.burst_mode_outlined),
|
icon: const Icon(Icons.burst_mode_outlined),
|
||||||
|
@ -5,6 +5,7 @@ import 'package:immich_mobile/providers/activity_statistics.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/providers/asset.provider.dart';
|
import 'package:immich_mobile/providers/asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/motion_photo_button.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||||
@ -39,6 +40,7 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
const double iconSize = 22.0;
|
const double iconSize = 22.0;
|
||||||
final a = ref.watch(assetWatcher(asset)).value ?? asset;
|
final a = ref.watch(assetWatcher(asset)).value ?? asset;
|
||||||
final album = ref.watch(currentAlbumProvider);
|
final album = ref.watch(currentAlbumProvider);
|
||||||
@ -178,15 +180,22 @@ class TopControlAppBar extends HookConsumerWidget {
|
|||||||
shape: const Border(),
|
shape: const Border(),
|
||||||
actions: [
|
actions: [
|
||||||
if (asset.isRemote && isOwner) buildFavoriteButton(a),
|
if (asset.isRemote && isOwner) buildFavoriteButton(a),
|
||||||
if (isOwner && !isInHomePage && !(isInTrash ?? false))
|
if (isOwner &&
|
||||||
|
!isInHomePage &&
|
||||||
|
!(isInTrash ?? false) &&
|
||||||
|
!isInLockedView)
|
||||||
buildLocateButton(),
|
buildLocateButton(),
|
||||||
if (asset.livePhotoVideoId != null) const MotionPhotoButton(),
|
if (asset.livePhotoVideoId != null) const MotionPhotoButton(),
|
||||||
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
if (asset.isLocal && !asset.isRemote) buildUploadButton(),
|
||||||
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
|
if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
|
||||||
if (asset.isRemote && (isOwner || isPartner) && !asset.isTrashed)
|
if (asset.isRemote &&
|
||||||
|
(isOwner || isPartner) &&
|
||||||
|
!asset.isTrashed &&
|
||||||
|
!isInLockedView)
|
||||||
buildAddToAlbumButton(),
|
buildAddToAlbumButton(),
|
||||||
if (asset.isTrashed) buildRestoreButton(),
|
if (asset.isTrashed) buildRestoreButton(),
|
||||||
if (album != null && album.shared) buildActivitiesButton(),
|
if (album != null && album.shared && !isInLockedView)
|
||||||
|
buildActivitiesButton(),
|
||||||
buildMoreInfoButton(),
|
buildMoreInfoButton(),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -35,7 +35,9 @@ class ControlBoxButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MaterialButton(
|
return MaterialButton(
|
||||||
padding: const EdgeInsets.all(10),
|
padding: const EdgeInsets.all(10),
|
||||||
shape: const CircleBorder(),
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||||
|
),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
onLongPress: onLongPressed,
|
onLongPress: onLongPressed,
|
||||||
minWidth: 75.0,
|
minWidth: 75.0,
|
||||||
@ -47,8 +49,8 @@ class ControlBoxButton extends StatelessWidget {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(fontSize: 12.0),
|
style: const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400),
|
||||||
maxLines: 2,
|
maxLines: 3,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -40,7 +40,7 @@ class ImmichToast {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(5.0),
|
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||||
color: context.colorScheme.surfaceContainer,
|
color: context.colorScheme.surfaceContainer,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: context.colorScheme.outline.withValues(alpha: .5),
|
color: context.colorScheme.outline.withValues(alpha: .5),
|
||||||
@ -59,14 +59,23 @@ class ImmichToast {
|
|||||||
msg,
|
msg,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: getColor(toastType, context),
|
color: getColor(toastType, context),
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 15,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
positionedToastBuilder: (context, child, gravity) {
|
||||||
|
return Positioned(
|
||||||
|
top: gravity == ToastGravity.TOP ? 150 : null,
|
||||||
|
bottom: gravity == ToastGravity.BOTTOM ? 150 : null,
|
||||||
|
left: MediaQuery.of(context).size.width / 2 - 150,
|
||||||
|
right: MediaQuery.of(context).size.width / 2 - 150,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
gravity: gravity,
|
gravity: gravity,
|
||||||
toastDuration: Duration(seconds: durationInSecond),
|
toastDuration: Duration(seconds: durationInSecond),
|
||||||
);
|
);
|
||||||
|
124
mobile/lib/widgets/forms/pin_input.dart
Normal file
124
mobile/lib/widgets/forms/pin_input.dart
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:pinput/pinput.dart';
|
||||||
|
|
||||||
|
class PinInput extends StatelessWidget {
|
||||||
|
final Function(String)? onCompleted;
|
||||||
|
final Function(String)? onChanged;
|
||||||
|
final int? length;
|
||||||
|
final bool? obscureText;
|
||||||
|
final bool? autoFocus;
|
||||||
|
final bool? hasError;
|
||||||
|
final String? label;
|
||||||
|
final TextEditingController? controller;
|
||||||
|
|
||||||
|
const PinInput({
|
||||||
|
super.key,
|
||||||
|
this.onCompleted,
|
||||||
|
this.onChanged,
|
||||||
|
this.length,
|
||||||
|
this.obscureText,
|
||||||
|
this.autoFocus,
|
||||||
|
this.hasError,
|
||||||
|
this.label,
|
||||||
|
this.controller,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
getPinSize() {
|
||||||
|
final minimumPadding = 18.0;
|
||||||
|
final gapWidth = 3.0;
|
||||||
|
final screenWidth = context.width;
|
||||||
|
final pinWidth =
|
||||||
|
(screenWidth - (minimumPadding * 2) - (gapWidth * 5)) / (length ?? 6);
|
||||||
|
|
||||||
|
if (pinWidth > 60) {
|
||||||
|
return const Size(60, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
final pinHeight = pinWidth / (60 / 64);
|
||||||
|
return Size(pinWidth, pinHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
final defaultPinTheme = PinTheme(
|
||||||
|
width: getPinSize().width,
|
||||||
|
height: getPinSize().height,
|
||||||
|
textStyle: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
color: context.colorScheme.onSurface,
|
||||||
|
fontFamily: 'Overpass Mono',
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(19)),
|
||||||
|
border: Border.all(color: context.colorScheme.surfaceBright),
|
||||||
|
color: context.colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
if (label != null) ...[
|
||||||
|
Text(
|
||||||
|
label!,
|
||||||
|
style: context.textTheme.displayLarge
|
||||||
|
?.copyWith(color: context.colorScheme.onSurface.withAlpha(200)),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
],
|
||||||
|
Pinput(
|
||||||
|
controller: controller,
|
||||||
|
forceErrorState: hasError ?? false,
|
||||||
|
autofocus: autoFocus ?? false,
|
||||||
|
obscureText: obscureText ?? false,
|
||||||
|
obscuringWidget: Icon(
|
||||||
|
Icons.vpn_key_rounded,
|
||||||
|
color: context.primaryColor,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
separatorBuilder: (index) => const SizedBox(
|
||||||
|
height: 64,
|
||||||
|
width: 3,
|
||||||
|
),
|
||||||
|
cursor: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 9),
|
||||||
|
width: 18,
|
||||||
|
height: 2,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
defaultPinTheme: defaultPinTheme,
|
||||||
|
focusedPinTheme: defaultPinTheme.copyWith(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(19)),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.primaryColor.withValues(alpha: 0.5),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
color: context.colorScheme.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
errorPinTheme: defaultPinTheme.copyWith(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.error.withAlpha(15),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(19)),
|
||||||
|
border: Border.all(
|
||||||
|
color: context.colorScheme.error.withAlpha(100),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pinputAutovalidateMode: PinputAutovalidateMode.onSubmit,
|
||||||
|
length: length ?? 6,
|
||||||
|
onChanged: onChanged,
|
||||||
|
onCompleted: onCompleted,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
128
mobile/lib/widgets/forms/pin_registration_form.dart
Normal file
128
mobile/lib/widgets/forms/pin_registration_form.dart
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/forms/pin_input.dart';
|
||||||
|
|
||||||
|
class PinRegistrationForm extends HookConsumerWidget {
|
||||||
|
final Function() onDone;
|
||||||
|
|
||||||
|
const PinRegistrationForm({
|
||||||
|
super.key,
|
||||||
|
required this.onDone,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final hasError = useState(false);
|
||||||
|
final newPinCodeController = useTextEditingController();
|
||||||
|
final confirmPinCodeController = useTextEditingController();
|
||||||
|
|
||||||
|
bool validatePinCode() {
|
||||||
|
if (confirmPinCodeController.text.length != 6) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPinCodeController.text != confirmPinCodeController.text) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
createNewPinCode() async {
|
||||||
|
final isValid = validatePinCode();
|
||||||
|
if (!isValid) {
|
||||||
|
hasError.value = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ref.read(authProvider.notifier).setupPinCode(
|
||||||
|
newPinCodeController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
onDone();
|
||||||
|
} catch (error) {
|
||||||
|
hasError.value = true;
|
||||||
|
context.showSnackBar(
|
||||||
|
SnackBar(content: Text(error.toString())),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Form(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.pin_outlined,
|
||||||
|
size: 64,
|
||||||
|
color: context.primaryColor,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
SizedBox(
|
||||||
|
width: context.width * 0.7,
|
||||||
|
child: Text(
|
||||||
|
'setup_pin_code'.tr(),
|
||||||
|
style: context.textTheme.labelLarge!.copyWith(
|
||||||
|
fontSize: 24,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(
|
||||||
|
width: context.width * 0.8,
|
||||||
|
child: Text(
|
||||||
|
'new_pin_code_subtitle'.tr(),
|
||||||
|
style: context.textTheme.bodyLarge!.copyWith(
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
PinInput(
|
||||||
|
controller: newPinCodeController,
|
||||||
|
label: 'new_pin_code'.tr(),
|
||||||
|
length: 6,
|
||||||
|
autoFocus: true,
|
||||||
|
hasError: hasError.value,
|
||||||
|
onChanged: (input) {
|
||||||
|
if (input.length < 6) {
|
||||||
|
hasError.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
PinInput(
|
||||||
|
controller: confirmPinCodeController,
|
||||||
|
label: 'confirm_new_pin_code'.tr(),
|
||||||
|
length: 6,
|
||||||
|
hasError: hasError.value,
|
||||||
|
onChanged: (input) {
|
||||||
|
if (input.length < 6) {
|
||||||
|
hasError.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: createNewPinCode,
|
||||||
|
child: Text('create'.tr()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
94
mobile/lib/widgets/forms/pin_verification_form.dart
Normal file
94
mobile/lib/widgets/forms/pin_verification_form.dart
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/forms/pin_input.dart';
|
||||||
|
|
||||||
|
class PinVerificationForm extends HookConsumerWidget {
|
||||||
|
final Function(String) onSuccess;
|
||||||
|
final VoidCallback? onError;
|
||||||
|
final bool? autoFocus;
|
||||||
|
final String? description;
|
||||||
|
final IconData? icon;
|
||||||
|
final IconData? successIcon;
|
||||||
|
|
||||||
|
const PinVerificationForm({
|
||||||
|
super.key,
|
||||||
|
required this.onSuccess,
|
||||||
|
this.onError,
|
||||||
|
this.autoFocus,
|
||||||
|
this.description,
|
||||||
|
this.icon,
|
||||||
|
this.successIcon,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final hasError = useState(false);
|
||||||
|
final isVerified = useState(false);
|
||||||
|
|
||||||
|
verifyPin(String pinCode) async {
|
||||||
|
final isUnlocked =
|
||||||
|
await ref.read(authProvider.notifier).unlockPinCode(pinCode);
|
||||||
|
|
||||||
|
if (isUnlocked) {
|
||||||
|
isVerified.value = true;
|
||||||
|
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
onSuccess(pinCode);
|
||||||
|
} else {
|
||||||
|
hasError.value = true;
|
||||||
|
onError?.call();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Form(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
child: isVerified.value
|
||||||
|
? Icon(
|
||||||
|
successIcon ?? Icons.lock_open_rounded,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.green[300],
|
||||||
|
)
|
||||||
|
: Icon(
|
||||||
|
icon ?? Icons.lock_outline_rounded,
|
||||||
|
size: 64,
|
||||||
|
color: hasError.value
|
||||||
|
? context.colorScheme.error
|
||||||
|
: context.primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 36),
|
||||||
|
SizedBox(
|
||||||
|
width: context.width * 0.7,
|
||||||
|
child: Text(
|
||||||
|
description ?? 'enter_your_pin_code_subtitle'.tr(),
|
||||||
|
style: context.textTheme.labelLarge!.copyWith(
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
PinInput(
|
||||||
|
obscureText: true,
|
||||||
|
autoFocus: autoFocus,
|
||||||
|
hasError: hasError.value,
|
||||||
|
length: 6,
|
||||||
|
onChanged: (pinCode) {
|
||||||
|
if (pinCode.length < 6) {
|
||||||
|
hasError.value = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCompleted: verifyPin,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -621,6 +621,54 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.1"
|
version: "2.6.1"
|
||||||
|
flutter_secure_storage:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage
|
||||||
|
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.2.4"
|
||||||
|
flutter_secure_storage_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_linux
|
||||||
|
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.3"
|
||||||
|
flutter_secure_storage_macos:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_macos
|
||||||
|
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.3"
|
||||||
|
flutter_secure_storage_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_platform_interface
|
||||||
|
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
flutter_secure_storage_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_web
|
||||||
|
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
|
flutter_secure_storage_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: flutter_secure_storage_windows
|
||||||
|
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.2"
|
||||||
flutter_svg:
|
flutter_svg:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -976,6 +1024,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.1"
|
version: "5.1.1"
|
||||||
|
local_auth:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: local_auth
|
||||||
|
sha256: "434d854cf478f17f12ab29a76a02b3067f86a63a6d6c4eb8fbfdcfe4879c1b7b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.0"
|
||||||
|
local_auth_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: local_auth_android
|
||||||
|
sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.49"
|
||||||
|
local_auth_darwin:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: local_auth_darwin
|
||||||
|
sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.3"
|
||||||
|
local_auth_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: local_auth_platform_interface
|
||||||
|
sha256: "1b842ff177a7068442eae093b64abe3592f816afd2a533c0ebcdbe40f9d2075a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.10"
|
||||||
|
local_auth_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: local_auth_windows
|
||||||
|
sha256: bc4e66a29b0fdf751aafbec923b5bed7ad6ed3614875d8151afe2578520b2ab5
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.11"
|
||||||
logging:
|
logging:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -1264,6 +1352,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
|
pinput:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: pinput
|
||||||
|
sha256: "8a73be426a91fefec90a7f130763ca39772d547e92f19a827cf4aa02e323d35a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.0.1"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1741,6 +1837,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.2"
|
version: "2.2.2"
|
||||||
|
universal_platform:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: universal_platform
|
||||||
|
sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
url_launcher:
|
url_launcher:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -64,6 +64,9 @@ dependencies:
|
|||||||
uuid: ^4.5.1
|
uuid: ^4.5.1
|
||||||
wakelock_plus: ^1.2.10
|
wakelock_plus: ^1.2.10
|
||||||
worker_manager: ^7.2.3
|
worker_manager: ^7.2.3
|
||||||
|
local_auth: ^2.3.0
|
||||||
|
pinput: ^5.0.1
|
||||||
|
flutter_secure_storage: ^9.2.4
|
||||||
|
|
||||||
native_video_player:
|
native_video_player:
|
||||||
git:
|
git:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user