From ea3a14ed2545beb78ce59c6f0b19487705becdb3 Mon Sep 17 00:00:00 2001 From: shenlong <139912620+shenlong-tanwen@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:20:39 +0530 Subject: [PATCH] feat(mobile): add album asset sync (#19522) * feat(mobile): add album asset sync * add SyncAlbumToAssetDeleteV1 to openapi-spec * update delete queries to use where in statements * clear remote album when clear remote data * fix: bad merge * fix: bad merge * fix: _SyncAckV1 return type --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: wuzihao051119 Co-authored-by: Alex Tran --- .../drift_schemas/main/drift_schema_v1.json | Bin 21688 -> 21740 bytes .../lib/domain/models/album/album.model.dart | 79 ++++++ .../models/{ => album}/local_album.model.dart | 0 .../domain/services/local_sync.service.dart | 2 +- .../domain/services/sync_stream.service.dart | 71 ++++- .../lib/domain/services/timeline.service.dart | 7 + .../entities/local_album.entity.dart | 2 +- .../entities/local_album.entity.drift.dart | Bin 21561 -> 21567 bytes .../entities/remote_album.entity.dart | 34 +++ .../entities/remote_album.entity.drift.dart | Bin 0 -> 38567 bytes .../entities/remote_album_asset.entity.dart | 17 ++ .../remote_album_asset.entity.drift.dart | Bin 0 -> 22086 bytes .../entities/remote_album_user.entity.dart | 20 ++ .../remote_album_user.entity.drift.dart | Bin 0 -> 24035 bytes .../entities/remote_asset.entity.dart | 19 ++ .../repositories/db.repository.dart | 6 + .../repositories/db.repository.drift.dart | Bin 6059 -> 9078 bytes .../repositories/local_album.repository.dart | 2 +- .../repositories/remote_album.repository.dart | 45 +++ .../repositories/sync_api.repository.dart | 28 +- .../repositories/sync_stream.repository.dart | 261 +++++++++++++----- .../repositories/timeline.repository.dart | 57 ++++ .../pages/dev/feat_in_development.page.dart | 3 + .../pages/dev/media_stat.page.dart | 53 +++- .../pages/dev/remote_timeline.page.dart | 32 +++ .../infrastructure/album.provider.dart | 5 + mobile/lib/repositories/auth.repository.dart | 6 + mobile/lib/routing/router.dart | 5 + mobile/lib/routing/router.gr.dart | 37 +++ mobile/openapi/README.md | Bin 37817 -> 37881 bytes mobile/openapi/lib/api.dart | Bin 13370 -> 13419 bytes mobile/openapi/lib/api_client.dart | Bin 33683 -> 33783 bytes .../model/sync_album_to_asset_delete_v1.dart | Bin 0 -> 3231 bytes .../services/sync_stream_service_test.dart | 24 +- mobile/test/fixtures/album.stub.dart | 2 +- .../local_album_repository_test.dart | 2 +- mobile/test/test_utils/medium_factory.dart | 2 +- open-api/immich-openapi-specs.json | 15 + server/src/dtos/sync.dto.ts | 1 + 39 files changed, 744 insertions(+), 93 deletions(-) create mode 100644 mobile/lib/domain/models/album/album.model.dart rename mobile/lib/domain/models/{ => album}/local_album.model.dart (100%) create mode 100644 mobile/lib/infrastructure/entities/remote_album.entity.dart create mode 100644 mobile/lib/infrastructure/entities/remote_album.entity.drift.dart create mode 100644 mobile/lib/infrastructure/entities/remote_album_asset.entity.dart create mode 100644 mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart create mode 100644 mobile/lib/infrastructure/entities/remote_album_user.entity.dart create mode 100644 mobile/lib/infrastructure/entities/remote_album_user.entity.drift.dart create mode 100644 mobile/lib/infrastructure/repositories/remote_album.repository.dart create mode 100644 mobile/lib/presentation/pages/dev/remote_timeline.page.dart create mode 100644 mobile/openapi/lib/model/sync_album_to_asset_delete_v1.dart diff --git a/mobile/drift_schemas/main/drift_schema_v1.json b/mobile/drift_schemas/main/drift_schema_v1.json index 1b2c86026c719c9f4dc201728bcbe3836c52a9e2..3663d35b5691ddd9d3652e8db754c34309d9b0c3 100644 GIT binary patch delta 87 zcmdn7lJU(-#tqY4CdV>Mvc<%x1JPth3G2"} + }'''; + } + + @override + bool operator ==(Object other) { + if (other is! Album) return false; + if (identical(this, other)) return true; + return id == other.id && + name == other.name && + ownerId == other.ownerId && + description == other.description && + createdAt == other.createdAt && + updatedAt == other.updatedAt && + thumbnailAssetId == other.thumbnailAssetId && + isActivityEnabled == other.isActivityEnabled && + order == other.order; + } + + @override + int get hashCode { + return id.hashCode ^ + name.hashCode ^ + ownerId.hashCode ^ + description.hashCode ^ + createdAt.hashCode ^ + updatedAt.hashCode ^ + thumbnailAssetId.hashCode ^ + isActivityEnabled.hashCode ^ + order.hashCode; + } +} diff --git a/mobile/lib/domain/models/local_album.model.dart b/mobile/lib/domain/models/album/local_album.model.dart similarity index 100% rename from mobile/lib/domain/models/local_album.model.dart rename to mobile/lib/domain/models/album/local_album.model.dart diff --git a/mobile/lib/domain/services/local_sync.service.dart b/mobile/lib/domain/services/local_sync.service.dart index 882bb801e..7a61dbc0c 100644 --- a/mobile/lib/domain/services/local_sync.service.dart +++ b/mobile/lib/domain/services/local_sync.service.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/local_album.model.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; import 'package:immich_mobile/platform/native_sync_api.g.dart'; import 'package:immich_mobile/presentation/pages/dev/dev_logger.dart'; diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart index d612e8336..2160018df 100644 --- a/mobile/lib/domain/services/sync_stream.service.dart +++ b/mobile/lib/domain/services/sync_stream.service.dart @@ -76,11 +76,76 @@ class SyncStreamService { case SyncEntityType.assetExifV1: return _syncStreamRepository.updateAssetsExifV1(data.cast()); case SyncEntityType.partnerAssetV1: - return _syncStreamRepository.updatePartnerAssetsV1(data.cast()); + return _syncStreamRepository.updateAssetsV1( + data.cast(), + debugLabel: 'partner', + ); + case SyncEntityType.partnerAssetBackfillV1: + return _syncStreamRepository.updateAssetsV1( + data.cast(), + debugLabel: 'partner backfill', + ); case SyncEntityType.partnerAssetDeleteV1: - return _syncStreamRepository.deletePartnerAssetsV1(data.cast()); + return _syncStreamRepository.deleteAssetsV1( + data.cast(), + debugLabel: "partner", + ); case SyncEntityType.partnerAssetExifV1: - return _syncStreamRepository.updatePartnerAssetsExifV1(data.cast()); + return _syncStreamRepository.updateAssetsExifV1( + data.cast(), + debugLabel: 'partner', + ); + case SyncEntityType.partnerAssetExifBackfillV1: + return _syncStreamRepository.updateAssetsExifV1( + data.cast(), + debugLabel: 'partner backfill', + ); + case SyncEntityType.albumV1: + return _syncStreamRepository.updateAlbumsV1(data.cast()); + case SyncEntityType.albumDeleteV1: + return _syncStreamRepository.deleteAlbumsV1(data.cast()); + case SyncEntityType.albumUserV1: + return _syncStreamRepository.updateAlbumUsersV1(data.cast()); + case SyncEntityType.albumUserBackfillV1: + return _syncStreamRepository.updateAlbumUsersV1( + data.cast(), + debugLabel: 'backfill', + ); + case SyncEntityType.albumUserDeleteV1: + return _syncStreamRepository.deleteAlbumUsersV1(data.cast()); + case SyncEntityType.albumAssetV1: + return _syncStreamRepository.updateAssetsV1( + data.cast(), + debugLabel: 'album', + ); + case SyncEntityType.albumAssetBackfillV1: + return _syncStreamRepository.updateAssetsV1( + data.cast(), + debugLabel: 'album backfill', + ); + case SyncEntityType.albumAssetExifV1: + return _syncStreamRepository.updateAssetsExifV1( + data.cast(), + debugLabel: 'album', + ); + case SyncEntityType.albumAssetExifBackfillV1: + return _syncStreamRepository.updateAssetsExifV1( + data.cast(), + debugLabel: 'album backfill', + ); + case SyncEntityType.albumToAssetV1: + return _syncStreamRepository.updateAlbumToAssetsV1(data.cast()); + case SyncEntityType.albumToAssetBackfillV1: + return _syncStreamRepository.updateAlbumToAssetsV1( + data.cast(), + debugLabel: 'backfill', + ); + case SyncEntityType.albumToAssetDeleteV1: + return _syncStreamRepository.deleteAlbumToAssetsV1(data.cast()); + // No-op. SyncAckV1 entities are checkpoints in the sync stream + // to acknowledge that the client has processed all the backfill events + case SyncEntityType.syncAckV1: + return; default: _logger.warning("Unknown sync data type: $type"); } diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 225f7a89b..1dd2dfa15 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -45,6 +45,13 @@ class TimelineFactory { bucketSource: () => _timelineRepository.watchLocalBucket(albumId, groupBy: groupBy), ); + + TimelineService remoteAlbum({required String albumId}) => TimelineService( + assetSource: (offset, count) => _timelineRepository + .getRemoteBucketAssets(albumId, offset: offset, count: count), + bucketSource: () => + _timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy), + ); } class TimelineService { diff --git a/mobile/lib/infrastructure/entities/local_album.entity.dart b/mobile/lib/infrastructure/entities/local_album.entity.dart index 9657173c3..398c5d4e4 100644 --- a/mobile/lib/infrastructure/entities/local_album.entity.dart +++ b/mobile/lib/infrastructure/entities/local_album.entity.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; class LocalAlbumEntity extends Table with DriftDefaultsMixin { diff --git a/mobile/lib/infrastructure/entities/local_album.entity.drift.dart b/mobile/lib/infrastructure/entities/local_album.entity.drift.dart index ff6226ba3f25213c0ab8650d69676e366ce849ae..06f65e25d8605e8eb76056f4de4a4e56233571f8 100644 GIT binary patch delta 18 acmdnFf^q)}#tE-k6LXSEb2q;74+a2IjtH3m delta 12 UcmdnLf^p{x#tE-Ce)J0l04j$Di2wiq diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.dart b/mobile/lib/infrastructure/entities/remote_album.entity.dart new file mode 100644 index 000000000..377d67446 --- /dev/null +++ b/mobile/lib/infrastructure/entities/remote_album.entity.dart @@ -0,0 +1,34 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; +import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; + +class RemoteAlbumEntity extends Table with DriftDefaultsMixin { + const RemoteAlbumEntity(); + + TextColumn get id => text()(); + + TextColumn get name => text()(); + + TextColumn get description => text().withDefault(const Constant(''))(); + + DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)(); + + DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + + TextColumn get ownerId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get thumbnailAssetId => text() + .references(RemoteAssetEntity, #id, onDelete: KeyAction.setNull) + .nullable()(); + + BoolColumn get isActivityEnabled => + boolean().withDefault(const Constant(true))(); + + IntColumn get order => intEnum()(); + + @override + Set get primaryKey => {id}; +} diff --git a/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album.entity.drift.dart new file mode 100644 index 0000000000000000000000000000000000000000..bc13c8cb5cdeb1897cc39093f8e128f7136e68e6 GIT binary patch literal 38567 zcmeG_YjYGwlHd6i#fD<16`>emuf4vIK{{E8Tf7D~KzKh8grYUmLT%4Xi|!tSPS$_F z%&e-c?0WR{=z+~1d;rvyRh5OM+-8xE{-1a24gBA+1*m$SSU$oxI*GYlEa-li$qKBT_; z{@y*5xoL5#Ha1?8Q0${j9HO9H9OWmo;-D__wm7Kf=lPfQ*A)462?i z7n2tHWS0P57w?y4T};)hwk{X%R5{&>d|l-80>76J#y{t?W${o$@2Y9hOzQF+hwHRI z2IP)_zq@Keh)(z0MEml53b3Z#$ENzQDC(z#k~j6Pg^OHqh&i0X@9@I%y&30AU z?88!j1EL)+kZRq_>DYRA)u-Y? zfX|&UfoBPsuq(N6@3aIhRFhfWG)ZN6S)3Miv6vK%Dn7QwVv0t1{6pRpuH_-=27LaX z0%QtB4rDYx0T>ia5bdwV3033vK&NnoU4?3tQd2j7Mzbi@EYHCtZ5&Tew)p>m$E*-P zqVB&en&qqoIfQ;qo44gemEVpxUV}jsWWgkF|>$bvkN&-=CFvgXnC4H-rB^2LoaVU)7cr zI1&CCh?55H;+A9@plHo4TFw}%Jf0A6`wjoS0a_uWdPrvH9GBBg&2J>R&*^{F707o} zOo`La@&&{nwR%^y%AikjsFc>1jpIo*Th15ik=i&e+hTq|KU%w;ji}RcIUQ}@ClRw9 z=VoAVBHyw+gxE+Vy6hy$=AONDL{KR^nh@)Wp6fq`U=4hBmYt%wt5YE7xOqQ2e80?R zO=by#6vlZpnUFF>B67D{Be%JdMA)B->^!JR@xw@`+P zdMfh`lp)GPM?TYQyn*^&j3?Fk#lOq;EIY5kzO|DxZL)UM8=Uj4UdffT58tr~Lyy=o z6TY+5_CoOKPE1D#cofQ`9JC@(%YCe&nTAMqc!fr9aO@OR* z{!eGXz=ZXOcTdQ$W29X{)ImHKVV?c|FlNEFdg8kN$gMjmzKf0!)>Sa~6c0nCL3}${S1niT#n)}e0wt$g&FxK8)FQKtc zpH#S zG(_sU7B-@vHSf1UA(t^@OvhRCTBvca+hJN4B~H}WLWz6b4oW($M+yb0{aR=Ry>Ek+ z7_1#Z?gmNKZ$%BUs8zr)#|66-6d&N*qP2M%1@=^u=v7zr?4HV+5{C8pYK7vYkoFWh zl96;RWHJ?rB{N=EZH*wEfo#S8-?K}&Ol|+69(z_cmR}0zJy_O|{)6Gfln3loOD3uv z2ZuduuKQ`p3h)0R_>}5>111>F>r?0dTy0;OH_?^`Z0a0*+RVwH_Or~L>>}!vN76Rm|F_qto;I6GPkIqr4a$o;|D z)o>PLqNEd!)hSXmJz9|zr7($PK^D{+*|qYZpz~ctQeQHmL9zC*OvvZjrwo=>?<`uQ zpkpJ*F#dl=M_7eSN4q`?No)OdV`w%M4Cb_$Grn|P1Nme-__Yme>#1+}2zOb3gB%Ix zl^OUAL-gUf5>D*mfSzm{0oSjEV4u|)cW4E$-x|{78w8p*Q6QnVYZghc??5)OAYz~H zA3iA2*@^ASlJ0SvrUXY6{Ov8RAna`R{(y zDxCN;OTPGUmadd`vL4#HGIAW*!1Cg}syzITJSQ{?!s@X)=T1Pu}vY`OY!5?Ax z+y}6^yoB3SPs(Bj=bP*qtuWbuqM&7uOeuC)q5AmRTkq7x~b^@1mfg+b3S^)FA zHyN?I071y2fun_{ZJeJpaI7pdieRy@&0L(&WrAyV7Fqk+2)Klu^X?mr!I|^gW7ylE zCegZP|HzfjwMWwFdvLmQ?Qz01+uTYVHd2^Mk=_S(l;yN5?d22>%+W1SD|3pjf9~)l zQ1@NP516a&bzBF<^0DKN7E3##6mx&a{kIEYxVytPy`xaRX~#G|yr1hZ3U~xdj@{OE zJ*Zd#v+FCm?t4c)^+K7hN&V*&`<;8wZcx9SN_(9J2WCJJf0&ejOjs-Q2Wzsw+Mr^B zgkfYg$^HcD%4mn`8j8uX^7#WgtbP8V zqg=4BHP6qptf0m&3#^hBu=QnbYGz?%WTAQkE=H$;`_Ee@uv3MN6x|p~ZTcU_ZS_;x zc+r8qs!6f1G(OHXp2Brhxa{p)M=%-G0%F>YROIzh5or4~38PiSzW8`PD<@?;yZCob zH%N~#c5UDe>D#VxhlfSn8)?mK8l6P>fOaeuR5wzR>7tC;IvXaW#_NINS{2 zfw^ZvN*7+TaPW9xu6yCaQ*sB|lNC879e1x{!xfFVWVu-_s)L44A{Wk^2KD1My~Is? zx912?>39EJ1z(V}zHRuUS?uGpit0v^Zc222S{fD~s#rDuZE-z`K)^N2RlG16X-u;} z05;~HG~bt(D}QV_SYxX%`f&i^a`2IW1}DB)Ogxu@*qV9Xyj?MgxuUHXC=kHi2RI0^ z>YbwKZE{*PyfBe?5y5#AfZ+XktU0;oS`Z(UuOOu&A#rp9m;l68uk0v^VPX}+6pb* zh^`SOk-(oWPOFCrVW0wIFbm3(u3`Q++|RD-a$3N!=5>*X^s2mVbNiGBiZiO81~+My z(l6%V_XH;3AIfIz545G;(0ASaE$J;<5qLPrm9+|Zu&$iY(WGZZn=5#Pf&P`HFApXg z*@qhB3TIF(jVx1JUapK$QB5jwx7~&#IK!5rhk}afb9`$|$HG<%(8xdz@?iWJ-`jij zeulSf#|^z$w#84FJznAqW=|<;gD|FE76WO3xxJ*QRv&&DbO%h~EBUilYuxbFOd>r8IZAKBakxK7kgLHt?T0rA=l!l+FfxT`FsT z33Yk>NaQffO2GK^(6z0r(U`cQK(Awk+}H@R2{k;j*FtF}G-vS47uXDHf7ksH$tCH5x>zz9S_@n^QXP zDKaixo6a27-kt(QgjX{uC4LS|!(22qL>Cf=*+6Pwmrn3&rlm%ZFursoUuX5_0dM)U z$iP4tS~y6iegnKQqI?QJzN_JJXDbK*uhT!Y9uHIH2Y6wbqVx%~>=G>o>~ZPN_zbqc z8KX0F5F$c=a<~X)8OfE+=K;kWb;(Qy`$bx)nv8Q(UC2j33cu0^OYI9)EEdYq6;5?$}v`pJbM}!U8ff+1ooIa8W-g+-fvG(K@911fcrY6h&=pV%*k}>(T7MxHc-C< zck3@R09a?8F}!G@!Kk6M@>5{soRHD<5?IG!<2&x68r_L*&g*iX*B9V;(0gB&)2qM_ zCpJ;wo$K=qPI8!f@0Q)~hu;eU9>~Tcm)iC3>Eji+UsZnOQx4j(7N@5L#{PwF1+PgS z&HQvwWCHOE!p;zL;_Qe5a29}UpOCQsEX%Nz}*QUq|+sMNH|(_ z)|PMM(!=};o$ls+yL9w8cRZo?k8VogB<^@-F_T;~TmSy)HF*WSeM$p`bGXmUBC1BrAKk|8>`ztyIA-2sH?waP`%zfQfX3{vq(q&k6SkQ%+Q4qz5E;tp zladfMpN=q;4?*!QxnJz(hvQRtT^aV7GT&B6zO7?6DDO=XQYY15HVpMB)(&iVVl&X3 z=iR4YxM7kk%Ij>@YAj=-d$2-NQFAz$EDqH3$EI2w$@gu)j%4MKmSBz#f6+73GVA6d zAB55RnEl~mIe)sC79S66dL9|EljnygJ;#s^E{lc`mOhViIyfK!zuT)ltCsD{>cdld z{2H?_-@`F**&q!kExJMt$ge#__kP_5GdK!xdI$K6YDh}pcZJQ0IouzhUq+)-%N2R) zC3KW~!I0r0qMc7@?}LxR6uUM+hYYAY@f~SnYw6mI5E(n>e+M`2o$GcGCXM!f=aOp= znWvBGB7~NSD2=W2zO;orKmj(uT>+;19VuiTaJNN}DGtECE8fh+iZ>gI*+xRrCzfsO zA{JQ7I2aiU{m8qNbQytxnjU^Uhu_}Q!0RI*!d!D8-r(<2II8yPT8p(0TDl;TgQ$xG zx$ZER6cH*6_jFkS%Nlrey)H?jwZ!b=o&>Ve0P4sj0g8aFHql4G$gVOB@OoMm z*v*);vZr+5$)3z~+0(gSub(A_kp74Omun3ee za*n4^rL5`;6}1`U!O>yG^OIko#p>84i8A<*S5RvN*ZIFiy{F*hzxh9>D((}MNJ#07$7O2vn8x~K7Ga4`hy*03QMTnu3pEk1zNMvZ|16`g&B=^JHl*D%2EB*qJr zCO)9nFvCt?&%|(~2kXSGMmJIESbVaWNRf&fs?e4W{5mPkS5kAd7%2~9Pp5-#`4RQ& zQ>qX9CyRu&Mp*3b&{`wMsso~TQ>kPy?-5XTFhjx<(IuK<{%E>S~+O@=RxRHAykv0BB&qc8fIh53f z5yYnEH?ax*;;NPlLs9KCdG~^gn)bVp61!CFW+h?I1?%x<=C*9MgBza8ChOabCS3c{ z9`b9i{~A~r-S6q}Uf#S*(b)Pxh!*JbEAB#iQNFwb%gv~(ZoPuKoy)KcE&yCFRz7h# zmV0ocgJ&LytFjEwjSk+hg)h!BY``yHtHMjPV~p(~sT7i^pfKup)Jbs%2*-k~$%k}k z28s)w$9GPQdeGv=F0jkQLWnQKEInC94U+t`Kd5Wy z49TDEfGosjK>pbu&^2@h4-1B20{O`9a?CB;c@^;vkABr zdJ+h}!65jaMj-@E;1B>ZIWcB46O(4y$*e5vk6l8#d-Ep7gGI|APkRvv>-2x%1rHrv z8MVsv=O31^S5s%BsQPBz-vvDW0H-qwJk9{Wg#-MgiZ^TZqLU#h5FzyVYlg%#2Z=FR zqye1&pBkOT6@dBwDPR`m4J(=n6nsg8!TnFEIReH*-{4!SGw~ie(7$nt!_hN$of!_M zSdMdUE#(5@Ejlf9mN#by)s%jB-;0x9V8aehq%>zx_0h*TVEa$M}7*>9ZbYZe`2lpic&ZK8>_ay>u7{kE! z=Lo3JI_?uRL^=mt=H6jTq&rCjcd_!3uGDk9KztOBk-hG|pVf0_3~iw{C2$a@wJ}#exh@=P+SjykR_%H$)rUm9H z9fVC({h43p)h4TF@|qg%`gkui5tvpyGqPnz3{a3B3Yfen^>b=qX2Bq6X2l?At7k*- z2=r@6r%l~JE2Jc2r<%2=)$qJ4XQ9nIM(vmc*f#F z6-3`q>V@$oCk9MLH(7=0B8gQpQ-8ssco)M&^8oxZ|3FeZ}9zT+Y8&)7Xyd$OS ztCs%y*fJSf2Z29Asn0RN9By_op%_kZ0{V!b^s;qVXK>vt<{Ms+6V)2oi&-M3Gv9PC z29Uk1w7Z$;3*Hptx{ZYQ*(E09CbwZ1)^X%mp>1|p#7?$E>N0XTt&?sD`WaW@5v8vsK1G5hEuSnf|sanyiT;fwj(YrA^XYP`IOI3@iZbVp;2iela{Z zT4<*GpjxP50c#YG>+!~CM5sU9(+9PP1xgjs8XhD7wxf{F7}a5Dp-Ayc$}@2!wquoV z`#^>^)3*3y3pPUzYUlznp)YSjpztJy!=~iX4C+VareH`RVu_IjL~;TFlR%wCGck+s z9R7(1wj=JCNMaHe_rxTo9UV=25;sVkVn!JfQ&21VC4T~A=xmghSb?uEiCGxTIkD7y z3~H=qV&Xu@5;Mm + text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get albumId => + text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + @override + Set get primaryKey => {assetId, albumId}; +} diff --git a/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album_asset.entity.drift.dart new file mode 100644 index 0000000000000000000000000000000000000000..ab50607c96814f1c84faed6c178c4a5a4e96597b GIT binary patch literal 22086 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 + text().references(RemoteAlbumEntity, #id, onDelete: KeyAction.cascade)(); + + TextColumn get userId => + text().references(UserEntity, #id, onDelete: KeyAction.cascade)(); + + IntColumn get role => intEnum()(); + + @override + Set get primaryKey => {albumId, userId}; +} diff --git a/mobile/lib/infrastructure/entities/remote_album_user.entity.drift.dart b/mobile/lib/infrastructure/entities/remote_album_user.entity.drift.dart new file mode 100644 index 0000000000000000000000000000000000000000..7ec1151a8a703faf80df884c13be55c4d175088c GIT binary patch literal 24035 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%0Ae1^w~zP8{0q>0XL_NT7yx5a@uB0{1vt;NOMXT z189HG0l^(a>6?Am<_i0K(`8xZb0smD(t>}jN3iGV2Cj}aEOX8uieyK|pU@8f@Q|!V zy_mzO6R2Yyco-bm!Ap2>>eX%Yp`lSneeex~R=sm=uU@XpeC!w* twXkDc(o=uXhvIFW)+%ypD^>dCCbup&cm!NzRkR(e7>)gFfTO#${{uVQ1WW(` literal 0 HcmV?d00001 diff --git a/mobile/lib/infrastructure/entities/remote_asset.entity.dart b/mobile/lib/infrastructure/entities/remote_asset.entity.dart index 3c7589949..bfe08346d 100644 --- a/mobile/lib/infrastructure/entities/remote_asset.entity.dart +++ b/mobile/lib/infrastructure/entities/remote_asset.entity.dart @@ -1,5 +1,6 @@ import 'package:drift/drift.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/utils/asset.mixin.dart'; import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart'; @@ -34,3 +35,21 @@ class RemoteAssetEntity extends Table @override Set get primaryKey => {id}; } + +extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData { + Asset toDto() => Asset( + id: id, + name: name, + checksum: checksum, + type: type, + createdAt: createdAt, + updatedAt: updatedAt, + durationInSeconds: durationInSeconds, + isFavorite: isFavorite, + height: height, + width: width, + thumbHash: thumbHash, + visibility: visibility, + localId: null, + ); +} diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart index 15b19f5c8..dbe491b03 100644 --- a/mobile/lib/infrastructure/repositories/db.repository.dart +++ b/mobile/lib/infrastructure/repositories/db.repository.dart @@ -8,6 +8,9 @@ import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; @@ -40,6 +43,9 @@ class IsarDatabaseRepository implements IDatabaseRepository { LocalAlbumAssetEntity, RemoteAssetEntity, RemoteExifEntity, + RemoteAlbumEntity, + RemoteAlbumAssetEntity, + RemoteAlbumUserEntity, ], include: { 'package:immich_mobile/infrastructure/entities/merged_asset.drift', diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart index d088e5420a1de301233aaa119f8be7c1eabb2396..69fd84b79a50377eb64bfacce0587fc8ff74895d 100644 GIT binary patch delta 810 zcmZ3j|IKYf5A(!#94t9WrMZ*uG099`%Pc%GUkxZU`3jR7qv6CDOCXyQBpqK`oLU4@ z!(?bUG1g*o53?eZq0!`Z%qEP+n;$bzXP*3lmB-FfPbDZdH@_s+5v0d8uOzdiG9)o6 zCsm;cU0A`EO92cr;hHA*v#Dtt8lafrSX`W1g2SN6A6P{B(bP}QXI1kzG(<5Y6zFsu zMj)gR?!nMK*^o_~ITPsH8g?B<Yk}J7P*M@UT-*h)+q<0TFsgdXN$X)O=uMBfA9@t{{hNfgFw;zji>k7(z`))iQaG Pu&NZ29SB`OD{8p_<^&UD delta 80 zcmV-W0I&b{MyoHdj02Or1R;~O0~3?+1R?=2vlIoF0|IF|llciO0Wgy$3N98oE=^@} mXJuqTb8}^ML~?0nbUr9)IkRjECjztW41Wl-fE*SD0(}Z!?ix7& diff --git a/mobile/lib/infrastructure/repositories/local_album.repository.dart b/mobile/lib/infrastructure/repositories/local_album.repository.dart index e5f8c7b52..33d61848d 100644 --- a/mobile/lib/infrastructure/repositories/local_album.repository.dart +++ b/mobile/lib/infrastructure/repositories/local_album.repository.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/local_album.model.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart'; diff --git a/mobile/lib/infrastructure/repositories/remote_album.repository.dart b/mobile/lib/infrastructure/repositories/remote_album.repository.dart new file mode 100644 index 000000000..dd237c95b --- /dev/null +++ b/mobile/lib/infrastructure/repositories/remote_album.repository.dart @@ -0,0 +1,45 @@ +import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; + +enum SortRemoteAlbumsBy { id } + +class DriftRemoteAlbumRepository extends DriftDatabaseRepository { + final Drift _db; + const DriftRemoteAlbumRepository(this._db) : super(_db); + + Future> getAll({Set sortBy = const {}}) { + final query = _db.remoteAlbumEntity.select(); + + if (sortBy.isNotEmpty) { + final orderings = >[]; + for (final sort in sortBy) { + orderings.add( + switch (sort) { + SortRemoteAlbumsBy.id => (row) => OrderingTerm.asc(row.id), + }, + ); + } + query.orderBy(orderings); + } + + return query.map((row) => row.toDto()).get(); + } +} + +extension on RemoteAlbumEntityData { + Album toDto() { + return Album( + id: id, + name: name, + ownerId: ownerId, + createdAt: createdAt, + updatedAt: updatedAt, + description: description, + thumbnailAssetId: thumbnailAssetId, + isActivityEnabled: isActivityEnabled, + order: order, + ); + } +} diff --git a/mobile/lib/infrastructure/repositories/sync_api.repository.dart b/mobile/lib/infrastructure/repositories/sync_api.repository.dart index f14773fc4..ccc79fa81 100644 --- a/mobile/lib/infrastructure/repositories/sync_api.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_api.repository.dart @@ -42,11 +42,16 @@ class SyncApiRepository { SyncStreamDto( types: [ SyncRequestType.usersV1, - SyncRequestType.partnersV1, SyncRequestType.assetsV1, - SyncRequestType.partnerAssetsV1, SyncRequestType.assetExifsV1, + SyncRequestType.partnersV1, + SyncRequestType.partnerAssetsV1, SyncRequestType.partnerAssetExifsV1, + SyncRequestType.albumsV1, + SyncRequestType.albumUsersV1, + SyncRequestType.albumAssetsV1, + SyncRequestType.albumAssetExifsV1, + SyncRequestType.albumToAssetsV1, ], ).toJson(), ); @@ -135,6 +140,25 @@ const _kResponseMap = { SyncEntityType.assetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.assetExifV1: SyncAssetExifV1.fromJson, SyncEntityType.partnerAssetV1: SyncAssetV1.fromJson, + SyncEntityType.partnerAssetBackfillV1: SyncAssetV1.fromJson, SyncEntityType.partnerAssetDeleteV1: SyncAssetDeleteV1.fromJson, SyncEntityType.partnerAssetExifV1: SyncAssetExifV1.fromJson, + SyncEntityType.partnerAssetExifBackfillV1: SyncAssetExifV1.fromJson, + SyncEntityType.albumV1: SyncAlbumV1.fromJson, + SyncEntityType.albumDeleteV1: SyncAlbumDeleteV1.fromJson, + SyncEntityType.albumUserV1: SyncAlbumUserV1.fromJson, + SyncEntityType.albumUserBackfillV1: SyncAlbumUserV1.fromJson, + SyncEntityType.albumUserDeleteV1: SyncAlbumUserDeleteV1.fromJson, + SyncEntityType.albumAssetV1: SyncAssetV1.fromJson, + SyncEntityType.albumAssetBackfillV1: SyncAssetV1.fromJson, + SyncEntityType.albumAssetExifV1: SyncAssetExifV1.fromJson, + SyncEntityType.albumAssetExifBackfillV1: SyncAssetExifV1.fromJson, + SyncEntityType.albumToAssetV1: SyncAlbumToAssetV1.fromJson, + SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson, + SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson, + SyncEntityType.syncAckV1: _SyncAckV1.fromJson, }; + +class _SyncAckV1 { + static _SyncAckV1? fromJson(dynamic _) => _SyncAckV1(); +} diff --git a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart index 56f1631ee..dfe65b698 100644 --- a/mobile/lib/infrastructure/repositories/sync_stream.repository.dart +++ b/mobile/lib/infrastructure/repositories/sync_stream.repository.dart @@ -1,13 +1,17 @@ import 'package:drift/drift.dart'; +import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:logging/logging.dart'; -import 'package:openapi/api.dart' as api show AssetVisibility; -import 'package:openapi/api.dart' hide AssetVisibility; +import 'package:openapi/api.dart' as api show AssetVisibility, AlbumUserRole; +import 'package:openapi/api.dart' hide AssetVisibility, AlbumUserRole; class SyncStreamRepository extends DriftDatabaseRepository { final Logger _logger = Logger('DriftSyncStreamRepository'); @@ -17,16 +21,10 @@ class SyncStreamRepository extends DriftDatabaseRepository { Future deleteUsersV1(Iterable data) async { try { - await _db.batch((batch) { - for (final user in data) { - batch.delete( - _db.userEntity, - UserEntityCompanion(id: Value(user.userId)), - ); - } - }); + await _db.userEntity + .deleteWhere((row) => row.id.isIn(data.map((e) => e.userId))); } catch (error, stack) { - _logger.severe('Error while processing SyncUserDeleteV1', error, stack); + _logger.severe('Error: SyncUserDeleteV1', error, stack); rethrow; } } @@ -48,7 +46,7 @@ class SyncStreamRepository extends DriftDatabaseRepository { } }); } catch (error, stack) { - _logger.severe('Error while processing SyncUserV1', error, stack); + _logger.severe('Error: SyncUserV1', error, stack); rethrow; } } @@ -67,7 +65,7 @@ class SyncStreamRepository extends DriftDatabaseRepository { } }); } catch (e, s) { - _logger.severe('Error while processing SyncPartnerDeleteV1', e, s); + _logger.severe('Error: SyncPartnerDeleteV1', e, s); rethrow; } } @@ -90,67 +88,30 @@ class SyncStreamRepository extends DriftDatabaseRepository { } }); } catch (e, s) { - _logger.severe('Error while processing SyncPartnerV1', e, s); + _logger.severe('Error: SyncPartnerV1', e, s); rethrow; } } - Future deleteAssetsV1(Iterable data) async { + Future deleteAssetsV1( + Iterable data, { + String debugLabel = 'user', + }) async { try { - await _deleteAssetsV1(data); + await _db.remoteAssetEntity + .deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId))); } catch (e, s) { - _logger.severe('Error while processing deleteAssetsV1', e, s); + _logger.severe('Error: deleteAssetsV1 - $debugLabel', e, s); rethrow; } } - Future updateAssetsV1(Iterable data) async { + Future updateAssetsV1( + Iterable data, { + String debugLabel = 'user', + }) async { try { - await _updateAssetsV1(data); - } catch (e, s) { - _logger.severe('Error while processing updateAssetsV1', e, s); - rethrow; - } - } - - Future deletePartnerAssetsV1(Iterable data) async { - try { - await _deleteAssetsV1(data); - } catch (e, s) { - _logger.severe('Error while processing deletePartnerAssetsV1', e, s); - rethrow; - } - } - - Future updatePartnerAssetsV1(Iterable data) async { - try { - await _updateAssetsV1(data); - } catch (e, s) { - _logger.severe('Error while processing updatePartnerAssetsV1', e, s); - rethrow; - } - } - - Future updateAssetsExifV1(Iterable data) async { - try { - await _updateAssetExifV1(data); - } catch (e, s) { - _logger.severe('Error while processing updateAssetsExifV1', e, s); - rethrow; - } - } - - Future updatePartnerAssetsExifV1(Iterable data) async { - try { - await _updateAssetExifV1(data); - } catch (e, s) { - _logger.severe('Error while processing updatePartnerAssetsExifV1', e, s); - rethrow; - } - } - - Future _updateAssetsV1(Iterable data) => - _db.batch((batch) { + await _db.batch((batch) { for (final asset in data) { final companion = RemoteAssetEntityCompanion( name: Value(asset.originalFileName), @@ -175,19 +136,18 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); + } catch (e, s) { + _logger.severe('Error: updateAssetsV1 - $debugLabel', e, s); + rethrow; + } + } - Future _deleteAssetsV1(Iterable assets) => - _db.batch((batch) { - for (final asset in assets) { - batch.delete( - _db.remoteAssetEntity, - RemoteAssetEntityCompanion(id: Value(asset.assetId)), - ); - } - }); - - Future _updateAssetExifV1(Iterable data) => - _db.batch((batch) { + Future updateAssetsExifV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.batch((batch) { for (final exif in data) { final companion = RemoteExifEntityCompanion( city: Value(exif.city), @@ -219,6 +179,141 @@ class SyncStreamRepository extends DriftDatabaseRepository { ); } }); + } catch (e, s) { + _logger.severe('Error: updateAssetsExifV1 - $debugLabel', e, s); + rethrow; + } + } + + Future deleteAlbumsV1(Iterable data) async { + try { + await _db.remoteAlbumEntity + .deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId))); + } catch (e, s) { + _logger.severe('Error: deleteAlbumsV1', e, s); + rethrow; + } + } + + Future updateAlbumsV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final album in data) { + final companion = RemoteAlbumEntityCompanion( + name: Value(album.name), + description: Value(album.description), + isActivityEnabled: Value(album.isActivityEnabled), + order: Value(album.order.toAlbumAssetOrder()), + thumbnailAssetId: Value(album.thumbnailAssetId), + ownerId: Value(album.ownerId), + createdAt: Value(album.createdAt), + updatedAt: Value(album.updatedAt), + ); + + batch.insert( + _db.remoteAlbumEntity, + companion.copyWith(id: Value(album.id)), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (e, s) { + _logger.severe('Error: updateAlbumsV1', e, s); + rethrow; + } + } + + Future deleteAlbumUsersV1(Iterable data) async { + try { + await _db.batch((batch) { + for (final album in data) { + batch.delete( + _db.remoteAlbumUserEntity, + RemoteAlbumUserEntityCompanion( + albumId: Value(album.albumId), + userId: Value(album.userId), + ), + ); + } + }); + } catch (e, s) { + _logger.severe('Error: deleteAlbumUsersV1', e, s); + rethrow; + } + } + + Future updateAlbumUsersV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.batch((batch) { + for (final album in data) { + final companion = RemoteAlbumUserEntityCompanion( + role: Value(album.role.toAlbumUserRole()), + ); + + batch.insert( + _db.remoteAlbumUserEntity, + companion.copyWith( + albumId: Value(album.albumId), + userId: Value(album.userId), + ), + onConflict: DoUpdate((_) => companion), + ); + } + }); + } catch (e, s) { + _logger.severe('Error: updateAlbumUsersV1 - $debugLabel', e, s); + rethrow; + } + } + + Future deleteAlbumToAssetsV1( + Iterable data, + ) async { + try { + await _db.batch((batch) { + for (final album in data) { + batch.delete( + _db.remoteAlbumAssetEntity, + RemoteAlbumAssetEntityCompanion( + albumId: Value(album.albumId), + assetId: Value(album.assetId), + ), + ); + } + }); + } catch (e, s) { + _logger.severe('Error: deleteAlbumToAssetsV1', e, s); + rethrow; + } + } + + Future updateAlbumToAssetsV1( + Iterable data, { + String debugLabel = 'user', + }) async { + try { + await _db.batch((batch) { + for (final album in data) { + final companion = RemoteAlbumAssetEntityCompanion( + albumId: Value(album.albumId), + assetId: Value(album.assetId), + ); + + batch.insert( + _db.remoteAlbumAssetEntity, + companion, + onConflict: DoNothing(), + ); + } + }); + } catch (e, s) { + _logger.severe('Error: updateAlbumToAssetsV1 - $debugLabel', e, s); + rethrow; + } + } } extension on AssetTypeEnum { @@ -231,6 +326,22 @@ extension on AssetTypeEnum { }; } +extension on AssetOrder { + AlbumAssetOrder toAlbumAssetOrder() => switch (this) { + AssetOrder.asc => AlbumAssetOrder.asc, + AssetOrder.desc => AlbumAssetOrder.desc, + _ => throw Exception('Unknown AssetOrder value: $this'), + }; +} + +extension on api.AlbumUserRole { + AlbumUserRole toAlbumUserRole() => switch (this) { + api.AlbumUserRole.editor => AlbumUserRole.editor, + api.AlbumUserRole.viewer => AlbumUserRole.viewer, + _ => throw Exception('Unknown AlbumUserRole value: $this'), + }; +} + extension on api.AssetVisibility { AssetVisibility toAssetVisibility() => switch (this) { api.AssetVisibility.timeline => AssetVisibility.timeline, diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index add327cf0..7f5591c04 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/constants/constants.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/timeline.model.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; +import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:stream_transform/stream_transform.dart'; @@ -139,6 +140,62 @@ class DriftTimelineRepository extends DriftDatabaseRepository { .map((row) => row.readTable(_db.localAssetEntity).toDto()) .get(); } + + Stream> watchRemoteBucket( + String albumId, { + GroupAssetsBy groupBy = GroupAssetsBy.day, + }) { + if (groupBy == GroupAssetsBy.none) { + return _db.remoteAlbumAssetEntity + .count(where: (row) => row.albumId.equals(albumId)) + .map(_generateBuckets) + .watchSingle(); + } + + final assetCountExp = _db.remoteAssetEntity.id.count(); + final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy); + + final query = _db.remoteAssetEntity.selectOnly() + ..addColumns([assetCountExp, dateExp]) + ..join([ + innerJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.assetId + .equalsExp(_db.remoteAssetEntity.id), + ), + ]) + ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) + ..groupBy([dateExp]) + ..orderBy([OrderingTerm.desc(dateExp)]); + + return query.map((row) { + final timeline = row.read(dateExp)!.dateFmt(groupBy); + final assetCount = row.read(assetCountExp)!; + return TimeBucket(date: timeline, assetCount: assetCount); + }).watch(); + } + + Future> getRemoteBucketAssets( + String albumId, { + required int offset, + required int count, + }) { + final query = _db.remoteAssetEntity.select().join( + [ + innerJoin( + _db.remoteAlbumAssetEntity, + _db.remoteAlbumAssetEntity.assetId + .equalsExp(_db.remoteAssetEntity.id), + ), + ], + ) + ..where(_db.remoteAlbumAssetEntity.albumId.equals(albumId)) + ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) + ..limit(count, offset: offset); + return query + .map((row) => row.readTable(_db.remoteAssetEntity).toDto()) + .get(); + } } extension on Expression { diff --git a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart index c53b7fe0d..f10c042e1 100644 --- a/mobile/lib/presentation/pages/dev/feat_in_development.page.dart +++ b/mobile/lib/presentation/pages/dev/feat_in_development.page.dart @@ -63,6 +63,9 @@ final _features = [ final db = ref.read(driftProvider); await db.remoteAssetEntity.deleteAll(); await db.remoteExifEntity.deleteAll(); + await db.remoteAlbumEntity.deleteAll(); + await db.remoteAlbumUserEntity.deleteAll(); + await db.remoteAlbumAssetEntity.deleteAll(); }, ), _Feature( diff --git a/mobile/lib/presentation/pages/dev/media_stat.page.dart b/mobile/lib/presentation/pages/dev/media_stat.page.dart index cc1fd0ae0..10d09f8de 100644 --- a/mobile/lib/presentation/pages/dev/media_stat.page.dart +++ b/mobile/lib/presentation/pages/dev/media_stat.page.dart @@ -40,7 +40,10 @@ class _Summary extends StatelessWidget { } else if (snapshot.hasError) { subtitle = const Icon(Icons.error_rounded); } else { - subtitle = Text('${snapshot.data ?? 0}'); + subtitle = Text( + '${snapshot.data ?? 0}', + style: ctx.textTheme.bodyLarge, + ); } return ListTile( leading: leading, @@ -147,6 +150,10 @@ final _remoteStats = [ name: 'Exif Entities', load: (db) => db.managers.remoteExifEntity.count(), ), + _Stat( + name: 'Remote Albums', + load: (db) => db.managers.remoteAlbumEntity.count(), + ), ]; @RoutePage() @@ -160,6 +167,7 @@ class RemoteMediaSummaryPage extends StatelessWidget { body: Consumer( builder: (ctx, ref, __) { final db = ref.watch(driftProvider); + final albumsFuture = ref.watch(remoteAlbumRepository).getAll(); return CustomScrollView( slivers: [ @@ -171,6 +179,49 @@ class RemoteMediaSummaryPage extends StatelessWidget { }, itemCount: _remoteStats.length, ), + SliverToBoxAdapter( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Divider(), + Padding( + padding: const EdgeInsets.only(left: 15), + child: Text( + "Album summary", + style: ctx.textTheme.titleMedium, + ), + ), + ], + ), + ), + FutureBuilder( + future: albumsFuture, + builder: (_, snap) { + final albums = snap.data ?? []; + if (albums.isEmpty) { + return const SliverToBoxAdapter(child: SizedBox.shrink()); + } + + albums.sortBy((a) => a.name); + return SliverList.builder( + itemBuilder: (_, index) { + final album = albums[index]; + final countFuture = db.managers.remoteAlbumAssetEntity + .filter((f) => f.albumId.id.equals(album.id)) + .count(); + return _Summary( + leading: const Icon(Icons.photo_album_rounded), + name: album.name, + countFuture: countFuture, + onTap: () => context.router.push( + RemoteTimelineRoute(albumId: album.id), + ), + ); + }, + itemCount: albums.length, + ); + }, + ), ], ); }, diff --git a/mobile/lib/presentation/pages/dev/remote_timeline.page.dart b/mobile/lib/presentation/pages/dev/remote_timeline.page.dart new file mode 100644 index 000000000..4965359ad --- /dev/null +++ b/mobile/lib/presentation/pages/dev/remote_timeline.page.dart @@ -0,0 +1,32 @@ +import 'dart:async'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/widgets.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart'; + +@RoutePage() +class RemoteTimelinePage extends StatelessWidget { + final String albumId; + + const RemoteTimelinePage({super.key, required this.albumId}); + + @override + Widget build(BuildContext context) { + return ProviderScope( + overrides: [ + timelineServiceProvider.overrideWith( + (ref) { + final timelineService = ref + .watch(timelineFactoryProvider) + .remoteAlbum(albumId: albumId); + ref.onDispose(() => unawaited(timelineService.dispose())); + return timelineService; + }, + ), + ], + child: const Timeline(), + ); + } +} diff --git a/mobile/lib/providers/infrastructure/album.provider.dart b/mobile/lib/providers/infrastructure/album.provider.dart index 6a4807319..b9dd21204 100644 --- a/mobile/lib/providers/infrastructure/album.provider.dart +++ b/mobile/lib/providers/infrastructure/album.provider.dart @@ -1,7 +1,12 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; +import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart'; import 'package:immich_mobile/providers/infrastructure/db.provider.dart'; final localAlbumRepository = Provider( (ref) => DriftLocalAlbumRepository(ref.watch(driftProvider)), ); + +final remoteAlbumRepository = Provider( + (ref) => DriftRemoteAlbumRepository(ref.watch(driftProvider)), +); diff --git a/mobile/lib/repositories/auth.repository.dart b/mobile/lib/repositories/auth.repository.dart index 140328072..3ad8e3458 100644 --- a/mobile/lib/repositories/auth.repository.dart +++ b/mobile/lib/repositories/auth.repository.dart @@ -34,6 +34,12 @@ class AuthRepository extends DatabaseRepository { db.users.clear(), _drift.remoteAssetEntity.deleteAll(), _drift.remoteExifEntity.deleteAll(), + _drift.userEntity.deleteAll(), + _drift.userMetadataEntity.deleteAll(), + _drift.partnerEntity.deleteAll(), + _drift.remoteAlbumEntity.deleteAll(), + _drift.remoteAlbumAssetEntity.deleteAll(), + _drift.remoteAlbumUserEntity.deleteAll(), ]); }); } diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 33631f85d..708171896 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -68,6 +68,7 @@ import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.da import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart'; import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart'; +import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/gallery_permission.provider.dart'; import 'package:immich_mobile/routing/auth_guard.dart'; @@ -365,6 +366,10 @@ class AppRouter extends RootStackRouter { page: MainTimelineRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: RemoteTimelineRoute.page, + guards: [_authGuard, _duplicateGuard], + ), // required to handle all deeplinks in deep_link.service.dart // auto_route_library#1722 RedirectRoute(path: '*', redirectTo: '/'), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 797b519dd..a5c2c5861 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -1425,6 +1425,43 @@ class RemoteMediaSummaryRoute extends PageRouteInfo { ); } +/// generated route for +/// [RemoteTimelinePage] +class RemoteTimelineRoute extends PageRouteInfo { + RemoteTimelineRoute({ + Key? key, + required String albumId, + List? children, + }) : super( + RemoteTimelineRoute.name, + args: RemoteTimelineRouteArgs(key: key, albumId: albumId), + initialChildren: children, + ); + + static const String name = 'RemoteTimelineRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return RemoteTimelinePage(key: args.key, albumId: args.albumId); + }, + ); +} + +class RemoteTimelineRouteArgs { + const RemoteTimelineRouteArgs({this.key, required this.albumId}); + + final Key? key; + + final String albumId; + + @override + String toString() { + return 'RemoteTimelineRouteArgs{key: $key, albumId: $albumId}'; + } +} + /// generated route for /// [SearchPage] class SearchRoute extends PageRouteInfo { diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 238a19e5d1143dea1085baf1b972a382448b076a..b09f36548aec2992b0effeb4ee463129319b8888 100644 GIT binary patch delta 50 zcmdnFoayIsrVSmCwlb5ds^r@DmHZomklP**t&!8@AEAvzqaxF_2H{WVqY2%J+h+-qQHmz!x z8u_JG2GvWk#=n&?_`j3JqH$x7t7p2jPLwWHIXYAeB@L_Fqs|IdN}CqCT%wp;i86il zOPZ~Ov4a7+bD$TXD_M$4CHU|4V2~BUTKKpvv-9GvUeENowbET+f=+&V3HJkl5f@^63*uJNoWghlzQCC%B8n$&ZoYlWm?N)i)GnMrGf*%FhG94l#+ zWw9s}CTa9f;pcGS9qun46e^U~Y5ybQ+6?=koCit zL%zihP}u~Wert7!K0G59suTr$#v>TR1L^zSYfb`KVs3`P zVP>;QI@l4teu(iP${hS4u%MiUucD}>g@w`UU~|q#*dz-S7*aRQu`IVSly!mZBr0x{ z<=*KaT^YH+cIHW7>J0g7Q(|_CgTbjR#Tl~FIU(;4di(Boj)f~85b!ZJ<*%!y5xKl@ zI^sZMc-DveGweg^Va&wKQhQ)7jVXWwEZJO=3F{u98u^a)1A&H2Z}<^=B*6gu8LUP1 ziK;wZ%(j&bwkHuWD26>jPEY(;VO8DFA>dltgz~>rlxELFoj`%t zSICJ29DKz0Jf)UKZ^T`JO%DrESQ+n{s`{E5Y3ssqhx9p_-|pcd(}WI%g@cHho+x{x z-Q~J++r~B`hJ!H*F7yT`Ig9S^8%!BYbl;a^LX79^Fp*F+s{EcSCu3%O*EBr`lvX10 z+%u}9=}S({Np=nZ2wF~4dQnrlGiZLX6>jQ)PInIVWiTOP9mS{#*_R2W7zTcg;@*Nx zn#n~KXyCv&zKaISjXhd@je~Sh%@Y{8w&ie6zmi>>lC<>I1-0wQV>|A3tf1a<#O7(l^Sj@QC`1 z_MJYEUwCoU4x<`hBUGaWkgOurjxdH;*5BLA8xlFnofw|h-{`!{sz+S@2)CX{o2O7P z%;e7Oc$8t7-y!&Y2qgqzTgnd6; mockSyncStreamRepo.updateAssetsV1(any())) .thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.updateAssetsV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); when(() => mockSyncStreamRepo.deleteAssetsV1(any())) .thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.deleteAssetsV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); when(() => mockSyncStreamRepo.updateAssetsExifV1(any())) .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.updatePartnerAssetsV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.deletePartnerAssetsV1(any())) - .thenAnswer(successHandler); - when(() => mockSyncStreamRepo.updatePartnerAssetsExifV1(any())) - .thenAnswer(successHandler); + when( + () => mockSyncStreamRepo.updateAssetsExifV1( + any(), + debugLabel: any(named: 'debugLabel'), + ), + ).thenAnswer(successHandler); sut = SyncStreamService( syncApiRepository: mockSyncApiRepo, diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart index 1432d3590..1e79f62fa 100644 --- a/mobile/test/fixtures/album.stub.dart +++ b/mobile/test/fixtures/album.stub.dart @@ -1,4 +1,4 @@ -import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; diff --git a/mobile/test/infrastructure/repositories/local_album_repository_test.dart b/mobile/test/infrastructure/repositories/local_album_repository_test.dart index f6c82c1be..bab25de52 100644 --- a/mobile/test/infrastructure/repositories/local_album_repository_test.dart +++ b/mobile/test/infrastructure/repositories/local_album_repository_test.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:immich_mobile/domain/models/local_album.model.dart'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; diff --git a/mobile/test/test_utils/medium_factory.dart b/mobile/test/test_utils/medium_factory.dart index affe9c9b3..8dafc564c 100644 --- a/mobile/test/test_utils/medium_factory.dart +++ b/mobile/test/test_utils/medium_factory.dart @@ -1,7 +1,7 @@ import 'dart:math'; +import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; -import 'package:immich_mobile/domain/models/local_album.model.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart'; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index b63b50338..129c12028 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -13450,6 +13450,21 @@ ], "type": "object" }, + "SyncAlbumToAssetDeleteV1": { + "properties": { + "albumId": { + "type": "string" + }, + "assetId": { + "type": "string" + } + }, + "required": [ + "albumId", + "assetId" + ], + "type": "object" + }, "SyncAlbumToAssetV1": { "properties": { "albumId": { diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index b552f52a3..7385edf40 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -198,6 +198,7 @@ const responseDtos = [ SyncAlbumUserV1, SyncAlbumUserDeleteV1, SyncAlbumToAssetV1, + SyncAlbumToAssetDeleteV1, SyncAckV1, ];