mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
chore(mobile) Improve mobile UI (#1038)
This commit is contained in:
parent
1068c4ad23
commit
d31eddf32f
@ -17,7 +17,7 @@
|
|||||||
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||||
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
|
||||||
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
|
||||||
"backup_album_selection_page_select_albums": "Select Albums",
|
"backup_album_selection_page_select_albums": "Select albums",
|
||||||
"backup_album_selection_page_selection_info": "Selection Info",
|
"backup_album_selection_page_selection_info": "Selection Info",
|
||||||
"backup_album_selection_page_total_assets": "Total unique assets",
|
"backup_album_selection_page_total_assets": "Total unique assets",
|
||||||
"backup_all": "All",
|
"backup_all": "All",
|
||||||
|
@ -21,8 +21,39 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
var box = Hive.box(userInfoBox);
|
var box = Hive.box(userInfoBox);
|
||||||
|
var cardSize = MediaQuery.of(context).size.width / 2 - 18;
|
||||||
|
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
|
||||||
final cardSize = MediaQuery.of(context).size.width / 2 - 18;
|
buildEmptyThumbnail() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDarkMode ? Colors.grey[800] : Colors.grey[200],
|
||||||
|
),
|
||||||
|
child: SizedBox(
|
||||||
|
height: cardSize,
|
||||||
|
width: cardSize,
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(Icons.no_photography),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
buildAlbumThumbnail() {
|
||||||
|
return CachedNetworkImage(
|
||||||
|
memCacheHeight: max(400, cardSize.toInt() * 3),
|
||||||
|
width: cardSize,
|
||||||
|
height: cardSize,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fadeInDuration: const Duration(milliseconds: 200),
|
||||||
|
imageUrl: getAlbumThumbnailUrl(
|
||||||
|
album,
|
||||||
|
type: ThumbnailFormat.JPEG,
|
||||||
|
),
|
||||||
|
httpHeaders: {"Authorization": "Bearer ${box.get(accessTokenKey)}"},
|
||||||
|
cacheKey: "${album.albumThumbnailAssetId}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -35,19 +66,9 @@ class AlbumThumbnailCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: CachedNetworkImage(
|
child: album.albumThumbnailAssetId == null
|
||||||
memCacheHeight: max(400, cardSize.toInt() * 3),
|
? buildEmptyThumbnail()
|
||||||
width: cardSize,
|
: buildAlbumThumbnail(),
|
||||||
height: cardSize,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
fadeInDuration: const Duration(milliseconds: 200),
|
|
||||||
imageUrl:
|
|
||||||
getAlbumThumbnailUrl(album, type: ThumbnailFormat.JPEG),
|
|
||||||
httpHeaders: {
|
|
||||||
"Authorization": "Bearer ${box.get(accessTokenKey)}"
|
|
||||||
},
|
|
||||||
cacheKey: "${album.albumThumbnailAssetId}",
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8.0),
|
padding: const EdgeInsets.only(top: 8.0),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:cancellation_token_http/http.dart';
|
import 'package:cancellation_token_http/http.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hive_flutter/hive_flutter.dart';
|
import 'package:hive_flutter/hive_flutter.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/hive_box.dart';
|
import 'package:immich_mobile/constants/hive_box.dart';
|
||||||
@ -68,6 +69,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
final AuthenticationState _authState;
|
final AuthenticationState _authState;
|
||||||
final BackgroundService _backgroundService;
|
final BackgroundService _backgroundService;
|
||||||
final Ref ref;
|
final Ref ref;
|
||||||
|
var isGettingBackupInfo = false;
|
||||||
|
|
||||||
///
|
///
|
||||||
/// UI INTERACTION
|
/// UI INTERACTION
|
||||||
@ -172,9 +174,10 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
/// Get all album on the device
|
/// Get all album on the device
|
||||||
/// Get all selected and excluded album from the user's persistent storage
|
/// Get all selected and excluded album from the user's persistent storage
|
||||||
/// If this is the first time performing backup - set the default selected album to be
|
/// If this is the first time performing backup - set the default selected album to be
|
||||||
/// the one that has all assets (Recent on Android, Recents on iOS)
|
/// the one that has all assets (`Recent` on Android, `Recents` on iOS)
|
||||||
///
|
///
|
||||||
Future<void> _getBackupAlbumsInfo() async {
|
Future<void> _getBackupAlbumsInfo() async {
|
||||||
|
Stopwatch stopwatch = Stopwatch()..start();
|
||||||
// Get all albums on the device
|
// Get all albums on the device
|
||||||
List<AvailableAlbum> availableAlbums = [];
|
List<AvailableAlbum> availableAlbums = [];
|
||||||
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
List<AssetPathEntity> albums = await PhotoManager.getAssetPathList(
|
||||||
@ -182,6 +185,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
type: RequestType.common,
|
type: RequestType.common,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
log.info('Found ${albums.length} local albums');
|
||||||
|
|
||||||
for (AssetPathEntity album in albums) {
|
for (AssetPathEntity album in albums) {
|
||||||
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
AvailableAlbum availableAlbum = AvailableAlbum(albumEntity: album);
|
||||||
|
|
||||||
@ -293,6 +298,8 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
log.severe("Failed to generate album from id", e, stackTrace);
|
log.severe("Failed to generate album from id", e, stackTrace);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugPrint("_getBackupAlbumsInfo takes ${stopwatch.elapsedMilliseconds}ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
///
|
||||||
@ -364,25 +371,29 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
/// Get all necessary information for calculating the available albums,
|
/// Get all necessary information for calculating the available albums,
|
||||||
/// which albums are selected or excluded
|
/// which albums are selected or excluded
|
||||||
/// and then update the UI according to those information
|
/// and then update the UI according to those information
|
||||||
///
|
|
||||||
Future<void> getBackupInfo() async {
|
Future<void> getBackupInfo() async {
|
||||||
final bool isEnabled = await _backgroundService.isBackgroundBackupEnabled();
|
if (!isGettingBackupInfo) {
|
||||||
|
isGettingBackupInfo = true;
|
||||||
|
|
||||||
|
var isEnabled = await _backgroundService.isBackgroundBackupEnabled();
|
||||||
|
|
||||||
state = state.copyWith(backgroundBackup: isEnabled);
|
state = state.copyWith(backgroundBackup: isEnabled);
|
||||||
|
|
||||||
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
if (state.backupProgress != BackUpProgressEnum.inBackground) {
|
||||||
await _getBackupAlbumsInfo();
|
await _getBackupAlbumsInfo();
|
||||||
await _updateServerInfo();
|
await _updateServerInfo();
|
||||||
await _updateBackupAssetCount();
|
await _updateBackupAssetCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isGettingBackupInfo = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
/// Save user selection of selected albums and excluded albums to
|
/// Save user selection of selected albums and excluded albums to
|
||||||
/// Hive database
|
/// Hive database
|
||||||
///
|
|
||||||
void _updatePersistentAlbumsSelection() {
|
void _updatePersistentAlbumsSelection() {
|
||||||
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
final epoch = DateTime.fromMillisecondsSinceEpoch(0, isUtc: true);
|
||||||
Box<HiveBackupAlbums> backupAlbumInfoBox =
|
Box<HiveBackupAlbums> backupAlbumInfoBox =
|
||||||
@ -402,9 +413,7 @@ class BackupNotifier extends StateNotifier<BackUpState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
///
|
|
||||||
/// Invoke backup process
|
/// Invoke backup process
|
||||||
///
|
|
||||||
Future<void> startBackupProcess() async {
|
Future<void> startBackupProcess() async {
|
||||||
assert(state.backupProgress == BackUpProgressEnum.idle);
|
assert(state.backupProgress == BackUpProgressEnum.idle);
|
||||||
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
|
state = state.copyWith(backupProgress: BackUpProgressEnum.inProgress);
|
||||||
|
@ -36,7 +36,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
|
|||||||
title: Column(
|
title: Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
"${album.name} (${album.assetCountAsync})",
|
album.name,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -5,6 +5,7 @@ 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/immich_colors.dart';
|
import 'package:immich_mobile/constants/immich_colors.dart';
|
||||||
|
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||||
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
|
import 'package:immich_mobile/modules/backup/ui/album_info_card.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
@ -14,10 +15,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
const BackupAlbumSelectionPage({Key? key}) : super(key: key);
|
const BackupAlbumSelectionPage({Key? key}) : super(key: key);
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final availableAlbums = ref.watch(backupProvider).availableAlbums;
|
// final availableAlbums = ref.watch(backupProvider).availableAlbums;
|
||||||
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
|
||||||
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
|
||||||
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
|
||||||
|
final albums = useState<List<AvailableAlbum>>(
|
||||||
|
ref.watch(backupProvider).availableAlbums,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
@ -28,7 +32,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
buildAlbumSelectionList() {
|
buildAlbumSelectionList() {
|
||||||
if (availableAlbums.isEmpty) {
|
if (albums.value.isEmpty) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: ImmichLoadingIndicator(),
|
child: ImmichLoadingIndicator(),
|
||||||
);
|
);
|
||||||
@ -38,17 +42,17 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
height: 265,
|
height: 265,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
itemCount: availableAlbums.length,
|
itemCount: albums.value.length,
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const BouncingScrollPhysics(),
|
||||||
itemBuilder: ((context, index) {
|
itemBuilder: ((context, index) {
|
||||||
var thumbnailData = availableAlbums[index].thumbnailData;
|
var thumbnailData = albums.value[index].thumbnailData;
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: index == 0
|
padding: index == 0
|
||||||
? const EdgeInsets.only(left: 16.00)
|
? const EdgeInsets.only(left: 16.00)
|
||||||
: const EdgeInsets.all(0),
|
: const EdgeInsets.all(0),
|
||||||
child: AlbumInfoCard(
|
child: AlbumInfoCard(
|
||||||
imageData: thumbnailData,
|
imageData: thumbnailData,
|
||||||
albumInfo: availableAlbums[index],
|
albumInfo: albums.value[index],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@ -79,15 +83,13 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
child: Chip(
|
child: Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
album.name,
|
album.name,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: Theme.of(context).brightness == Brightness.dark
|
color: isDarkTheme ? Colors.black : Colors.white,
|
||||||
? Colors.black
|
|
||||||
: Colors.white,
|
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -119,7 +121,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
child: Chip(
|
child: Chip(
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
album.name,
|
album.name,
|
||||||
@ -143,6 +145,46 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
}).toSet();
|
}).toSet();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildSearchBar() {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0),
|
||||||
|
child: TextFormField(
|
||||||
|
onChanged: (searchValue) {
|
||||||
|
albums.value = ref
|
||||||
|
.watch(backupProvider)
|
||||||
|
.availableAlbums
|
||||||
|
.where(
|
||||||
|
(album) => album.name
|
||||||
|
.toLowerCase()
|
||||||
|
.contains(searchValue.toLowerCase()),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8.0,
|
||||||
|
vertical: 8.0,
|
||||||
|
),
|
||||||
|
hintText: "Search",
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: isDarkTheme ? Colors.white : Colors.grey,
|
||||||
|
fontSize: 14.0,
|
||||||
|
),
|
||||||
|
prefixIcon: const Icon(
|
||||||
|
Icons.search,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: isDarkTheme ? Colors.white30 : Colors.grey[200],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
@ -188,7 +230,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
child: Card(
|
child: Card(
|
||||||
margin: const EdgeInsets.all(0),
|
margin: const EdgeInsets.all(0),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(5),
|
borderRadius: BorderRadius.circular(10),
|
||||||
side: BorderSide(
|
side: BorderSide(
|
||||||
color: isDarkTheme
|
color: isDarkTheme
|
||||||
? const Color.fromARGB(255, 0, 0, 0)
|
? const Color.fromARGB(255, 0, 0, 0)
|
||||||
@ -225,8 +267,11 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
"backup_album_selection_page_albums_device"
|
"backup_album_selection_page_albums_device".tr(
|
||||||
.tr(args: [availableAlbums.length.toString()]),
|
args: [
|
||||||
|
ref.watch(backupProvider).availableAlbums.length.toString()
|
||||||
|
],
|
||||||
|
),
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
|
||||||
),
|
),
|
||||||
subtitle: Padding(
|
subtitle: Padding(
|
||||||
@ -254,7 +299,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
elevation: 5,
|
elevation: 5,
|
||||||
title: Text(
|
title: Text(
|
||||||
@ -284,6 +329,8 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
buildSearchBar(),
|
||||||
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
child: buildAlbumSelectionList(),
|
child: buildAlbumSelectionList(),
|
||||||
|
@ -12,6 +12,7 @@ import 'package:immich_mobile/shared/providers/api.provider.dart';
|
|||||||
import 'package:immich_mobile/shared/services/api.service.dart';
|
import 'package:immich_mobile/shared/services/api.service.dart';
|
||||||
import 'package:immich_mobile/utils/openapi_extensions.dart';
|
import 'package:immich_mobile/utils/openapi_extensions.dart';
|
||||||
import 'package:immich_mobile/utils/tuple.dart';
|
import 'package:immich_mobile/utils/tuple.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
final assetServiceProvider = Provider(
|
final assetServiceProvider = Provider(
|
||||||
@ -26,11 +27,13 @@ class AssetService {
|
|||||||
final ApiService _apiService;
|
final ApiService _apiService;
|
||||||
final BackupService _backupService;
|
final BackupService _backupService;
|
||||||
final BackgroundService _backgroundService;
|
final BackgroundService _backgroundService;
|
||||||
|
final log = Logger('AssetService');
|
||||||
|
|
||||||
AssetService(this._apiService, this._backupService, this._backgroundService);
|
AssetService(this._apiService, this._backupService, this._backgroundService);
|
||||||
|
|
||||||
/// Returns `null` if the server state did not change, else list of assets
|
/// Returns `null` if the server state did not change, else list of assets
|
||||||
Future<List<Asset>?> getRemoteAssets({required bool hasCache}) async {
|
Future<List<Asset>?> getRemoteAssets({required bool hasCache}) async {
|
||||||
|
try {
|
||||||
final Box box = Hive.box(userInfoBox);
|
final Box box = Hive.box(userInfoBox);
|
||||||
final Pair<List<AssetResponseDto>, String?>? remote = await _apiService
|
final Pair<List<AssetResponseDto>, String?>? remote = await _apiService
|
||||||
.assetApi
|
.assetApi
|
||||||
@ -40,6 +43,10 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
box.put(assetEtagKey, remote.second);
|
box.put(assetEtagKey, remote.second);
|
||||||
return remote.first.map(Asset.remote).toList(growable: false);
|
return remote.first.map(Asset.remote).toList(growable: false);
|
||||||
|
} catch (e, stack) {
|
||||||
|
log.severe('Error while getting remote assets', e, stack);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// if [urgent] is `true`, do not block by waiting on the background service
|
/// if [urgent] is `true`, do not block by waiting on the background service
|
||||||
|
@ -32,7 +32,9 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
snap: false,
|
snap: false,
|
||||||
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
|
backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(5)),
|
borderRadius: BorderRadius.all(
|
||||||
|
Radius.circular(5),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
leading: Builder(
|
leading: Builder(
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -20,6 +22,7 @@ import 'package:immich_mobile/shared/providers/asset.provider.dart';
|
|||||||
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
import 'package:immich_mobile/shared/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
import 'package:immich_mobile/shared/providers/websocket.provider.dart';
|
||||||
import 'package:immich_mobile/shared/services/share.service.dart';
|
import 'package:immich_mobile/shared/services/share.service.dart';
|
||||||
|
import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
|
||||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||||
import 'package:openapi/api.dart';
|
import 'package:openapi/api.dart';
|
||||||
|
|
||||||
@ -37,6 +40,8 @@ class HomePage extends HookConsumerWidget {
|
|||||||
final albums = ref.watch(albumProvider);
|
final albums = ref.watch(albumProvider);
|
||||||
final albumService = ref.watch(albumServiceProvider);
|
final albumService = ref.watch(albumServiceProvider);
|
||||||
|
|
||||||
|
final tipOneOpacity = useState(0.0);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
ref.read(websocketProvider.notifier).connect();
|
ref.read(websocketProvider.notifier).connect();
|
||||||
@ -146,6 +151,49 @@ class HomePage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildLoadingIndicator() {
|
||||||
|
Timer(const Duration(seconds: 2), () {
|
||||||
|
tipOneOpacity.value = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const ImmichLoadingIndicator(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 16.0),
|
||||||
|
child: Text(
|
||||||
|
'Building the timeline',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 16,
|
||||||
|
color: Theme.of(context).primaryColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AnimatedOpacity(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
opacity: tipOneOpacity.value,
|
||||||
|
child: const SizedBox(
|
||||||
|
width: 250,
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(top: 8.0),
|
||||||
|
child: Text(
|
||||||
|
'If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).',
|
||||||
|
textAlign: TextAlign.justify,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
bottom: !multiselectEnabled.state,
|
bottom: !multiselectEnabled.state,
|
||||||
top: true,
|
top: true,
|
||||||
@ -164,10 +212,12 @@ class HomePage extends HookConsumerWidget {
|
|||||||
top: selectionEnabledHook.value ? 0 : 60,
|
top: selectionEnabledHook.value ? 0 : 60,
|
||||||
bottom: 0.0,
|
bottom: 0.0,
|
||||||
),
|
),
|
||||||
child: ImmichAssetGrid(
|
child: ref.watch(assetProvider).isEmpty
|
||||||
|
? buildLoadingIndicator()
|
||||||
|
: ImmichAssetGrid(
|
||||||
renderList: renderList,
|
renderList: renderList,
|
||||||
assetsPerRow:
|
assetsPerRow: appSettingService
|
||||||
appSettingService.getSetting(AppSettingsEnum.tilesPerRow),
|
.getSetting(AppSettingsEnum.tilesPerRow),
|
||||||
showStorageIndicator: appSettingService
|
showStorageIndicator: appSettingService
|
||||||
.getSetting(AppSettingsEnum.storageIndicator),
|
.getSetting(AppSettingsEnum.storageIndicator),
|
||||||
listener: selectionListener,
|
listener: selectionListener,
|
||||||
|
@ -83,6 +83,13 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
populateTestLoginInfo() {
|
||||||
|
usernameController.text = 'testuser@email.com';
|
||||||
|
passwordController.text = 'password';
|
||||||
|
serverEndpointController.text = 'http://10.1.15.216:2283/api';
|
||||||
|
isSaveLoginInfo.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 300),
|
constraints: const BoxConstraints(maxWidth: 300),
|
||||||
@ -92,11 +99,14 @@ class LoginForm extends HookConsumerWidget {
|
|||||||
runSpacing: 16,
|
runSpacing: 16,
|
||||||
alignment: WrapAlignment.center,
|
alignment: WrapAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Image(
|
GestureDetector(
|
||||||
|
onDoubleTap: () => populateTestLoginInfo(),
|
||||||
|
child: const Image(
|
||||||
image: AssetImage('assets/immich-logo-no-outline.png'),
|
image: AssetImage('assets/immich-logo-no-outline.png'),
|
||||||
width: 100,
|
width: 100,
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Text(
|
Text(
|
||||||
'IMMICH',
|
'IMMICH',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
@ -15,7 +15,10 @@ class ImmichLoadingIndicator extends StatelessWidget {
|
|||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(15),
|
padding: const EdgeInsets.all(15),
|
||||||
child: const CircularProgressIndicator(color: Colors.white),
|
child: const CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Binary file not shown.
Loading…
Reference in New Issue
Block a user