Compare commits

...

16 Commits

Author SHA1 Message Date
midzelis
66367a40dd feat: skeleton title is optional
feat: skeleton title optional
2025-09-28 20:40:06 +00:00
shenlong
bea116e1b9 fix: prefer remote images in new timeline (#22452)
fix: prefer remote images in new thumbnail

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-27 21:29:28 -05:00
shenlong
cdbe1d7f10 chore: show download button for remote only assets (#22453)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-27 21:28:07 -05:00
Brandon Wees
df469cc412 feat: show motion photo icon on mobile timeline tile (#22454)
* feat: show motion photo icon on timeline tile

* chore: switch to private widget for asset type icons

* chore: small cleanup on asset type icons widget
2025-09-27 21:27:34 -05:00
shenlong
8de7eed940 feat(mobile): add unstack button (#21869)
* fix: add unstack button

* feat: allow unstacking inside of asset viewer

* chore: update tests

* chore: rework unstacking in asset viewer

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: bwees <brandonwees@gmail.com>
2025-09-28 06:51:38 +05:30
shenlong
7d8cd05bc2 fix: remote album timeline filter (#22423)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-26 17:35:46 +00:00
Brandon Wees
30a378c580 fix: local assets should not be added to album (#22304) 2025-09-26 22:41:12 +05:30
renovate[bot]
8a3684c127 chore(deps): update ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0 docker digest to 41eacbe (#22305)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 14:26:55 +02:00
renovate[bot]
61e5c6349c chore(deps): update github-actions (#22311)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-09-26 14:26:47 +02:00
Mert
3bcb4b7af7 fix(mobile): scrubbing mode on scroll to date event (#22390) 2025-09-25 19:20:42 -05:00
Mert
5116b215a2 fix(mobile): load local thumbnails in album timeline (#22329)
* join local asset in album query

* missed one

* formatting
2025-09-26 00:38:19 +05:30
shenlong
c5fbbee8f6 chore: update android background worker notification text (#22347)
chore: update android bg notification text

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-26 00:22:17 +05:30
shenlong
d73aabc494 chore: log mobile upload failures (#22349)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-26 00:22:03 +05:30
shenlong
b62feb726b fix: delete temp file on iOS after upload (#22364)
fix: delete temp files on iOS after upload

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-26 00:21:25 +05:30
Brandon Wees
972e9cc039 fix: map attribution and other styling (#22303)
* chore: map widget and page styling

* fix: map bottom sheet styling

* fix: attribution location on android

it appears that on android, the attribution marker is positioned from the top of the display and on iOS its positioned from the safe area edge
2025-09-26 00:08:25 +05:30
shenlong
ee49136e97 chore: deprecate old timeline (#22328)
* chore: deprecate old timeline

* change trigger and duration

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-09-26 00:06:17 +05:30
39 changed files with 310 additions and 177 deletions

View File

@@ -36,7 +36,7 @@ jobs:
steps: steps:
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1 uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with: with:
filters: | filters: |
mobile: mobile:
@@ -73,7 +73,7 @@ jobs:
- name: Restore Gradle Cache - name: Restore Gradle Cache
id: cache-gradle-restore id: cache-gradle-restore
uses: actions/cache/restore@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches
@@ -130,7 +130,7 @@ jobs:
- name: Save Gradle Cache - name: Save Gradle Cache
id: cache-gradle-save id: cache-gradle-save
uses: actions/cache/save@0400d5f644dc74513175e3cd8d07132dd4860809 # v4 uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
with: with:
path: | path: |

View File

@@ -24,7 +24,7 @@ jobs:
steps: steps:
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1 uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with: with:
filters: | filters: |
server: server:

View File

@@ -22,7 +22,7 @@ jobs:
steps: steps:
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1 uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with: with:
filters: | filters: |
docs: docs:

View File

@@ -16,7 +16,7 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -58,7 +58,7 @@ jobs:
- name: Generate a token - name: Generate a token
id: generate_token id: generate_token
if: ${{ inputs.skip != true }} if: ${{ inputs.skip != true }}
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -49,7 +49,7 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
@@ -111,7 +111,7 @@ jobs:
steps: steps:
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1 uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2.1.4
with: with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}

View File

@@ -21,7 +21,7 @@ jobs:
steps: steps:
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1 uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with: with:
filters: | filters: |
mobile: mobile:

View File

@@ -18,7 +18,7 @@ jobs:
steps: steps:
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1 uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with: with:
filters: | filters: |
i18n: i18n:

View File

@@ -25,7 +25,7 @@ jobs:
steps: steps:
- name: Check what should run - name: Check what should run
id: check id: check
uses: immich-app/devtools/actions/pre-job@24820aa4ef67959b0dcf69a438cccf00d7c7042b # pre-job-action-v1.0.1 uses: immich-app/devtools/actions/pre-job@5f91b52dfbb92b8d96ca411ab59c896cd59714ca # pre-job-action-v1.1.0
with: with:
filters: | filters: |
i18n: i18n:

View File

@@ -140,7 +140,7 @@ services:
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:c44be5f2871c59362966d71eab4268170eb6f5653c0e6170184e72b38ffdf107 image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84
env_file: env_file:
- .env - .env
environment: environment:

View File

@@ -63,7 +63,7 @@ services:
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:c44be5f2871c59362966d71eab4268170eb6f5653c0e6170184e72b38ffdf107 image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84
env_file: env_file:
- .env - .env
environment: environment:

View File

@@ -56,7 +56,7 @@ services:
database: database:
container_name: immich_postgres container_name: immich_postgres
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:c44be5f2871c59362966d71eab4268170eb6f5653c0e6170184e72b38ffdf107 image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:41eacbe83eca995561fe43814fd4891e16e39632806253848efaf04d3c8a8b84
environment: environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}

View File

@@ -28,6 +28,7 @@
"add_to_album": "Add to album", "add_to_album": "Add to album",
"add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}",
"add_to_album_bottom_sheet_some_local_assets": "Some local assets could not be added to album",
"add_to_album_toggle": "Toggle selection for {album}", "add_to_album_toggle": "Toggle selection for {album}",
"add_to_albums": "Add to albums", "add_to_albums": "Add to albums",
"add_to_albums_count": "Add to albums ({count})", "add_to_albums_count": "Add to albums ({count})",

View File

@@ -116,7 +116,7 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
if (Platform.isAndroid) { if (Platform.isAndroid) {
await _backgroundHostApi.showNotification( await _backgroundHostApi.showNotification(
IntlKeys.uploading_media.t(), IntlKeys.uploading_media.t(),
IntlKeys.backup_background_service_in_progress_notification.t(), IntlKeys.backup_background_service_default_notification.t(),
); );
} }

View File

@@ -21,7 +21,7 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
} }
extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData { extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
LocalAsset toDto() => LocalAsset( LocalAsset toDto({String? remoteId}) => LocalAsset(
id: id, id: id,
name: name, name: name,
checksum: checksum, checksum: checksum,
@@ -32,7 +32,7 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
isFavorite: isFavorite, isFavorite: isFavorite,
height: height, height: height,
width: width, width: width,
remoteId: null, remoteId: remoteId,
orientation: orientation, orientation: orientation,
); );
} }

View File

@@ -49,7 +49,7 @@ class RemoteAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin
} }
extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData { extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
RemoteAsset toDto() => RemoteAsset( RemoteAsset toDto({String? localId}) => RemoteAsset(
id: id, id: id,
name: name, name: name,
ownerId: ownerId, ownerId: ownerId,
@@ -64,7 +64,7 @@ extension RemoteAssetEntityDataDomainEx on RemoteAssetEntityData {
thumbHash: thumbHash, thumbHash: thumbHash,
visibility: visibility, visibility: visibility,
livePhotoVideoId: livePhotoVideoId, livePhotoVideoId: livePhotoVideoId,
localId: null, localId: localId,
stackId: stackId, stackId: stackId,
); );
} }

View File

@@ -148,10 +148,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)]) ..orderBy([OrderingTerm.desc(_db.localAssetEntity.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);
return query.map((row) { return query
final asset = row.readTable(_db.localAssetEntity).toDto(); .map((row) => row.readTable(_db.localAssetEntity).toDto(remoteId: row.read(_db.remoteAssetEntity.id)))
return asset.copyWith(remoteId: row.read(_db.remoteAssetEntity.id)); .get();
}).get();
} }
TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => ( TimelineQuery remoteAlbum(String albumId, GroupAssetsBy groupBy) => (
@@ -165,17 +164,15 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
.count(where: (row) => row.albumId.equals(albumId)) .count(where: (row) => row.albumId.equals(albumId))
.map(_generateBuckets) .map(_generateBuckets)
.watch() .watch()
.map((results) => results.isNotEmpty ? results.first : <Bucket>[]) .map((results) => results.isNotEmpty ? results.first : const <Bucket>[])
.handleError((error) { .handleError((error) => const <Bucket>[]);
return [];
});
} }
return (_db.remoteAlbumEntity.select()..where((row) => row.id.equals(albumId))) return (_db.remoteAlbumEntity.select()..where((row) => row.id.equals(albumId)))
.watch() .watch()
.switchMap((albums) { .switchMap((albums) {
if (albums.isEmpty) { if (albums.isEmpty) {
return Stream.value(<Bucket>[]); return Stream.value(const <Bucket>[]);
} }
final album = albums.first; final album = albums.first;
@@ -207,10 +204,8 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return TimeBucket(date: timeline, assetCount: assetCount); return TimeBucket(date: timeline, assetCount: assetCount);
}).watch(); }).watch();
}) })
.handleError((error) { // If there's an error (e.g., album was deleted), return empty buckets
// If there's an error (e.g., album was deleted), return empty buckets .handleError((error) => const <Bucket>[]);
return <Bucket>[];
});
} }
Future<List<BaseAsset>> _getRemoteAlbumBucketAssets(String albumId, {required int offset, required int count}) async { Future<List<BaseAsset>> _getRemoteAlbumBucketAssets(String albumId, {required int offset, required int count}) async {
@@ -218,17 +213,22 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
// If album doesn't exist (was deleted), return empty list // If album doesn't exist (was deleted), return empty list
if (albumData == null) { if (albumData == null) {
return <BaseAsset>[]; return const <BaseAsset>[];
} }
final isAscending = albumData.order == AlbumAssetOrder.asc; final isAscending = albumData.order == AlbumAssetOrder.asc;
final query = _db.remoteAssetEntity.select().join([ final query = _db.remoteAssetEntity.select().addColumns([_db.localAssetEntity.id]).join([
innerJoin( innerJoin(
_db.remoteAlbumAssetEntity, _db.remoteAlbumAssetEntity,
_db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id), _db.remoteAlbumAssetEntity.assetId.equalsExp(_db.remoteAssetEntity.id),
useColumns: false, useColumns: false,
), ),
leftOuterJoin(
_db.localAssetEntity,
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
useColumns: false,
),
])..where(_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId)); ])..where(_db.remoteAssetEntity.deletedAt.isNull() & _db.remoteAlbumAssetEntity.albumId.equals(albumId));
if (isAscending) { if (isAscending) {
@@ -239,12 +239,14 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
query.limit(count, offset: offset); query.limit(count, offset: offset);
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); return query
.map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id)))
.get();
} }
TimelineQuery fromAssets(List<BaseAsset> assets) => ( TimelineQuery fromAssets(List<BaseAsset> assets) => (
bucketSource: () => Stream.value(_generateBuckets(assets.length)), bucketSource: () => Stream.value(_generateBuckets(assets.length)),
assetSource: (offset, count) => Future.value(assets.skip(offset).take(count).toList()), assetSource: (offset, count) => Future.value(assets.skip(offset).take(count).toList(growable: false)),
); );
TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => _remoteQueryBuilder( TimelineQuery remote(String ownerId, GroupAssetsBy groupBy) => _remoteQueryBuilder(
@@ -486,6 +488,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get(); return query.map((row) => row.readTable(_db.remoteAssetEntity).toDto()).get();
} }
@pragma('vm:prefer-inline')
TimelineQuery _remoteQueryBuilder({ TimelineQuery _remoteQueryBuilder({
required Expression<bool> Function($RemoteAssetEntityTable row) filter, required Expression<bool> Function($RemoteAssetEntityTable row) filter,
GroupAssetsBy groupBy = GroupAssetsBy.day, GroupAssetsBy groupBy = GroupAssetsBy.day,
@@ -523,6 +526,7 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
}).watch(); }).watch();
} }
@pragma('vm:prefer-inline')
Future<List<BaseAsset>> _getRemoteAssets({ Future<List<BaseAsset>> _getRemoteAssets({
required Expression<bool> Function($RemoteAssetEntityTable row) filter, required Expression<bool> Function($RemoteAssetEntityTable row) filter,
required int offset, required int offset,
@@ -543,11 +547,9 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)]) ..orderBy([OrderingTerm.desc(_db.remoteAssetEntity.createdAt)])
..limit(count, offset: offset); ..limit(count, offset: offset);
return query.map((row) { return query
final asset = row.readTable(_db.remoteAssetEntity).toDto(); .map((row) => row.readTable(_db.remoteAssetEntity).toDto(localId: row.read(_db.localAssetEntity.id)))
final localId = row.read(_db.localAssetEntity.id); .get();
return asset.copyWith(localId: localId);
}).get();
} else { } else {
final query = _db.remoteAssetEntity.select() final query = _db.remoteAssetEntity.select()
..where(filter) ..where(filter)
@@ -560,12 +562,12 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
} }
List<Bucket> _generateBuckets(int count) { List<Bucket> _generateBuckets(int count) {
final buckets = List.generate( final buckets = List.filled(
(count / kTimelineNoneSegmentSize).floor(), (count / kTimelineNoneSegmentSize).ceil(),
(_) => const Bucket(assetCount: kTimelineNoneSegmentSize), const Bucket(assetCount: kTimelineNoneSegmentSize),
); );
if (count % kTimelineNoneSegmentSize != 0) { if (count % kTimelineNoneSegmentSize != 0) {
buckets.add(Bucket(assetCount: count % kTimelineNoneSegmentSize)); buckets[buckets.length - 1] = Bucket(assetCount: count % kTimelineNoneSegmentSize);
} }
return buckets; return buckets;
} }
@@ -590,10 +592,6 @@ extension on String {
GroupAssetsBy.month => "y-M", GroupAssetsBy.month => "y-M",
GroupAssetsBy.none => throw ArgumentError("GroupAssetsBy.none is not supported for date formatting"), GroupAssetsBy.none => throw ArgumentError("GroupAssetsBy.none is not supported for date formatting"),
}; };
try { return DateFormat(format, 'en').parse(this);
return DateFormat(format, 'en').parse(this);
} catch (e) {
throw FormatException("Invalid date format: $this", e);
}
} }
} }

