Compare commits
17 Commits
feat/serve
...
refactor/t
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
41066b1c31 | ||
|
|
9051fa6949 | ||
|
|
bea116e1b9 | ||
|
|
cdbe1d7f10 | ||
|
|
df469cc412 | ||
|
|
8de7eed940 | ||
|
|
7d8cd05bc2 | ||
|
|
30a378c580 | ||
|
|
8a3684c127 | ||
|
|
61e5c6349c | ||
|
|
3bcb4b7af7 | ||
|
|
5116b215a2 | ||
|
|
c5fbbee8f6 | ||
|
|
d73aabc494 | ||
|
|
b62feb726b | ||
|
|
972e9cc039 | ||
|
|
ee49136e97 |
6
.github/workflows/build-mobile.yml
vendored
6
.github/workflows/build-mobile.yml
vendored
@@ -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: |
|
||||||
|
|||||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -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:
|
||||||
|
|||||||
2
.github/workflows/docs-build.yml
vendored
2
.github/workflows/docs-build.yml
vendored
@@ -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:
|
||||||
|
|||||||
2
.github/workflows/fix-format.yml
vendored
2
.github/workflows/fix-format.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
2
.github/workflows/merge-translations.yml
vendored
2
.github/workflows/merge-translations.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
4
.github/workflows/prepare-release.yml
vendored
4
.github/workflows/prepare-release.yml
vendored
@@ -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 }}
|
||||||
|
|||||||
2
.github/workflows/static_analysis.yml
vendored
2
.github/workflows/static_analysis.yml
vendored
@@ -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:
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -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:
|
||||||
|
|||||||
2
.github/workflows/weblate-lock.yml
vendored
2
.github/workflows/weblate-lock.yml
vendored
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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})",
|
||||||
|
|||||||
@@ -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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
|
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { getTabbable } from '$lib/utils/focus-util';
|
import { getTabbable } from '$lib/utils/focus-util';
|
||||||
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
import { type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
import { Icon } from '@immich/ui';
|
import { Icon } from '@immich/ui';
|
||||||
import { mdiPlay } from '@mdi/js';
|
import { mdiPlay } from '@mdi/js';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
@@ -11,18 +11,31 @@
|
|||||||
import { fade, fly } from 'svelte/transition';
|
import { fade, fly } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/** Offset from the top of the timeline (e.g., for headers) */
|
||||||
timelineTopOffset?: number;
|
timelineTopOffset?: number;
|
||||||
|
/** Offset from the bottom of the timeline (e.g., for footers) */
|
||||||
timelineBottomOffset?: number;
|
timelineBottomOffset?: number;
|
||||||
|
/** Total height of the scrubber component */
|
||||||
height?: number;
|
height?: number;
|
||||||
|
/** Timeline manager instance that controls the timeline state */
|
||||||
timelineManager: TimelineManager;
|
timelineManager: TimelineManager;
|
||||||
scrubOverallPercent?: number;
|
/** Overall scroll percentage through the entire timeline (0-1), used when no specific month is targeted */
|
||||||
scrubberMonthPercent?: number;
|
timelineScrollPercent?: number;
|
||||||
scrubberMonth?: { year: number; month: number };
|
/** The percentage of scroll through the month that is currently intersecting the top boundary of the viewport */
|
||||||
leadout?: boolean;
|
viewportTopMonthScrollPercent?: number;
|
||||||
|
/** The year/month of the timeline month at the top of the viewport */
|
||||||
|
viewportTopMonth?: TimelineYearMonth;
|
||||||
|
/** Indicates whether the viewport is currently in the lead-out section (after all months) */
|
||||||
|
isInLeadOutSection?: boolean;
|
||||||
|
/** Width of the scrubber component in pixels (bindable for parent component margin adjustments) */
|
||||||
scrubberWidth?: number;
|
scrubberWidth?: number;
|
||||||
|
/** Callback fired when user interacts with the scrubber to navigate */
|
||||||
onScrub?: ScrubberListener;
|
onScrub?: ScrubberListener;
|
||||||
|
/** Callback fired when keyboard events occur on the scrubber */
|
||||||
onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
|
onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
|
||||||
|
/** Callback fired when scrubbing starts */
|
||||||
startScrub?: ScrubberListener;
|
startScrub?: ScrubberListener;
|
||||||
|
/** Callback fired when scrubbing stops */
|
||||||
stopScrub?: ScrubberListener;
|
stopScrub?: ScrubberListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,10 +44,10 @@
|
|||||||
timelineBottomOffset = 0,
|
timelineBottomOffset = 0,
|
||||||
height = 0,
|
height = 0,
|
||||||
timelineManager,
|
timelineManager,
|
||||||
scrubOverallPercent = 0,
|
timelineScrollPercent = 0,
|
||||||
scrubberMonthPercent = 0,
|
viewportTopMonthScrollPercent = 0,
|
||||||
scrubberMonth = undefined,
|
viewportTopMonth = undefined,
|
||||||
leadout = false,
|
isInLeadOutSection = false,
|
||||||
onScrub = undefined,
|
onScrub = undefined,
|
||||||
onScrubKeyDown = undefined,
|
onScrubKeyDown = undefined,
|
||||||
startScrub = undefined,
|
startScrub = undefined,
|
||||||
@@ -100,7 +113,7 @@
|
|||||||
offset += scrubberMonthPercent * relativeBottomOffset;
|
offset += scrubberMonthPercent * relativeBottomOffset;
|
||||||
}
|
}
|
||||||
return offset;
|
return offset;
|
||||||
} else if (leadout) {
|
} else if (isInLeadOutSection) {
|
||||||
let offset = relativeTopOffset;
|
let offset = relativeTopOffset;
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
offset += segment.height;
|
offset += segment.height;
|
||||||
@@ -111,7 +124,9 @@
|
|||||||
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let scrollY = $derived(toScrollFromMonthGroupPercentage(scrubberMonth, scrubberMonthPercent, scrubOverallPercent));
|
let scrollY = $derived(
|
||||||
|
toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
|
||||||
|
);
|
||||||
let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
|
let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
|
||||||
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
||||||
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
||||||
@@ -295,12 +310,24 @@
|
|||||||
|
|
||||||
const scrollPercent = toTimelineY(hoverY);
|
const scrollPercent = toTimelineY(hoverY);
|
||||||
if (wasDragging === false && isDragging) {
|
if (wasDragging === false && isDragging) {
|
||||||
void startScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
void startScrub?.({
|
||||||
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
|
void onScrub?.({
|
||||||
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wasDragging && !isDragging) {
|
if (wasDragging && !isDragging) {
|
||||||
void stopScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
void stopScrub?.({
|
||||||
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +335,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
void onScrub?.({
|
||||||
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
/* eslint-disable tscompat/tscompat */
|
/* eslint-disable tscompat/tscompat */
|
||||||
const getTouch = (event: TouchEvent) => {
|
const getTouch = (event: TouchEvent) => {
|
||||||
@@ -412,7 +443,11 @@
|
|||||||
}
|
}
|
||||||
if (next) {
|
if (next) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void onScrub?.({ year: next.year, month: next.month }, -1, 0);
|
void onScrub?.({
|
||||||
|
scrubberMonth: { year: next.year, month: next.month },
|
||||||
|
overallScrollPercent: -1,
|
||||||
|
scrubberMonthScrollPercent: 0,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,7 +457,11 @@
|
|||||||
const next = segments[idx + 1];
|
const next = segments[idx + 1];
|
||||||
if (next) {
|
if (next) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void onScrub?.({ year: next.year, month: next.month }, -1, 0);
|
void onScrub?.({
|
||||||
|
scrubberMonth: { year: next.year, month: next.month },
|
||||||
|
overallScrollPercent: -1,
|
||||||
|
scrubberMonthScrollPercent: 0,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { afterNavigate, beforeNavigate, goto } from '$app/navigation';
|
import { afterNavigate, beforeNavigate } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
import { resizeObserver, type OnResizeCallback } from '$lib/actions/resize-observer';
|
||||||
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
|
||||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||||
import type { AbsoluteResult, RelativeResult } from '$lib/components/shared-components/change-date.svelte';
|
|
||||||
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
|
||||||
import Scrubber from '$lib/components/timeline/Scrubber.svelte';
|
import Scrubber from '$lib/components/timeline/Scrubber.svelte';
|
||||||
import {
|
import TimelineKeyboardActions from '$lib/components/timeline/actions/TimelineKeyboardActions.svelte';
|
||||||
setFocusToAsset as setFocusAssetInit,
|
import { AssetAction } from '$lib/constants';
|
||||||
setFocusTo as setFocusToInit,
|
|
||||||
} from '$lib/components/timeline/actions/focus-actions';
|
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
|
||||||
import HotModuleReload from '$lib/elements/HotModuleReload.svelte';
|
import HotModuleReload from '$lib/elements/HotModuleReload.svelte';
|
||||||
import Portal from '$lib/elements/Portal.svelte';
|
import Portal from '$lib/elements/Portal.svelte';
|
||||||
import Skeleton from '$lib/elements/Skeleton.svelte';
|
import Skeleton from '$lib/elements/Skeleton.svelte';
|
||||||
@@ -21,25 +15,17 @@
|
|||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
import { searchStore } from '$lib/stores/search.svelte';
|
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
|
||||||
import { handlePromiseError } from '$lib/utils';
|
|
||||||
import { deleteAssets, updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
|
||||||
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
import { getTimes, toTimelineAsset, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||||
import { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
import { getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||||
import { modalManager } from '@immich/ui';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import type { UpdatePayload } from 'vite';
|
import type { UpdatePayload } from 'vite';
|
||||||
import DeleteAssetDialog from '../photos-page/delete-asset-dialog.svelte';
|
|
||||||
import TimelineDateGroup from './TimelineDateGroup.svelte';
|
import TimelineDateGroup from './TimelineDateGroup.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -109,16 +95,21 @@
|
|||||||
|
|
||||||
let timelineElement: HTMLElement | undefined = $state();
|
let timelineElement: HTMLElement | undefined = $state();
|
||||||
let showSkeleton = $state(true);
|
let showSkeleton = $state(true);
|
||||||
let isShowSelectDate = $state(false);
|
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
|
||||||
let scrubberMonthPercent = $state(0);
|
// Note: There may be multiple months visible within the viewport at any given time.
|
||||||
let scrubberMonth: { year: number; month: number } | undefined = $state(undefined);
|
let viewportTopMonthScrollPercent = $state(0);
|
||||||
let scrubOverallPercent: number = $state(0);
|
// The timeline month intersecting the top position of the viewport
|
||||||
|
let viewportTopMonth: { year: number; month: number } | undefined = $state(undefined);
|
||||||
|
// Overall scroll percentage through the entire timeline (0-1)
|
||||||
|
let timelineScrollPercent: number = $state(0);
|
||||||
let scrubberWidth = $state(0);
|
let scrubberWidth = $state(0);
|
||||||
|
|
||||||
// 60 is the bottom spacer element at 60px
|
// 60 is the bottom spacer element at 60px
|
||||||
let bottomSectionHeight = 60;
|
let bottomSectionHeight = 60;
|
||||||
let leadout = $state(false);
|
// Indicates whether the viewport is currently in the lead-out section (after all months)
|
||||||
|
let isInLeadOutSection = $state(false);
|
||||||
|
|
||||||
|
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||||
const maxMd = $derived(mobileDevice.maxMd);
|
const maxMd = $derived(mobileDevice.maxMd);
|
||||||
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
||||||
|
|
||||||
@@ -301,20 +292,19 @@
|
|||||||
scrollTop(scrollToTop);
|
scrollTop(scrollToTop);
|
||||||
};
|
};
|
||||||
|
|
||||||
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker
|
||||||
const onScrub: ScrubberListener = (
|
// this function scrolls the timeline to the specified month group and offset, based on scrubber interaction
|
||||||
scrubMonth: { year: number; month: number },
|
const onScrub: ScrubberListener = (scrubberData) => {
|
||||||
overallScrollPercent: number,
|
const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent } = scrubberData;
|
||||||
scrubberMonthScrollPercent: number,
|
|
||||||
) => {
|
if (!scrubberMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||||
if (!scrubMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
|
||||||
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
||||||
const maxScroll = getMaxScroll();
|
const maxScroll = getMaxScroll();
|
||||||
const offset = maxScroll * overallScrollPercent;
|
const offset = maxScroll * overallScrollPercent;
|
||||||
scrollTop(offset);
|
scrollTop(offset);
|
||||||
} else {
|
} else {
|
||||||
const monthGroup = timelineManager.months.find(
|
const monthGroup = timelineManager.months.find(
|
||||||
({ yearMonth: { year, month } }) => year === scrubMonth.year && month === scrubMonth.month,
|
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
|
||||||
);
|
);
|
||||||
if (!monthGroup) {
|
if (!monthGroup) {
|
||||||
return;
|
return;
|
||||||
@@ -325,7 +315,7 @@
|
|||||||
|
|
||||||
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
||||||
const handleTimelineScroll = () => {
|
const handleTimelineScroll = () => {
|
||||||
leadout = false;
|
isInLeadOutSection = false;
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
@@ -334,19 +324,19 @@
|
|||||||
if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||||
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
|
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
|
||||||
const maxScroll = getMaxScroll();
|
const maxScroll = getMaxScroll();
|
||||||
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
|
timelineScrollPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||||
|
|
||||||
scrubberMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
} else {
|
} else {
|
||||||
let top = element.scrollTop;
|
let top = element.scrollTop;
|
||||||
if (top < timelineManager.topSectionHeight) {
|
if (top < timelineManager.topSectionHeight) {
|
||||||
// in the lead-in area
|
// in the lead-in area
|
||||||
scrubberMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
const maxScroll = getMaxScroll();
|
const maxScroll = getMaxScroll();
|
||||||
|
|
||||||
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
|
timelineScrollPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,15 +361,15 @@
|
|||||||
let next = top - monthGroupHeight * maxScrollPercent;
|
let next = top - monthGroupHeight * maxScrollPercent;
|
||||||
// instead of checking for < 0, add a little wiggle room for subpixel resolution
|
// instead of checking for < 0, add a little wiggle room for subpixel resolution
|
||||||
if (next < -1 && monthGroup) {
|
if (next < -1 && monthGroup) {
|
||||||
scrubberMonth = monthGroup;
|
viewportTopMonth = monthGroup;
|
||||||
|
|
||||||
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
|
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
|
||||||
scrubberMonthPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent));
|
viewportTopMonthScrollPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent));
|
||||||
|
|
||||||
// compensate for lost precision/rounding errors advance to the next bucket, if present
|
// compensate for lost precision/rounding errors advance to the next bucket, if present
|
||||||
if (scrubberMonthPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
if (viewportTopMonthScrollPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
||||||
scrubberMonth = timelineManager.months[i + 1].yearMonth;
|
viewportTopMonth = timelineManager.months[i + 1].yearMonth;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
found = true;
|
found = true;
|
||||||
@@ -388,61 +378,14 @@
|
|||||||
top = next;
|
top = next;
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
leadout = true;
|
isInLeadOutSection = true;
|
||||||
scrubberMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
scrubOverallPercent = 1;
|
timelineScrollPercent = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const trashOrDelete = async (force: boolean = false) => {
|
|
||||||
isShowDeleteConfirmation = false;
|
|
||||||
await deleteAssets(
|
|
||||||
!(isTrashEnabled && !force),
|
|
||||||
(assetIds) => timelineManager.removeAssets(assetIds),
|
|
||||||
assetInteraction.selectedAssets,
|
|
||||||
!isTrashEnabled || force ? undefined : (assets) => timelineManager.addAssets(assets),
|
|
||||||
);
|
|
||||||
assetInteraction.clearMultiselect();
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDelete = () => {
|
|
||||||
const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
|
|
||||||
|
|
||||||
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
|
|
||||||
isShowDeleteConfirmation = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlePromiseError(trashOrDelete(hasTrashedAsset));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onForceDelete = () => {
|
|
||||||
if ($showDeleteModal) {
|
|
||||||
isShowDeleteConfirmation = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
handlePromiseError(trashOrDelete(true));
|
|
||||||
};
|
|
||||||
|
|
||||||
const onStackAssets = async () => {
|
|
||||||
const result = await stackAssets(assetInteraction.selectedAssets);
|
|
||||||
|
|
||||||
updateStackedAssetInTimeline(timelineManager, result);
|
|
||||||
|
|
||||||
onEscape();
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleArchive = async () => {
|
|
||||||
const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive;
|
|
||||||
const ids = await archiveAssets(assetInteraction.selectedAssets, visibility);
|
|
||||||
timelineManager.updateAssetOperation(ids, (asset) => {
|
|
||||||
asset.visibility = visibility;
|
|
||||||
return { remove: false };
|
|
||||||
});
|
|
||||||
deselectAllAssets();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSelectAsset = (asset: TimelineAsset) => {
|
const handleSelectAsset = (asset: TimelineAsset) => {
|
||||||
if (!timelineManager.albumAssets.has(asset.id)) {
|
if (!timelineManager.albumAssets.has(asset.id)) {
|
||||||
assetInteraction.selectAsset(asset);
|
assetInteraction.selectAsset(asset);
|
||||||
@@ -573,15 +516,7 @@
|
|||||||
|
|
||||||
let shiftKeyIsDown = $state(false);
|
let shiftKeyIsDown = $state(false);
|
||||||
|
|
||||||
const deselectAllAssets = () => {
|
|
||||||
cancelMultiselect(assetInteraction);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onKeyDown = (event: KeyboardEvent) => {
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
if (searchStore.isSearchEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Shift') {
|
if (event.key === 'Shift') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
shiftKeyIsDown = true;
|
shiftKeyIsDown = true;
|
||||||
@@ -589,16 +524,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onKeyUp = (event: KeyboardEvent) => {
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
if (searchStore.isSearchEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event.key === 'Shift') {
|
if (event.key === 'Shift') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
shiftKeyIsDown = false;
|
shiftKeyIsDown = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAssetCandidates = (asset: TimelineAsset | null) => {
|
const handleSelectAssetCandidates = (asset: TimelineAsset | null) => {
|
||||||
if (asset) {
|
if (asset) {
|
||||||
void selectAssetCandidates(asset);
|
void selectAssetCandidates(asset);
|
||||||
@@ -724,72 +654,6 @@
|
|||||||
assetInteraction.setAssetSelectionCandidates(assets);
|
assetInteraction.setAssetSelectionCandidates(assets);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectStart = (e: Event) => {
|
|
||||||
if (assetInteraction.selectionActive && shiftKeyIsDown) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
|
|
||||||
let isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
|
||||||
let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
|
|
||||||
let isShortcutModalOpen = false;
|
|
||||||
|
|
||||||
const handleOpenShortcutModal = async () => {
|
|
||||||
if (isShortcutModalOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isShortcutModalOpen = true;
|
|
||||||
await modalManager.show(ShortcutsModal, {});
|
|
||||||
isShortcutModalOpen = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (isEmpty) {
|
|
||||||
assetInteraction.clearMultiselect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
|
|
||||||
const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
|
|
||||||
|
|
||||||
let shortcutList = $derived(
|
|
||||||
(() => {
|
|
||||||
if (searchStore.isSearchEnabled || $showAssetViewer) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortcuts: ShortcutOptions[] = [
|
|
||||||
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
|
|
||||||
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
|
|
||||||
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
|
|
||||||
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(timelineManager, assetInteraction) },
|
|
||||||
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') },
|
|
||||||
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') },
|
|
||||||
{ shortcut: { key: 'D' }, onShortcut: () => setFocusTo('earlier', 'day') },
|
|
||||||
{ shortcut: { key: 'D', shift: true }, onShortcut: () => setFocusTo('later', 'day') },
|
|
||||||
{ shortcut: { key: 'M' }, onShortcut: () => setFocusTo('earlier', 'month') },
|
|
||||||
{ shortcut: { key: 'M', shift: true }, onShortcut: () => setFocusTo('later', 'month') },
|
|
||||||
{ shortcut: { key: 'Y' }, onShortcut: () => setFocusTo('earlier', 'year') },
|
|
||||||
{ shortcut: { key: 'Y', shift: true }, onShortcut: () => setFocusTo('later', 'year') },
|
|
||||||
{ shortcut: { key: 'G' }, onShortcut: () => (isShowSelectDate = true) },
|
|
||||||
];
|
|
||||||
|
|
||||||
if (assetInteraction.selectionActive) {
|
|
||||||
shortcuts.push(
|
|
||||||
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
|
|
||||||
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
|
|
||||||
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
|
|
||||||
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
|
|
||||||
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return shortcuts;
|
|
||||||
})(),
|
|
||||||
);
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!lastAssetMouseEvent) {
|
if (!lastAssetMouseEvent) {
|
||||||
assetInteraction.clearAssetSelectionCandidates();
|
assetInteraction.clearAssetSelectionCandidates();
|
||||||
@@ -816,37 +680,17 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
|
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
||||||
|
|
||||||
<HotModuleReload onAfterUpdate={handleAfterUpdate} onBeforeUpdate={handleBeforeUpdate} />
|
<HotModuleReload onAfterUpdate={handleAfterUpdate} onBeforeUpdate={handleBeforeUpdate} />
|
||||||
|
|
||||||
{#if isShowDeleteConfirmation}
|
<TimelineKeyboardActions
|
||||||
<DeleteAssetDialog
|
scrollToAsset={(asset) => scrollToAsset(asset) ?? false}
|
||||||
size={idsSelectedAssets.length}
|
{timelineManager}
|
||||||
onCancel={() => (isShowDeleteConfirmation = false)}
|
{assetInteraction}
|
||||||
onConfirm={() => handlePromiseError(trashOrDelete(true))}
|
bind:isShowDeleteConfirmation
|
||||||
/>
|
{onEscape}
|
||||||
{/if}
|
/>
|
||||||
|
|
||||||
{#if isShowSelectDate}
|
|
||||||
<ChangeDate
|
|
||||||
title="Navigate to Time"
|
|
||||||
initialDate={DateTime.now()}
|
|
||||||
timezoneInput={false}
|
|
||||||
onConfirm={async (dateString: AbsoluteResult | RelativeResult) => {
|
|
||||||
isShowSelectDate = false;
|
|
||||||
if (dateString.mode == 'absolute') {
|
|
||||||
const asset = await timelineManager.getClosestAssetToDate(
|
|
||||||
(DateTime.fromISO(dateString.date) as DateTime<true>).toObject(),
|
|
||||||
);
|
|
||||||
if (asset) {
|
|
||||||
setFocusAsset(asset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onCancel={() => (isShowSelectDate = false)}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if timelineManager.months.length > 0}
|
{#if timelineManager.months.length > 0}
|
||||||
<Scrubber
|
<Scrubber
|
||||||
@@ -854,10 +698,10 @@
|
|||||||
height={timelineManager.viewportHeight}
|
height={timelineManager.viewportHeight}
|
||||||
timelineTopOffset={timelineManager.topSectionHeight}
|
timelineTopOffset={timelineManager.topSectionHeight}
|
||||||
timelineBottomOffset={bottomSectionHeight}
|
timelineBottomOffset={bottomSectionHeight}
|
||||||
{leadout}
|
{isInLeadOutSection}
|
||||||
{scrubOverallPercent}
|
{timelineScrollPercent}
|
||||||
{scrubberMonthPercent}
|
{viewportTopMonthScrollPercent}
|
||||||
{scrubberMonth}
|
{viewportTopMonth}
|
||||||
{onScrub}
|
{onScrub}
|
||||||
bind:scrubberWidth
|
bind:scrubberWidth
|
||||||
onScrubKeyDown={(evt) => {
|
onScrubKeyDown={(evt) => {
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut';
|
||||||
|
import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte';
|
||||||
|
import ChangeDate, {
|
||||||
|
type AbsoluteResult,
|
||||||
|
type RelativeResult,
|
||||||
|
} from '$lib/components/shared-components/change-date.svelte';
|
||||||
|
import {
|
||||||
|
setFocusToAsset as setFocusAssetInit,
|
||||||
|
setFocusTo as setFocusToInit,
|
||||||
|
} from '$lib/components/timeline/actions/focus-actions';
|
||||||
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
|
import { searchStore } from '$lib/stores/search.svelte';
|
||||||
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
|
import { handlePromiseError } from '$lib/utils';
|
||||||
|
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
|
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';
|
||||||
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
|
import { modalManager } from '@immich/ui';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
timelineManager: TimelineManager;
|
||||||
|
assetInteraction: AssetInteraction;
|
||||||
|
isShowDeleteConfirmation: boolean;
|
||||||
|
onEscape?: () => void;
|
||||||
|
scrollToAsset: (asset: TimelineAsset) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
let {
|
||||||
|
timelineManager = $bindable(),
|
||||||
|
assetInteraction,
|
||||||
|
isShowDeleteConfirmation = $bindable(false),
|
||||||
|
onEscape,
|
||||||
|
scrollToAsset,
|
||||||
|
}: Props = $props();
|
||||||
|
|
||||||
|
let isShowSelectDate = $state(false);
|
||||||
|
|
||||||
|
const trashOrDelete = async (force: boolean = false) => {
|
||||||
|
isShowDeleteConfirmation = false;
|
||||||
|
await deleteAssets(
|
||||||
|
!(isTrashEnabled && !force),
|
||||||
|
(assetIds) => timelineManager.removeAssets(assetIds),
|
||||||
|
assetInteraction.selectedAssets,
|
||||||
|
!isTrashEnabled || force ? undefined : (assets) => timelineManager.addAssets(assets),
|
||||||
|
);
|
||||||
|
assetInteraction.clearMultiselect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDelete = () => {
|
||||||
|
const hasTrashedAsset = assetInteraction.selectedAssets.some((asset) => asset.isTrashed);
|
||||||
|
|
||||||
|
if ($showDeleteModal && (!isTrashEnabled || hasTrashedAsset)) {
|
||||||
|
isShowDeleteConfirmation = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePromiseError(trashOrDelete(hasTrashedAsset));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onForceDelete = () => {
|
||||||
|
if ($showDeleteModal) {
|
||||||
|
isShowDeleteConfirmation = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handlePromiseError(trashOrDelete(true));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onStackAssets = async () => {
|
||||||
|
const result = await stackAssets(assetInteraction.selectedAssets);
|
||||||
|
|
||||||
|
updateStackedAssetInTimeline(timelineManager, result);
|
||||||
|
|
||||||
|
onEscape?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleArchive = async () => {
|
||||||
|
const visibility = assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive;
|
||||||
|
const ids = await archiveAssets(assetInteraction.selectedAssets, visibility);
|
||||||
|
timelineManager.updateAssetOperation(ids, (asset) => {
|
||||||
|
asset.visibility = visibility;
|
||||||
|
return { remove: false };
|
||||||
|
});
|
||||||
|
deselectAllAssets();
|
||||||
|
};
|
||||||
|
|
||||||
|
let shiftKeyIsDown = $state(false);
|
||||||
|
|
||||||
|
const deselectAllAssets = () => {
|
||||||
|
cancelMultiselect(assetInteraction);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (searchStore.isSearchEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Shift') {
|
||||||
|
event.preventDefault();
|
||||||
|
shiftKeyIsDown = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onKeyUp = (event: KeyboardEvent) => {
|
||||||
|
if (searchStore.isSearchEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key === 'Shift') {
|
||||||
|
event.preventDefault();
|
||||||
|
shiftKeyIsDown = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelectStart = (e: Event) => {
|
||||||
|
if (assetInteraction.selectionActive && shiftKeyIsDown) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
|
||||||
|
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||||
|
const idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id));
|
||||||
|
let isShortcutModalOpen = false;
|
||||||
|
|
||||||
|
const handleOpenShortcutModal = async () => {
|
||||||
|
if (isShortcutModalOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isShortcutModalOpen = true;
|
||||||
|
await modalManager.show(ShortcutsModal, {});
|
||||||
|
isShortcutModalOpen = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (isEmpty) {
|
||||||
|
assetInteraction.clearMultiselect();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const setFocusTo = setFocusToInit.bind(undefined, scrollToAsset, timelineManager);
|
||||||
|
const setFocusAsset = setFocusAssetInit.bind(undefined, scrollToAsset);
|
||||||
|
|
||||||
|
let shortcutList = $derived(
|
||||||
|
(() => {
|
||||||
|
if (searchStore.isSearchEnabled || $showAssetViewer) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcuts: ShortcutOptions[] = [
|
||||||
|
{ shortcut: { key: '?', shift: true }, onShortcut: handleOpenShortcutModal },
|
||||||
|
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
|
||||||
|
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(timelineManager, assetInteraction) },
|
||||||
|
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => setFocusTo('earlier', 'asset') },
|
||||||
|
{ shortcut: { key: 'ArrowLeft' }, onShortcut: () => setFocusTo('later', 'asset') },
|
||||||
|
{ shortcut: { key: 'D' }, onShortcut: () => setFocusTo('earlier', 'day') },
|
||||||
|
{ shortcut: { key: 'D', shift: true }, onShortcut: () => setFocusTo('later', 'day') },
|
||||||
|
{ shortcut: { key: 'M' }, onShortcut: () => setFocusTo('earlier', 'month') },
|
||||||
|
{ shortcut: { key: 'M', shift: true }, onShortcut: () => setFocusTo('later', 'month') },
|
||||||
|
{ shortcut: { key: 'Y' }, onShortcut: () => setFocusTo('earlier', 'year') },
|
||||||
|
{ shortcut: { key: 'Y', shift: true }, onShortcut: () => setFocusTo('later', 'year') },
|
||||||
|
{ shortcut: { key: 'G' }, onShortcut: () => (isShowSelectDate = true) },
|
||||||
|
];
|
||||||
|
if (onEscape) {
|
||||||
|
shortcuts.push({ shortcut: { key: 'Escape' }, onShortcut: onEscape });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assetInteraction.selectionActive) {
|
||||||
|
shortcuts.push(
|
||||||
|
{ shortcut: { key: 'Delete' }, onShortcut: onDelete },
|
||||||
|
{ shortcut: { key: 'Delete', shift: true }, onShortcut: onForceDelete },
|
||||||
|
{ shortcut: { key: 'D', ctrl: true }, onShortcut: () => deselectAllAssets() },
|
||||||
|
{ shortcut: { key: 's' }, onShortcut: () => onStackAssets() },
|
||||||
|
{ shortcut: { key: 'a', shift: true }, onShortcut: toggleArchive },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return shortcuts;
|
||||||
|
})(),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} onselectstart={onSelectStart} use:shortcuts={shortcutList} />
|
||||||
|
|
||||||
|
{#if isShowDeleteConfirmation}
|
||||||
|
<DeleteAssetDialog
|
||||||
|
size={idsSelectedAssets.length}
|
||||||
|
onCancel={() => (isShowDeleteConfirmation = false)}
|
||||||
|
onConfirm={() => handlePromiseError(trashOrDelete(true))}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if isShowSelectDate}
|
||||||
|
<ChangeDate
|
||||||
|
withDuration={false}
|
||||||
|
title="Navigate to Time"
|
||||||
|
initialDate={DateTime.now()}
|
||||||
|
timezoneInput={false}
|
||||||
|
onConfirm={async (dateString: AbsoluteResult | RelativeResult) => {
|
||||||
|
isShowSelectDate = false;
|
||||||
|
if (dateString.mode == 'absolute') {
|
||||||
|
const asset = await timelineManager.getClosestAssetToDate(
|
||||||
|
(DateTime.fromISO(dateString.date) as DateTime<true>).toObject(),
|
||||||
|
);
|
||||||
|
if (asset) {
|
||||||
|
setFocusAsset(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onCancel={() => (isShowSelectDate = false)}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
@@ -23,11 +23,11 @@ export type TimelineDateTime = TimelineDate & {
|
|||||||
millisecond: number;
|
millisecond: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScrubberListener = (
|
export type ScrubberListener = (scrubberData: {
|
||||||
scrubberMonth: { year: number; month: number },
|
scrubberMonth: { year: number; month: number };
|
||||||
overallScrollPercent: number,
|
overallScrollPercent: number;
|
||||||
scrubberMonthScrollPercent: number,
|
scrubberMonthScrollPercent: number;
|
||||||
) => void | Promise<void>;
|
}) => void | Promise<void>;
|
||||||
|
|
||||||
// used for AssetResponseDto.dateTimeOriginal, amongst others
|
// used for AssetResponseDto.dateTimeOriginal, amongst others
|
||||||
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
||||||
|
|||||||
Reference in New Issue
Block a user