feature(mobile): Hardening synchronization mechanism + Pull to refresh (#2085)

* fix(mobile): allow syncing duplicate local IDs

* enable to run isar unit tests on CI

* serialize sync operations, add pull to refresh on timeline

---------

Co-authored-by: Fynn Petersen-Frey <zoodyy@users.noreply.github.com>
This commit is contained in:
Fynn Petersen-Frey 2023-03-27 04:35:52 +02:00 committed by GitHub
parent 1a94530935
commit cae37657e9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 653 additions and 249 deletions

View file

@ -53,7 +53,7 @@ class AlbumThumbnailCard extends StatelessWidget {
// Add the owner name to the subtitle
String? owner;
if (showOwner) {
if (album.ownerId == Store.get(StoreKey.userRemoteId)) {
if (album.ownerId == Store.get(StoreKey.currentUser).id) {
owner = 'album_thumbnail_owned'.tr();
} else if (album.ownerName != null) {
owner = 'album_thumbnail_shared_by'.tr(args: [album.ownerName!]);

View file

@ -17,7 +17,7 @@ class SharingPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider);
final userId = store.Store.get(store.StoreKey.userRemoteId);
final userId = store.Store.get(store.StoreKey.currentUser).id;
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
useEffect(

View file

@ -17,10 +17,12 @@ class ImmichAssetGrid extends HookConsumerWidget {
final bool selectionActive;
final List<Asset> assets;
final RenderList? renderList;
final Future<void> Function()? onRefresh;
const ImmichAssetGrid({
super.key,
required this.assets,
this.onRefresh,
this.renderList,
this.assetsPerRow,
this.showStorageIndicator,
@ -62,11 +64,12 @@ class ImmichAssetGrid extends HookConsumerWidget {
enabled: enableHeroAnimations.value,
child: ImmichAssetGridView(
allAssets: assets,
assetsPerRow: assetsPerRow
?? settings.getSetting(AppSettingsEnum.tilesPerRow),
onRefresh: onRefresh,
assetsPerRow: assetsPerRow ??
settings.getSetting(AppSettingsEnum.tilesPerRow),
listener: listener,
showStorageIndicator: showStorageIndicator
?? settings.getSetting(AppSettingsEnum.storageIndicator),
showStorageIndicator: showStorageIndicator ??
settings.getSetting(AppSettingsEnum.storageIndicator),
renderList: renderList!,
margin: margin,
selectionActive: selectionActive,
@ -76,26 +79,25 @@ class ImmichAssetGrid extends HookConsumerWidget {
}
return renderListFuture.when(
data: (renderList) =>
WillPopScope(
onWillPop: onWillPop,
child: HeroMode(
enabled: enableHeroAnimations.value,
child: ImmichAssetGridView(
allAssets: assets,
assetsPerRow: assetsPerRow
?? settings.getSetting(AppSettingsEnum.tilesPerRow),
listener: listener,
showStorageIndicator: showStorageIndicator
?? settings.getSetting(AppSettingsEnum.storageIndicator),
renderList: renderList,
margin: margin,
selectionActive: selectionActive,
),
data: (renderList) => WillPopScope(
onWillPop: onWillPop,
child: HeroMode(
enabled: enableHeroAnimations.value,
child: ImmichAssetGridView(
allAssets: assets,
onRefresh: onRefresh,
assetsPerRow: assetsPerRow ??
settings.getSetting(AppSettingsEnum.tilesPerRow),
listener: listener,
showStorageIndicator: showStorageIndicator ??
settings.getSetting(AppSettingsEnum.storageIndicator),
renderList: renderList,
margin: margin,
selectionActive: selectionActive,
),
),
error: (err, stack) =>
Center(child: Text("$err")),
),
error: (err, stack) => Center(child: Text("$err")),
loading: () => const Center(
child: ImmichLoadingIndicator(),
),

View file

@ -199,21 +199,23 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
addRepaintBoundaries: true,
);
if (!useDragScrolling) {
return listWidget;
}
final child = useDragScrolling
? DraggableScrollbar.semicircle(
scrollStateListener: dragScrolling,
itemPositionsListener: _itemPositionsListener,
controller: _itemScrollController,
backgroundColor: Theme.of(context).hintColor,
labelTextBuilder: _labelBuilder,
labelConstraints: const BoxConstraints(maxHeight: 28),
scrollbarAnimationDuration: const Duration(seconds: 1),
scrollbarTimeToFade: const Duration(seconds: 4),
child: listWidget,
)
: listWidget;
return DraggableScrollbar.semicircle(
scrollStateListener: dragScrolling,
itemPositionsListener: _itemPositionsListener,
controller: _itemScrollController,
backgroundColor: Theme.of(context).hintColor,
labelTextBuilder: _labelBuilder,
labelConstraints: const BoxConstraints(maxHeight: 28),
scrollbarAnimationDuration: const Duration(seconds: 1),
scrollbarTimeToFade: const Duration(seconds: 4),
child: listWidget,
);
return widget.onRefresh == null
? child
: RefreshIndicator(onRefresh: widget.onRefresh!, child: child);
}
@override
@ -248,7 +250,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
}
void _scrollToTop() {
// for some reason, this is necessary as well in order
// for some reason, this is necessary as well in order
// to correctly reposition the drag thumb scroll bar
_itemScrollController.jumpTo(
index: 0,
@ -281,6 +283,7 @@ class ImmichAssetGridView extends StatefulWidget {
final ImmichAssetGridSelectionListener? listener;
final bool selectionActive;
final List<Asset> allAssets;
final Future<void> Function()? onRefresh;
const ImmichAssetGridView({
super.key,
@ -291,6 +294,7 @@ class ImmichAssetGridView extends StatefulWidget {
this.listener,
this.margin = 5.0,
this.selectionActive = false,
this.onRefresh,
});
@override

View file

@ -43,6 +43,7 @@ class HomePage extends HookConsumerWidget {
final albumService = ref.watch(albumServiceProvider);
final tipOneOpacity = useState(0.0);
final refreshCount = useState(0);
useEffect(
() {
@ -182,6 +183,22 @@ class HomePage extends HookConsumerWidget {
}
}
Future<void> refreshAssets() async {
debugPrint("refreshCount.value ${refreshCount.value}");
final fullRefresh = refreshCount.value > 0;
await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh);
if (fullRefresh) {
// refresh was forced: user requested another refresh within 2 seconds
refreshCount.value = 0;
} else {
refreshCount.value++;
// set counter back to 0 if user does not request refresh again
Timer(const Duration(seconds: 2), () {
refreshCount.value = 0;
});
}
}
buildLoadingIndicator() {
Timer(const Duration(seconds: 2), () {
tipOneOpacity.value = 1;
@ -241,6 +258,7 @@ class HomePage extends HookConsumerWidget {
.getSetting(AppSettingsEnum.storageIndicator),
listener: selectionListener,
selectionActive: selectionEnabledHook.value,
onRefresh: refreshAssets,
),
if (selectionEnabledHook.value)
SafeArea(

View file

@ -78,7 +78,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
await Future.wait([
_apiService.authenticationApi.logout(),
Store.delete(StoreKey.assetETag),
Store.delete(StoreKey.userRemoteId),
Store.delete(StoreKey.currentUser),
Store.delete(StoreKey.accessToken),
]);
@ -133,7 +132,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
var deviceInfo = await _deviceInfoService.getDeviceInfo();
Store.put(StoreKey.deviceId, deviceInfo["deviceId"]);
Store.put(StoreKey.deviceIdHash, fastHash(deviceInfo["deviceId"]));
Store.put(StoreKey.userRemoteId, userResponseDto.id);
Store.put(StoreKey.currentUser, User.fromDto(userResponseDto));
Store.put(StoreKey.serverUrl, serverUrl);
Store.put(StoreKey.accessToken, accessToken);