View File

@@ -25,9 +25,10 @@ class DriftMapPage extends StatelessWidget {
onPressed: () => context.pop(), onPressed: () => context.pop(),
icon: const Icon(Icons.arrow_back_ios_new_rounded), icon: const Icon(Icons.arrow_back_ios_new_rounded),
style: IconButton.styleFrom( style: IconButton.styleFrom(
shape: const CircleBorder(side: BorderSide(width: 1, color: Colors.black26)),
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
backgroundColor: Colors.indigo.withValues(alpha: 0.7), backgroundColor: Colors.indigo,
shadowColor: Colors.black26,
elevation: 4,
), ),
), ),
), ),

View File

@@ -36,7 +36,7 @@ class UnStackActionButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return BaseActionButton( return BaseActionButton(
iconData: Icons.filter_none_rounded, iconData: Icons.layers_clear_outlined,
label: "unstack".t(context: context), label: "unstack".t(context: context),
onPressed: () => _onTap(context, ref), onPressed: () => _onTap(context, ref),
); );

View File

@@ -51,6 +51,7 @@ class AssetDetailBottomSheet extends ConsumerWidget {
isArchived: isArchived, isArchived: isArchived,
isTrashEnabled: isTrashEnable, isTrashEnabled: isTrashEnable,
isInLockedView: isInLockedView, isInLockedView: isInLockedView,
isStacked: asset.hasRemote && (asset as RemoteAsset).stackId != null,
currentAlbum: currentAlbum, currentAlbum: currentAlbum,
advancedTroubleshooting: advancedTroubleshooting, advancedTroubleshooting: advancedTroubleshooting,
source: ActionSource.viewer, source: ActionSource.viewer,

View File

@@ -57,7 +57,7 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
final actions = <Widget>[ final actions = <Widget>[
if (asset.hasRemote) const DownloadActionButton(source: ActionSource.viewer, menuItem: true), if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true), if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
if (album != null && album.isActivityEnabled && album.isShared) if (album != null && album.isActivityEnabled && album.isShared)
IconButton( IconButton(

View File

@@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -44,6 +45,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
const EditLocationActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -13,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -44,6 +45,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
const EditLocationActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -5,6 +5,7 @@ import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.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/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/setting.model.dart'; import 'package:immich_mobile/domain/models/setting.model.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
@@ -19,6 +20,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
@@ -62,11 +64,19 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
return; return;
} }
final remoteAssets = selectedAssets.whereType<RemoteAsset>();
final addedCount = await ref final addedCount = await ref
.read(remoteAlbumProvider.notifier) .read(remoteAlbumProvider.notifier)
.addAssets(album.id, selectedAssets.map((e) => (e as RemoteAsset).id).toList()); .addAssets(album.id, remoteAssets.map((e) => e.id).toList());
if (addedCount != selectedAssets.length) { if (selectedAssets.length != remoteAssets.length) {
ImmichToast.show(
context: context,
msg: 'add_to_album_bottom_sheet_some_local_assets'.t(context: context),
);
}
if (addedCount != remoteAssets.length) {
ImmichToast.show( ImmichToast.show(
context: context, context: context,
msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}), msg: 'add_to_album_bottom_sheet_already_exists'.tr(namedArgs: {"album": album.name}),
@@ -108,15 +118,18 @@ class _GeneralBottomSheetState extends ConsumerState<GeneralBottomSheet> {
const EditLocationActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
const DeleteActionButton(source: ActionSource.timeline), const DeleteActionButton(source: ActionSource.timeline),
], ],
if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline), if (multiselect.hasLocal || multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
if (multiselect.hasLocal) const UploadActionButton(source: ActionSource.timeline), if (multiselect.hasLocal) const UploadActionButton(source: ActionSource.timeline),
], ],
slivers: [ slivers: multiselect.hasRemote
const AddToAlbumHeader(), ? [
AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand), const AddToAlbumHeader(),
], AlbumSelector(onAlbumSelected: addAssetsToAlbum, onKeyboardExpanded: onKeyboardExpand),
]
: [],
); );
} }
} }

View File

@@ -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/extensions/build_context_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart'; import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
@@ -10,13 +11,14 @@ class MapBottomSheet extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return const BaseBottomSheet( return BaseBottomSheet(
initialChildSize: 0.25, initialChildSize: 0.25,
maxChildSize: 0.9, maxChildSize: 0.9,
shouldCloseOnMinExtent: false, shouldCloseOnMinExtent: false,
resizeOnScroll: false, resizeOnScroll: false,
actions: [], actions: [],
slivers: [SliverFillRemaining(hasScrollBody: false, child: _ScopedMapTimeline())], backgroundColor: context.themeData.colorScheme.surface,
slivers: [const SliverFillRemaining(hasScrollBody: false, child: _ScopedMapTimeline())],
); );
} }
} }

View File

@@ -17,6 +17,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
@@ -102,6 +103,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
const EditLocationActionButton(source: ActionSource.timeline), const EditLocationActionButton(source: ActionSource.timeline),
const MoveToLockFolderActionButton(source: ActionSource.timeline), const MoveToLockFolderActionButton(source: ActionSource.timeline),
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasLocal) ...[
const DeleteLocalActionButton(source: ActionSource.timeline), const DeleteLocalActionButton(source: ActionSource.timeline),

View File

@@ -123,28 +123,14 @@ ImageProvider getFullImageProvider(BaseAsset asset, {Size size = const Size(1080
return provider; return provider;
} }
ImageProvider getThumbnailImageProvider({BaseAsset? asset, String? remoteId, Size size = kThumbnailResolution}) { ImageProvider? getThumbnailImageProvider(BaseAsset asset, {Size size = kThumbnailResolution}) {
assert(asset != null || remoteId != null, 'Either asset or remoteId must be provided'); if (_shouldUseLocalAsset(asset)) {
if (remoteId != null) {
return RemoteThumbProvider(assetId: remoteId);
}
if (_shouldUseLocalAsset(asset!)) {
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!; final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
return LocalThumbProvider(id: id, size: size, assetType: asset.type); return LocalThumbProvider(id: id, size: size, assetType: asset.type);
} }
final String assetId; final assetId = asset is RemoteAsset ? asset.id : (asset as LocalAsset).remoteId;
if (asset is LocalAsset && asset.hasRemote) { return assetId != null ? RemoteThumbProvider(assetId: assetId) : null;
assetId = asset.remoteId!;
} else if (asset is RemoteAsset) {
assetId = asset.id;
} else {
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
}
return RemoteThumbProvider(assetId: assetId);
} }
bool _shouldUseLocalAsset(BaseAsset asset) => bool _shouldUseLocalAsset(BaseAsset asset) =>

View File

@@ -5,7 +5,6 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart'; import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart';
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart'; import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
@@ -39,14 +38,7 @@ class Thumbnail extends StatefulWidget {
), ),
_ => null, _ => null,
}, },
imageProvider = switch (asset) { imageProvider = asset == null ? null : getThumbnailImageProvider(asset, size: size);
RemoteAsset() =>
asset.localId == null
? RemoteThumbProvider(assetId: asset.id)
: LocalThumbProvider(id: asset.localId!, size: size, assetType: asset.type),
LocalAsset() => LocalThumbProvider(id: asset.id, size: size, assetType: asset.type),
_ => null,
};
@override @override
State<Thumbnail> createState() => _ThumbnailState(); State<Thumbnail> createState() => _ThumbnailState();

View File

@@ -54,8 +54,6 @@ class ThumbnailTile extends ConsumerWidget {
) )
: const BoxDecoration(); : const BoxDecoration();
final hasStack = asset is RemoteAsset && asset.stackId != null;
final bool storageIndicator = final bool storageIndicator =
showStorageIndicator ?? ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator))); showStorageIndicator ?? ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator)));
@@ -77,21 +75,10 @@ class ThumbnailTile extends ConsumerWidget {
child: Thumbnail.fromAsset(asset: asset, size: size), child: Thumbnail.fromAsset(asset: asset, size: size),
), ),
), ),
if (hasStack) if (asset != null)
Align( Align(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: Padding( child: _AssetTypeIcons(asset: asset),
padding: EdgeInsets.only(right: 10.0, top: asset.isVideo ? 24.0 : 6.0),
child: const _TileOverlayIcon(Icons.burst_mode_rounded),
),
),
if (asset != null && asset.isVideo)
Align(
alignment: Alignment.topRight,
child: Padding(
padding: const EdgeInsets.only(right: 10.0, top: 6.0),
child: _VideoIndicator(asset.duration),
),
), ),
if (storageIndicator && asset != null) if (storageIndicator && asset != null)
switch (asset.storage) { switch (asset.storage) {
@@ -214,3 +201,34 @@ class _TileOverlayIcon extends StatelessWidget {
); );
} }
} }
class _AssetTypeIcons extends StatelessWidget {
final BaseAsset asset;
const _AssetTypeIcons({required this.asset});
@override
Widget build(BuildContext context) {
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
final isLivePhoto = asset is RemoteAsset && asset.livePhotoVideoId != null;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (asset.isVideo)
Padding(padding: const EdgeInsets.only(right: 10.0, top: 6.0), child: _VideoIndicator(asset.duration)),
if (hasStack)
const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0),
child: _TileOverlayIcon(Icons.burst_mode_rounded),
),
if (isLivePhoto)
const Padding(
padding: EdgeInsets.only(right: 10.0, top: 6.0),
child: _TileOverlayIcon(Icons.motion_photos_on_rounded),
),
],
);
}
}

View File

@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
@@ -9,8 +10,8 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart';
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
import 'package:immich_mobile/presentation/widgets/map/map.state.dart'; import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
import 'package:immich_mobile/utils/async_mutex.dart'; import 'package:immich_mobile/utils/async_mutex.dart';
import 'package:immich_mobile/utils/debounce.dart'; import 'package:immich_mobile/utils/debounce.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart';
@@ -187,6 +188,8 @@ class _Map extends StatelessWidget {
styleString: style, styleString: style,
onMapCreated: onMapCreated, onMapCreated: onMapCreated,
onStyleLoadedCallback: onMapReady, onStyleLoadedCallback: onMapReady,
attributionButtonPosition: AttributionButtonPosition.topRight,
attributionButtonMargins: Platform.isIOS ? const Point(40, 12) : const Point(40, 72),
), ),
), ),
); );

View File

@@ -212,11 +212,14 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
if (fallbackSegment != null) { if (fallbackSegment != null) {
// Scroll to the segment with a small offset to show the header // Scroll to the segment with a small offset to show the header
final targetOffset = fallbackSegment.startOffset - 50; final targetOffset = fallbackSegment.startOffset - 50;
_scrollController.animateTo( ref.read(timelineStateProvider.notifier).setScrubbing(true);
targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent), _scrollController
duration: const Duration(milliseconds: 500), .animateTo(
curve: Curves.easeInOut, targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent),
); duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
)
.whenComplete(() => ref.read(timelineStateProvider.notifier).setScrubbing(false));
} }
}); });
} }

View File

@@ -1,6 +1,5 @@
// ignore_for_file: public_member_api_docs, sort_constructors_first // ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
@@ -12,8 +11,8 @@ import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/services/upload.service.dart'; import 'package:immich_mobile/services/upload.service.dart';
import 'package:logging/logging.dart';
import 'package:immich_mobile/utils/debug_print.dart'; import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
class EnqueueStatus { class EnqueueStatus {
final int enqueueCount; final int enqueueCount;
@@ -90,33 +89,6 @@ class DriftUploadStatus {
networkSpeedAsString.hashCode ^ networkSpeedAsString.hashCode ^
isFailed.hashCode; isFailed.hashCode;
} }
Map<String, dynamic> toMap() {
return <String, dynamic>{
'taskId': taskId,
'filename': filename,
'progress': progress,
'fileSize': fileSize,
'networkSpeedAsString': networkSpeedAsString,
'isFailed': isFailed,
};
}
factory DriftUploadStatus.fromMap(Map<String, dynamic> map) {
return DriftUploadStatus(
taskId: map['taskId'] as String,
filename: map['filename'] as String,
progress: map['progress'] as double,
fileSize: map['fileSize'] as int,
networkSpeedAsString: map['networkSpeedAsString'] as String,
isFailed: map['isFailed'] != null ? map['isFailed'] as bool : null,
);
}
String toJson() => json.encode(toMap());
factory DriftUploadStatus.fromJson(String source) =>
DriftUploadStatus.fromMap(json.decode(source) as Map<String, dynamic>);
} }
class DriftBackupState { class DriftBackupState {
@@ -267,6 +239,7 @@ class DriftBackupNotifier extends StateNotifier<DriftBackupState> {
} }
state = state.copyWith(uploadItems: {...state.uploadItems, taskId: currentItem.copyWith(isFailed: true)}); state = state.copyWith(uploadItems: {...state.uploadItems, taskId: currentItem.copyWith(isFailed: true)});
_logger.fine("Upload failed for taskId: $taskId, exception: ${update.exception}");
break; break;
case TaskStatus.canceled: case TaskStatus.canceled:

View File

@@ -3,7 +3,10 @@ import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/services/asset.service.dart';
import 'package:immich_mobile/models/download/livephotos_medatada.model.dart'; import 'package:immich_mobile/models/download/livephotos_medatada.model.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
@@ -36,6 +39,7 @@ class ActionNotifier extends Notifier<void> {
late ActionService _service; late ActionService _service;
late UploadService _uploadService; late UploadService _uploadService;
late DownloadService _downloadService; late DownloadService _downloadService;
late AssetService _assetService;
ActionNotifier() : super(); ActionNotifier() : super();
@@ -43,6 +47,7 @@ class ActionNotifier extends Notifier<void> {
void build() { void build() {
_uploadService = ref.watch(uploadServiceProvider); _uploadService = ref.watch(uploadServiceProvider);
_service = ref.watch(actionServiceProvider); _service = ref.watch(actionServiceProvider);
_assetService = ref.watch(assetServiceProvider);
_downloadService = ref.watch(downloadServiceProvider); _downloadService = ref.watch(downloadServiceProvider);
_downloadService.onImageDownloadStatus = _downloadImageCallback; _downloadService.onImageDownloadStatus = _downloadImageCallback;
_downloadService.onVideoDownloadStatus = _downloadVideoCallback; _downloadService.onVideoDownloadStatus = _downloadVideoCallback;
@@ -335,6 +340,14 @@ class ActionNotifier extends Notifier<void> {
final assets = _getOwnedRemoteAssetsForSource(source); final assets = _getOwnedRemoteAssetsForSource(source);
try { try {
await _service.unStack(assets.map((e) => e.stackId).nonNulls.toList()); await _service.unStack(assets.map((e) => e.stackId).nonNulls.toList());
if (source == ActionSource.viewer) {
final updatedParent = await _assetService.getRemoteAsset(assets.first.id);
if (updatedParent != null) {
ref.read(currentAssetNotifier.notifier).setAsset(updatedParent);
ref.read(assetViewerProvider.notifier).setAsset(updatedParent);
}
}
return ActionResult(count: assets.length, success: true); return ActionResult(count: assets.length, success: true);
} catch (error, stack) { } catch (error, stack) {
_logger.severe('Failed to unstack assets', error, stack); _logger.severe('Failed to unstack assets', error, stack);

View File

@@ -28,6 +28,8 @@ class MultiSelectState {
bool get hasRemote => bool get hasRemote =>
selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged); selectedAssets.any((asset) => asset.storage == AssetState.remote || asset.storage == AssetState.merged);
bool get hasStacked => selectedAssets.any((asset) => asset is RemoteAsset && asset.stackId != null);
bool get hasLocal => selectedAssets.any((asset) => asset.storage == AssetState.local); bool get hasLocal => selectedAssets.any((asset) => asset.storage == AssetState.local);
bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged); bool get hasMerged => selectedAssets.any((asset) => asset.storage == AssetState.merged);

View File

@@ -9,6 +9,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/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart'; import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/backup.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart'; import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
@@ -19,9 +20,9 @@ import 'package:immich_mobile/providers/infrastructure/storage.provider.dart';
import 'package:immich_mobile/repositories/upload.repository.dart'; import 'package:immich_mobile/repositories/upload.repository.dart';
import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:immich_mobile/utils/debug_print.dart';
final uploadServiceProvider = Provider((ref) { final uploadServiceProvider = Provider((ref) {
final service = UploadService( final service = UploadService(
@@ -205,10 +206,20 @@ class UploadService {
return _uploadRepository.start(); return _uploadRepository.start();
} }
void _handleTaskStatusUpdate(TaskStatusUpdate update) { void _handleTaskStatusUpdate(TaskStatusUpdate update) async {
switch (update.status) { switch (update.status) {
case TaskStatus.complete: case TaskStatus.complete:
_handleLivePhoto(update); _handleLivePhoto(update);
if (CurrentPlatform.isIOS) {
try {
final path = await update.task.filePath();
await File(path).delete();
} catch (e) {
_logger.severe('Error deleting file path for iOS: $e');
}
}
break; break;
default: default:

View File

@@ -16,6 +16,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
class ActionButtonContext { class ActionButtonContext {
@@ -24,6 +25,7 @@ class ActionButtonContext {
final bool isArchived; final bool isArchived;
final bool isTrashEnabled; final bool isTrashEnabled;
final bool isInLockedView; final bool isInLockedView;
final bool isStacked;
final RemoteAlbum? currentAlbum; final RemoteAlbum? currentAlbum;
final bool advancedTroubleshooting; final bool advancedTroubleshooting;
final ActionSource source; final ActionSource source;
@@ -33,6 +35,7 @@ class ActionButtonContext {
required this.isOwner, required this.isOwner,
required this.isArchived, required this.isArchived,
required this.isTrashEnabled, required this.isTrashEnabled,
required this.isStacked,
required this.isInLockedView, required this.isInLockedView,
required this.currentAlbum, required this.currentAlbum,
required this.advancedTroubleshooting, required this.advancedTroubleshooting,
@@ -55,6 +58,7 @@ enum ActionButtonType {
deleteLocal, deleteLocal,
upload, upload,
removeFromAlbum, removeFromAlbum,
unstack,
likeActivity; likeActivity;
bool shouldShow(ActionButtonContext context) { bool shouldShow(ActionButtonContext context) {
@@ -110,6 +114,10 @@ enum ActionButtonType {
context.isOwner && // context.isOwner && //
!context.isInLockedView && // !context.isInLockedView && //
context.currentAlbum != null, context.currentAlbum != null,
ActionButtonType.unstack =>
context.isOwner && //
!context.isInLockedView && //
context.isStacked,
ActionButtonType.likeActivity => ActionButtonType.likeActivity =>
!context.isInLockedView && !context.isInLockedView &&
context.currentAlbum != null && context.currentAlbum != null &&
@@ -138,28 +146,13 @@ enum ActionButtonType {
source: context.source, source: context.source,
), ),
ActionButtonType.likeActivity => const LikeActivityActionButton(), ActionButtonType.likeActivity => const LikeActivityActionButton(),
ActionButtonType.unstack => UnStackActionButton(source: context.source),
}; };
} }
} }
class ActionButtonBuilder { class ActionButtonBuilder {
static const List<ActionButtonType> _actionTypes = [ static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
ActionButtonType.advancedInfo,
ActionButtonType.share,
ActionButtonType.shareLink,
ActionButtonType.likeActivity,
ActionButtonType.archive,
ActionButtonType.unarchive,
ActionButtonType.download,
ActionButtonType.trash,
ActionButtonType.deletePermanent,
ActionButtonType.delete,
ActionButtonType.moveToLockFolder,
ActionButtonType.removeFromLockFolder,
ActionButtonType.deleteLocal,
ActionButtonType.upload,
ActionButtonType.removeFromAlbum,
];
static List<Widget> build(ActionButtonContext context) { static List<Widget> build(ActionButtonContext context) {
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList(); return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();

View File

@@ -129,19 +129,24 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
title: Builder( title: Builder(
builder: (BuildContext context) { builder: (BuildContext context) {
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Builder( Padding(
builder: (context) { padding: const EdgeInsets.only(top: 3.0),
return Padding( child: SvgPicture.asset(
padding: const EdgeInsets.only(top: 3.0), context.isDarkTheme ? 'assets/immich-logo-inline-dark.svg' : 'assets/immich-logo-inline-light.svg',
child: SvgPicture.asset( height: 40,
context.isDarkTheme ),
? 'assets/immich-logo-inline-dark.svg' ),
: 'assets/immich-logo-inline-light.svg', const Tooltip(
height: 40, triggerMode: TooltipTriggerMode.tap,
), showDuration: Duration(seconds: 4),
); message:
}, "The old timeline is deprecated and will be removed in a future release. Kindly switch to the new timeline under Advanced Settings.",
child: Padding(
padding: EdgeInsets.only(top: 3.0),
child: Icon(Icons.error_rounded, fill: 1, color: Colors.amber, size: 20),
),
), ),
], ],
); );

View File

@@ -82,6 +82,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -112,6 +113,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -127,6 +129,7 @@ void main() {
isInLockedView: true, isInLockedView: true,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -145,6 +148,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -161,6 +165,7 @@ void main() {
isInLockedView: true, isInLockedView: true,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -177,6 +182,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -195,6 +201,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -211,6 +218,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -227,6 +235,7 @@ void main() {
isInLockedView: true, isInLockedView: true,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -243,6 +252,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -259,6 +269,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -277,6 +288,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -293,6 +305,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -309,6 +322,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -327,6 +341,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -343,6 +358,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -359,6 +375,7 @@ void main() {
isInLockedView: true, isInLockedView: true,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -377,6 +394,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -393,6 +411,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -411,6 +430,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -427,6 +447,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -445,6 +466,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -463,6 +485,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -481,6 +504,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -497,6 +521,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -512,6 +537,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -530,6 +556,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -548,6 +575,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -563,6 +591,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -581,6 +610,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -597,6 +627,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -613,6 +644,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -628,6 +660,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -645,6 +678,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: true, advancedTroubleshooting: true,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -660,6 +694,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -668,6 +703,59 @@ void main() {
}); });
}); });
group('unstack button', () {
test('should show when owner, not locked, has remote, and is stacked', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: true,
source: ActionSource.timeline,
);
expect(ActionButtonType.unstack.shouldShow(context), isTrue);
});
test('should not show when not stacked', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
expect(ActionButtonType.unstack.shouldShow(context), isFalse);
});
test('should not show when not owner', () {
final remoteAsset = createRemoteAsset();
final context = ActionButtonContext(
asset: remoteAsset,
isOwner: false,
isArchived: true,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: null,
advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
expect(ActionButtonType.unstack.shouldShow(context), isFalse);
});
});
group('ActionButtonType.buildButton', () { group('ActionButtonType.buildButton', () {
late BaseAsset asset; late BaseAsset asset;
late ActionButtonContext context; late ActionButtonContext context;
@@ -682,6 +770,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
}); });
@@ -698,6 +787,22 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline,
);
final widget = buttonType.buildButton(contextWithAlbum);
expect(widget, isA<Widget>());
} else if (buttonType == ActionButtonType.unstack) {
final album = createRemoteAlbum();
final contextWithAlbum = ActionButtonContext(
asset: asset,
isOwner: true,
isArchived: false,
isTrashEnabled: true,
isInLockedView: false,
currentAlbum: album,
advancedTroubleshooting: false,
isStacked: true,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
final widget = buttonType.buildButton(contextWithAlbum); final widget = buttonType.buildButton(contextWithAlbum);
@@ -721,6 +826,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -741,6 +847,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: album, currentAlbum: album,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -759,6 +866,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -778,6 +886,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );
@@ -791,6 +900,7 @@ void main() {
isInLockedView: false, isInLockedView: false,
currentAlbum: null, currentAlbum: null,
advancedTroubleshooting: false, advancedTroubleshooting: false,
isStacked: false,
source: ActionSource.timeline, source: ActionSource.timeline,
); );

View File

@@ -1,18 +1,20 @@
<script lang="ts"> <script lang="ts">
interface Props { interface Props {
height: number; height: number;
title: string; title?: string;
} }
let { height = 0, title }: Props = $props(); let { height = 0, title }: Props = $props();
</script> </script>
<div class="overflow-clip" style:height={height + 'px'}> <div class="overflow-clip" style:height={height + 'px'}>
<div {#if title}
class="flex pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-light dark:text-immich-dark-fg md:text-sm" <div
> class="flex pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-light dark:text-immich-dark-fg md:text-sm"
{title} >
</div> {title}
</div>
{/if}
<div <div
class="animate-pulse absolute h-full ms-[10px] me-[10px]" class="animate-pulse absolute h-full ms-[10px] me-[10px]"
style:width="calc(100% - 20px)" style:width="calc(100% - 20px)"