mirror of
https://github.com/immich-app/immich.git
synced 2026-07-16 05:34:30 +03:00
Compare commits
3 Commits
v3.0.3
...
fix/slides
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f29f884f0b | ||
|
|
52562c4972 | ||
|
|
fc65603699 |
4
.github/workflows/build-mobile.yml
vendored
4
.github/workflows/build-mobile.yml
vendored
@@ -103,7 +103,7 @@ jobs:
|
||||
working-directory: ./mobile
|
||||
run: printf "%s" $KEY_JKS | base64 -d > android/key.jks
|
||||
|
||||
- uses: actions/setup-java@0f481fcb613427c0f801b606911222b5b6f3083a # v5.5.0
|
||||
- uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
@@ -237,7 +237,7 @@ jobs:
|
||||
run: flutter build ios --config-only --no-codesign
|
||||
|
||||
- name: Setup Ruby
|
||||
uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
|
||||
uses: ruby/setup-ruby@0dafeac902942906541bc140009cdbf32665b601 # v1.315.0
|
||||
with:
|
||||
ruby-version: '3.3'
|
||||
bundler-cache: true
|
||||
|
||||
2
.github/workflows/check-openapi.yml
vendored
2
.github/workflows/check-openapi.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for breaking API changes
|
||||
uses: oasdiff/oasdiff-action/breaking@291b6594ce617f22f49af5898ee038ed0b46e8fe # v0.1.5
|
||||
uses: oasdiff/oasdiff-action/breaking@ae5ef6402ebe218ac6e872c59d0fa4557694e75c # v0.1.4
|
||||
with:
|
||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||
revision: open-api/immich-openapi-specs.json
|
||||
|
||||
2
.github/workflows/cli.yml
vendored
2
.github/workflows/cli.yml
vendored
@@ -81,7 +81,7 @@ jobs:
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
|
||||
2
.github/workflows/close-duplicates.yml
vendored
2
.github/workflows/close-duplicates.yml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
needs: [get_body, should_run]
|
||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||
container:
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:e3fe456f346b02c2f479462100763aee3ba8245768dfeac3c4262acb068e5c62
|
||||
image: ghcr.io/immich-app/mdq:main@sha256:2a64c7f045cb7b580fbdf3614d7d1805f5775fec453e3d1023764180efa8c70b
|
||||
outputs:
|
||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||
steps:
|
||||
|
||||
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
@@ -57,7 +57,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
uses: github/codeql-action/autobuild@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -83,6 +83,6 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
with:
|
||||
category: '/language:${{matrix.language}}'
|
||||
|
||||
4
.github/workflows/docker.yml
vendored
4
.github/workflows/docker.yml
vendored
@@ -60,7 +60,7 @@ jobs:
|
||||
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
@@ -90,7 +90,7 @@ jobs:
|
||||
suffix: ['']
|
||||
steps:
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4.4.0
|
||||
uses: docker/login-action@c99871dec2022cc055c062a10cc1a1310835ceb4 # v4.3.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
|
||||
56
.github/workflows/fdroid.yml
vendored
56
.github/workflows/fdroid.yml
vendored
@@ -1,56 +0,0 @@
|
||||
name: Update F-Droid Repo
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
permissions: {}
|
||||
|
||||
jobs:
|
||||
update-index:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ !github.event.release.prerelease }}
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout pubspec for versionCode
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name }}
|
||||
persist-credentials: false
|
||||
sparse-checkout: mobile/pubspec.yaml
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Update F-Droid repo
|
||||
env:
|
||||
GITLAB_TOKEN: ${{ secrets.FDROID_REPO_TOKEN }}
|
||||
RELEASE: ${{ toJSON(github.event.release) }}
|
||||
TAG: ${{ github.event.release.tag_name }}
|
||||
run: |
|
||||
VERSION_CODE=$(yq '.version' mobile/pubspec.yaml | cut -d+ -f2)
|
||||
export VERSION_CODE
|
||||
|
||||
NEW_ENTRY=$(yq -P '
|
||||
[.assets[] | select(.name == "app-release.apk")] as $matches |
|
||||
with(select($matches | length == 0); error("app-release.apk not found in release assets")) |
|
||||
{
|
||||
"url": $matches[0].browser_download_url,
|
||||
"sha256sum": ($matches[0].digest | sub("^sha256:", "")),
|
||||
"date": (.published_at | split("T") | .[0]),
|
||||
"version-code": env(VERSION_CODE)
|
||||
} | .date tag= "!!timestamp"
|
||||
' <<< "$RELEASE")
|
||||
export NEW_ENTRY
|
||||
|
||||
git clone --depth 1 "https://oauth2:${GITLAB_TOKEN}@gitlab.futo.org/fdroid/repo-v2.git" fdroid-repo
|
||||
cd fdroid-repo
|
||||
|
||||
yq -i '
|
||||
with(select([.apks[] | select(."version-code" == env(VERSION_CODE))] | length > 0); error("version-code already in index")) |
|
||||
.apks = [env(NEW_ENTRY)] + .apks
|
||||
' apps/Immich/index.yml
|
||||
|
||||
git config user.name "Immich Release Bot"
|
||||
git config user.email "bot@immich.app"
|
||||
git commit -am "Immich: add version-code ${VERSION_CODE} (${TAG})"
|
||||
git push
|
||||
2
.github/workflows/pr-label-validation.yml
vendored
2
.github/workflows/pr-label-validation.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Require PR to have a changelog label
|
||||
uses: mheap/github-action-required-labels@23e10fde7e062233401931a0eece796cd9bf3177 # v5.6.0
|
||||
uses: mheap/github-action-required-labels@0ac283b4e65c1fb28ce6079dea5546ceca98ccbe # v5.5.2
|
||||
with:
|
||||
token: ${{ steps.token.outputs.token }}
|
||||
mode: exactly
|
||||
|
||||
2
.github/workflows/pr-labeler.yml
vendored
2
.github/workflows/pr-labeler.yml
vendored
@@ -17,6 +17,6 @@ jobs:
|
||||
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- uses: actions/labeler@b8dd2d9be0f68b860e7dae5dae7d772984eacd6d # v6.2.0
|
||||
- uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
|
||||
with:
|
||||
repo-token: ${{ steps.token.outputs.token }}
|
||||
|
||||
2
.github/workflows/prepare-release.yml
vendored
2
.github/workflows/prepare-release.yml
vendored
@@ -73,7 +73,7 @@ jobs:
|
||||
|
||||
# TODO move to mise
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
|
||||
|
||||
- name: Bump version
|
||||
env:
|
||||
|
||||
@@ -1,30 +1,5 @@
|
||||
# System Integrity
|
||||
|
||||
## Integrity report
|
||||
|
||||
At a [customizable interval](https://my.immich.app/admin/system-settings?isOpen=integrity-checks), Immich runs integrity checks to ensure that your library is still intact and there are no corrupt files.
|
||||
There are three kind of issues Immich checks for:
|
||||
|
||||
- Untracked files: the path was found in Immich's directories but it is not referenced in Immich's database
|
||||
- Missing files: the path is found in the Immich internal database, but does not actually exist on disk
|
||||
- Checksum mismatches: the checksum of the file stored in Immich's database does not match the actual file's checksum anymore
|
||||
|
||||
All three run nightly at 3am by default. For the "Checksum files" check, there are additional time and progress limits, as those are the most taxing checks. With these additional limits, Immich can slowly check checksums of your files over the course of a couple of days.
|
||||
|
||||
You can see the results of these checks on the [maintenance](https://my.immich.app/admin/maintenance) page. Here, you can also trigger a full scan (a _check_) for specific jobs, or all of them. In addition, you can also _refresh_ checks. This will only look at items that have currently been reported on, and check if those have been fixed.
|
||||
|
||||
### Common causes
|
||||
|
||||
Most common are untracked files. In many cases those are corrupted thumbnails or encoded videos that have been partially generated at some point and never got cleaned up properly. These are usually fine to delete, as both can always be regenerated at a later point. Other files will need to be investigated on a case-by-case basis by checking they already exist in Immich and thinking about how they might have gotten untracked.
|
||||
|
||||
:::info
|
||||
You might want to run the _missing_ jobs for thumbnails and encoded videos (https://my.immich.app/admin/queues) to make sure all your assets have proper thumbnails and encoded videos. Watch the server logs while running the jobs, in case there are any issues with some assets.
|
||||
:::
|
||||
|
||||
Missing files are files where Immich references them internally, but they don't actually exist on disk in that location. It could be that you deleted a file on disk from the internal library (don't do that, Immich doesn't support it). It could also be that there are issues with your file storage. Please carefully investigate missing files, and never hesitate to reach out on [our Discord](https://discord.immich.app) if you have any questions!
|
||||
|
||||
Checksum mismatches are often indicative of file system corruption. It could also be that you previously edited a file from the internal library on the disk, which is also not supported and will cause a checksum mismatch. Again, the recommended action is to look at any reported item individually, check it out, try to remember if you changed it or some metadata on it at some point. If you edited the file, the supported resolution is to delete the mismatched asset from Immich and reupload it as a new asset.
|
||||
|
||||
## Folder checks
|
||||
|
||||
:::info
|
||||
@@ -65,7 +40,7 @@ The above error messages show that the server has previously (successfully) writ
|
||||
|
||||
### Ignoring the checks
|
||||
|
||||
:::danger
|
||||
:::warning
|
||||
The checks are designed to catch common problems that we have seen users have in the past, and often indicate there's something wrong that you should solve. If you know what you're doing and you want to disable them you can set the following environment variable:
|
||||
:::
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
The mobile app can be downloaded from the following places:
|
||||
|
||||
- [Apple App Store](https://apps.apple.com/us/app/immich/id1613945652)
|
||||
- [Google Play Store](https://play.google.com/store/apps/details?id=app.alextran.immich)
|
||||
- [GitHub Releases (APK)](https://github.com/immich-app/immich/releases)
|
||||
- Obtainium: You can get your Obtainium config link from the [Utilities page of your Immich server](https://my.immich.app/utilities).
|
||||
- [F-Droid](https://app.futo.org/fdroid/repo/)
|
||||
- [Google Play Store](https://play.google.com/store/apps/details?id=app.alextran.immich)
|
||||
- [Apple App Store](https://apps.apple.com/us/app/immich/id1613945652)
|
||||
- [F-Droid](https://f-droid.org/packages/app.alextran.immich)
|
||||
- [GitHub Releases (apk)](https://github.com/immich-app/immich/releases)
|
||||
|
||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"label": "v3.0.3",
|
||||
"url": "https://docs.v3.0.3.archive.immich.app"
|
||||
"label": "v3.0.2",
|
||||
"url": "https://docs.v3.0.2.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v2.7.5",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
|
||||
@@ -796,22 +796,5 @@ describe('/albums', () => {
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('Not found or no album.share access'));
|
||||
});
|
||||
|
||||
it('should not allow an editor to change the role of an owner', async () => {
|
||||
const album = await utils.createAlbum(user1.accessToken, {
|
||||
albumName: 'testAlbum',
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }],
|
||||
});
|
||||
|
||||
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Editor);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/albums/${album.id}/user/${user1.userId}`)
|
||||
.set('Authorization', `Bearer ${user2.accessToken}`)
|
||||
.send({ role: AlbumUserRole.Editor });
|
||||
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(errorDto.badRequest('User is owner'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1621,7 +1621,7 @@
|
||||
"person": "Person",
|
||||
"person_age_months": "{months, plural, one {# month} other {# months}} old",
|
||||
"person_age_year_months": "1 year, {months, plural, one {# month} other {# months}} old",
|
||||
"person_age_years": "{years, plural, one {# year} other {# years}} old",
|
||||
"person_age_years": "{years, plural, other {# years}} old",
|
||||
"person_birthdate": "Born on {date}",
|
||||
"person_hidden": "{name}{hidden, select, true { (hidden)} other {}}",
|
||||
"person_recognized": "Person recognized",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "immich-ml"
|
||||
version = "3.0.3"
|
||||
version = "3.0.2"
|
||||
description = ""
|
||||
authors = [{ name = "Hau Tran", email = "alex.tran1502@gmail.com" }]
|
||||
requires-python = ">=3.11,<4.0"
|
||||
|
||||
2
machine-learning/uv.lock
generated
2
machine-learning/uv.lock
generated
@@ -974,7 +974,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "immich-ml"
|
||||
version = "3.0.3"
|
||||
version = "3.0.2"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "aiocache" },
|
||||
|
||||
@@ -22,8 +22,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3056,
|
||||
"android.injected.version.name" => "3.0.3",
|
||||
"android.injected.version.code" => 3055,
|
||||
"android.injected.version.name" => "3.0.2",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab', track: 'beta')
|
||||
@@ -35,8 +35,8 @@ platform :android do
|
||||
task: 'bundle',
|
||||
build_type: 'Release',
|
||||
properties: {
|
||||
"android.injected.version.code" => 3056,
|
||||
"android.injected.version.name" => "3.0.3",
|
||||
"android.injected.version.code" => 3055,
|
||||
"android.injected.version.name" => "3.0.2",
|
||||
}
|
||||
)
|
||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.0.3</string>
|
||||
<string>3.0.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
@@ -26,6 +26,7 @@ const String kDownloadGroupLivePhoto = 'group_livephoto';
|
||||
const String kShareDownloadGroup = 'group_share';
|
||||
|
||||
// Timeline constants
|
||||
const int kTimelineNoneSegmentSize = 120;
|
||||
const int kTimelineAssetLoadBatchSize = 1024;
|
||||
const int kTimelineAssetLoadOppositeSize = 64;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:immich_mobile/constants/constants.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/timeline.model.dart';
|
||||
@@ -684,7 +685,16 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
List<Bucket> _generateBuckets(int count) => count == 0 ? const [] : [Bucket(assetCount: count)];
|
||||
List<Bucket> _generateBuckets(int count) {
|
||||
final buckets = List.filled(
|
||||
(count / kTimelineNoneSegmentSize).ceil(),
|
||||
const Bucket(assetCount: kTimelineNoneSegmentSize),
|
||||
);
|
||||
if (count % kTimelineNoneSegmentSize != 0) {
|
||||
buckets[buckets.length - 1] = Bucket(assetCount: count % kTimelineNoneSegmentSize);
|
||||
}
|
||||
return buckets;
|
||||
}
|
||||
|
||||
extension on Expression<DateTime> {
|
||||
Expression<String> dateFmt(GroupAssetsBy groupBy, {bool toLocal = false}) {
|
||||
|
||||
@@ -51,6 +51,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
||||
int? _crossfadeFromIndex;
|
||||
int? _crossfadeToIndex;
|
||||
int _zoomCycle = 0;
|
||||
bool _disableAnimations = false;
|
||||
|
||||
@override
|
||||
initState() {
|
||||
@@ -70,6 +71,12 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
||||
unawaited(WakelockPlus.enable());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_disableAnimations = MediaQuery.disableAnimationsOf(context);
|
||||
}
|
||||
|
||||
@override
|
||||
dispose() {
|
||||
_timer.cancel();
|
||||
@@ -166,6 +173,11 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
||||
}
|
||||
|
||||
void _crossFadeToPage(int page) {
|
||||
if (_disableAnimations) {
|
||||
_pageController.jumpToPage(page);
|
||||
return;
|
||||
}
|
||||
|
||||
final previousIndex = _index;
|
||||
_pageController.jumpToPage(page);
|
||||
setState(() {
|
||||
@@ -273,19 +285,12 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
||||
}
|
||||
|
||||
if (asset.isImage) {
|
||||
final elapsed = _stopwatch.elapsedMilliseconds;
|
||||
final duration = _config.duration * 1000;
|
||||
|
||||
return TweenAnimationBuilder(
|
||||
return _SlideshowProgressBar(
|
||||
key: Key(_index.toString()),
|
||||
tween: Tween<double>(begin: elapsed / duration.toDouble(), end: _paused ? elapsed / duration.toDouble() : 1.0),
|
||||
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
||||
builder: (context, value, _) => LinearProgressIndicator(
|
||||
color: context.colorScheme.primary,
|
||||
borderRadius: const BorderRadius.all(Radius.zero),
|
||||
minHeight: 5,
|
||||
value: value,
|
||||
),
|
||||
durationMs: _config.duration * 1000,
|
||||
elapsedMs: _stopwatch.elapsedMilliseconds,
|
||||
paused: _paused,
|
||||
color: context.colorScheme.primary,
|
||||
);
|
||||
} else {
|
||||
return LinearProgressIndicator(
|
||||
@@ -334,6 +339,21 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
||||
final imageProvider = getFullImageProvider(asset, size: context.sizeData);
|
||||
|
||||
if (asset.isImage) {
|
||||
PhotoView buildPhotoView(PhotoViewComputedScale initialScale) => PhotoView(
|
||||
imageProvider: imageProvider,
|
||||
index: index,
|
||||
disableScaleGestures: true,
|
||||
gaplessPlayback: true,
|
||||
filterQuality: FilterQuality.high,
|
||||
initialScale: initialScale,
|
||||
controller: PhotoViewController(),
|
||||
onTapUp: (_, _, _) => _onTapUp(),
|
||||
);
|
||||
|
||||
if (_disableAnimations) {
|
||||
return buildPhotoView(scale);
|
||||
}
|
||||
|
||||
final zoomOut = _zoomCycle.isOdd;
|
||||
final elapsed = _stopwatch.elapsedMilliseconds;
|
||||
final duration = _config.duration * 1000;
|
||||
@@ -349,16 +369,7 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
||||
: 1.0,
|
||||
),
|
||||
duration: Duration(milliseconds: _paused ? 1 : max(duration - elapsed, 1)),
|
||||
builder: (context, value, _) => PhotoView(
|
||||
imageProvider: imageProvider,
|
||||
index: index,
|
||||
disableScaleGestures: true,
|
||||
gaplessPlayback: true,
|
||||
filterQuality: FilterQuality.high,
|
||||
initialScale: scale * (1.0 + value * _kenBurnsZoom),
|
||||
controller: PhotoViewController(),
|
||||
onTapUp: (_, _, _) => _onTapUp(),
|
||||
),
|
||||
builder: (context, value, _) => buildPhotoView(scale * (1.0 + value * _kenBurnsZoom)),
|
||||
);
|
||||
} else {
|
||||
final status = ref.watch(videoPlayerProvider(asset.heroTag).select((s) => s.status));
|
||||
@@ -463,3 +474,75 @@ class _DriftSlideshowPageState extends ConsumerState<DriftSlideshowPage> with Si
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Progress bar for image slides, driven by an explicit [AnimationController].
|
||||
///
|
||||
/// [TweenAnimationBuilder] creates its controller internally with the default
|
||||
/// [AnimationBehavior.normal], which makes it run ~20x too fast while the system
|
||||
/// "reduce motion" setting is on (flutter/flutter#164287). This owns its
|
||||
/// controller so it can use [AnimationBehavior.preserve] and animate at the real
|
||||
/// slide duration regardless of that setting.
|
||||
class _SlideshowProgressBar extends StatefulWidget {
|
||||
final int durationMs;
|
||||
final int elapsedMs;
|
||||
final bool paused;
|
||||
final Color color;
|
||||
|
||||
const _SlideshowProgressBar({
|
||||
super.key,
|
||||
required this.durationMs,
|
||||
required this.elapsedMs,
|
||||
required this.paused,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_SlideshowProgressBar> createState() => _SlideshowProgressBarState();
|
||||
}
|
||||
|
||||
class _SlideshowProgressBarState extends State<_SlideshowProgressBar> with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = AnimationController(
|
||||
vsync: this,
|
||||
duration: Duration(milliseconds: widget.durationMs),
|
||||
animationBehavior: AnimationBehavior.preserve,
|
||||
)..value = (widget.elapsedMs / widget.durationMs).clamp(0.0, 1.0);
|
||||
if (!widget.paused) {
|
||||
_controller.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_SlideshowProgressBar oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.durationMs != oldWidget.durationMs) {
|
||||
_controller.duration = Duration(milliseconds: widget.durationMs);
|
||||
}
|
||||
if (widget.paused != oldWidget.paused) {
|
||||
widget.paused ? _controller.stop() : _controller.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedBuilder(
|
||||
animation: _controller,
|
||||
builder: (context, _) => LinearProgressIndicator(
|
||||
color: widget.color,
|
||||
borderRadius: const BorderRadius.all(Radius.zero),
|
||||
minHeight: 5,
|
||||
value: _controller.value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +99,6 @@ class _AssetPageState extends ConsumerState<AssetPage> {
|
||||
case TimelineReloadEvent():
|
||||
final asset = ref.read(timelineServiceProvider).getAssetSafe(widget.index);
|
||||
if (asset != _asset) {
|
||||
_isZoomed = false;
|
||||
_viewer.setZoomed(false);
|
||||
setState(() => _asset = asset);
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -81,12 +81,6 @@ class AssetViewer extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
static const _viewerOverlayStyle = SystemUiOverlayStyle(
|
||||
statusBarIconBrightness: Brightness.light,
|
||||
statusBarBrightness: Brightness.dark,
|
||||
systemNavigationBarIconBrightness: Brightness.light,
|
||||
);
|
||||
|
||||
late final _heroOffset = widget.heroOffset ?? TabsRouterScope.of(context)?.controller.activeIndex ?? 0;
|
||||
late final _pageController = PageController(initialPage: widget.initialIndex);
|
||||
late final _preloader = AssetPreloader(timelineService: ref.read(timelineServiceProvider), mounted: () => mounted);
|
||||
@@ -286,52 +280,49 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||
_setSystemUIMode(controls, details);
|
||||
});
|
||||
|
||||
return AnnotatedRegion(
|
||||
value: _viewerOverlayStyle,
|
||||
child: Scaffold(
|
||||
backgroundColor: backgroundColor,
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: const ViewerTopAppBar(),
|
||||
extendBody: true,
|
||||
extendBodyBehindAppBar: true,
|
||||
floatingActionButton: IgnorePointer(
|
||||
ignoring: !showingControls,
|
||||
child: AnimatedOpacity(
|
||||
opacity: showingControls ? 1.0 : 0.0,
|
||||
duration: Durations.short2,
|
||||
child: const DownloadStatusFloatingButton(),
|
||||
),
|
||||
return Scaffold(
|
||||
backgroundColor: backgroundColor,
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: const ViewerTopAppBar(),
|
||||
extendBody: true,
|
||||
extendBodyBehindAppBar: true,
|
||||
floatingActionButton: IgnorePointer(
|
||||
ignoring: !showingControls,
|
||||
child: AnimatedOpacity(
|
||||
opacity: showingControls ? 1.0 : 0.0,
|
||||
duration: Durations.short2,
|
||||
child: const DownloadStatusFloatingButton(),
|
||||
),
|
||||
bottomNavigationBar: const ViewerBottomAppBar(),
|
||||
body: Stack(
|
||||
children: [
|
||||
NotificationListener<ScrollEndNotification>(
|
||||
onNotification: _onScrollEnd,
|
||||
child: PhotoViewGestureDetectorScope(
|
||||
axis: Axis.horizontal,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
physics: isZoomed
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: CurrentPlatform.isIOS
|
||||
? const FastScrollPhysics()
|
||||
: const FastClampingScrollPhysics(),
|
||||
itemCount: _totalAssets,
|
||||
itemBuilder: (context, index) =>
|
||||
AssetPage(index: index, heroOffset: _heroOffset, onTapNavigate: _onTapNavigate),
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: const ViewerBottomAppBar(),
|
||||
body: Stack(
|
||||
children: [
|
||||
NotificationListener<ScrollEndNotification>(
|
||||
onNotification: _onScrollEnd,
|
||||
child: PhotoViewGestureDetectorScope(
|
||||
axis: Axis.horizontal,
|
||||
child: PageView.builder(
|
||||
controller: _pageController,
|
||||
physics: isZoomed
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: CurrentPlatform.isIOS
|
||||
? const FastScrollPhysics()
|
||||
: const FastClampingScrollPhysics(),
|
||||
itemCount: _totalAssets,
|
||||
itemBuilder: (context, index) =>
|
||||
AssetPage(index: index, heroOffset: _heroOffset, onTapNavigate: _onTapNavigate),
|
||||
),
|
||||
),
|
||||
if (!CurrentPlatform.isIOS)
|
||||
IgnorePointer(
|
||||
child: AnimatedContainer(
|
||||
duration: Durations.short2,
|
||||
color: Colors.black.withValues(alpha: showingDetails ? 0.6 : 0.0),
|
||||
height: context.padding.top,
|
||||
),
|
||||
),
|
||||
if (!CurrentPlatform.isIOS)
|
||||
IgnorePointer(
|
||||
child: AnimatedContainer(
|
||||
duration: Durations.short2,
|
||||
color: Colors.black.withValues(alpha: showingDetails ? 0.6 : 0.0),
|
||||
height: context.padding.top,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -317,8 +317,6 @@ class BackgroundUploadService {
|
||||
priority: priority,
|
||||
isFavorite: asset.isFavorite,
|
||||
requiresWiFi: requiresWiFi,
|
||||
// Visibility hidden on upload to prevent the server from running regular jobs on the live photo asset
|
||||
fields: entity.isLivePhoto ? {'visibility': api.AssetVisibility.hidden.value} : null,
|
||||
cloudId: entity.isLivePhoto ? null : asset.cloudId,
|
||||
adjustmentTime: entity.isLivePhoto ? null : asset.adjustmentTime?.toIso8601String(),
|
||||
latitude: entity.isLivePhoto ? null : asset.latitude?.toString(),
|
||||
@@ -338,7 +336,8 @@ class BackgroundUploadService {
|
||||
return null;
|
||||
}
|
||||
|
||||
final fields = {'livePhotoVideoId': livePhotoVideoId};
|
||||
// Visibility hidden on upload to prevent the server from running regular jobs on the live photo asset
|
||||
final fields = {'livePhotoVideoId': livePhotoVideoId, 'visibility': api.AssetVisibility.hidden.value};
|
||||
|
||||
final requiresWiFi = _shouldRequireWiFi(asset);
|
||||
final originalFileName = await _assetMediaRepository.getOriginalFilename(asset.id) ?? asset.name;
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/asset_metadata.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart' hide AssetVisibility;
|
||||
@@ -101,7 +100,7 @@ class ForegroundUploadService {
|
||||
final requireWifi = _shouldRequireWiFi(asset);
|
||||
return requireWifi && !hasWifi;
|
||||
},
|
||||
processItem: (asset) => uploadSingleAsset(asset, cancelToken, callbacks: callbacks),
|
||||
processItem: (asset) => _uploadSingleAsset(asset, cancelToken, callbacks: callbacks),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -127,7 +126,7 @@ class ForegroundUploadService {
|
||||
continue;
|
||||
}
|
||||
|
||||
await uploadSingleAsset(asset, cancelToken, callbacks: callbacks);
|
||||
await _uploadSingleAsset(asset, cancelToken, callbacks: callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +143,7 @@ class ForegroundUploadService {
|
||||
await _executeWithWorkerPool<LocalAsset>(
|
||||
items: localAssets,
|
||||
cancelToken: cancelToken,
|
||||
processItem: (asset) => uploadSingleAsset(asset, cancelToken, callbacks: callbacks),
|
||||
processItem: (asset) => _uploadSingleAsset(asset, cancelToken, callbacks: callbacks),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -234,8 +233,7 @@ class ForegroundUploadService {
|
||||
await Future.wait(workerFutures);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<void> uploadSingleAsset(
|
||||
Future<void> _uploadSingleAsset(
|
||||
LocalAsset asset,
|
||||
Completer<void>? cancelToken, {
|
||||
required UploadCallbacks callbacks,
|
||||
|
||||
@@ -393,8 +393,10 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _zoomAnimation.value,
|
||||
filterQuality: Platform.isAndroid ? FilterQuality.low : null,
|
||||
child: Transform.translate(
|
||||
offset: _panAnimation.value,
|
||||
filterQuality: Platform.isAndroid ? FilterQuality.low : null,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
|
||||
@@ -496,8 +496,10 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _zoomAnimation.value,
|
||||
filterQuality: Platform.isAndroid ? FilterQuality.low : null,
|
||||
child: Transform.translate(
|
||||
offset: _panAnimation.value,
|
||||
filterQuality: Platform.isAndroid ? FilterQuality.low : null,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
|
||||
@@ -472,8 +472,10 @@ class _RandomAssetBackgroundState extends State<_RandomAssetBackground> with Tic
|
||||
builder: (context, child) {
|
||||
return Transform.scale(
|
||||
scale: _zoomAnimation.value,
|
||||
filterQuality: Platform.isAndroid ? FilterQuality.low : null,
|
||||
child: Transform.translate(
|
||||
offset: _panAnimation.value,
|
||||
filterQuality: Platform.isAndroid ? FilterQuality.low : null,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
|
||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
||||
|
||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 3.0.3
|
||||
- API version: 3.0.2
|
||||
- Generator version: 7.22.0
|
||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
||||
description: Immich - selfhosted backup media file on mobile phone
|
||||
|
||||
publish_to: 'none'
|
||||
version: 3.0.3+3056
|
||||
version: 3.0.2+3055
|
||||
|
||||
environment:
|
||||
sdk: '>=3.12.0 <4.0.0'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:immich_mobile/platform/connectivity_api.g.dart';
|
||||
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
@@ -8,5 +7,3 @@ class MockSyncApi extends Mock implements SyncApi {}
|
||||
class MockServerApi extends Mock implements ServerApi {}
|
||||
|
||||
class MockPartnerApiRepository extends Mock implements PartnerApiRepository {}
|
||||
|
||||
class MockConnectivityApi extends Mock implements ConnectivityApi {}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_page.widget.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
|
||||
import '../../../fixtures/asset.stub.dart';
|
||||
|
||||
/// Timeline displaying a single asset that we can swap
|
||||
class _SwappableTimelineService extends TimelineService {
|
||||
BaseAsset asset;
|
||||
_SwappableTimelineService(this.asset)
|
||||
: super((
|
||||
assetSource: (_, __) async => [],
|
||||
bucketSource: () => Stream.value(const [Bucket(assetCount: 1)]),
|
||||
origin: TimelineOrigin.main,
|
||||
));
|
||||
|
||||
@override
|
||||
int get totalAssets => 1;
|
||||
@override
|
||||
BaseAsset? getAssetSafe(int index) => index == 0 ? asset : null;
|
||||
}
|
||||
|
||||
class _ZoomedNotifier extends AssetViewerStateNotifier {
|
||||
@override
|
||||
AssetViewerState build() {
|
||||
super.build();
|
||||
return AssetViewerState(currentAsset: LocalAssetStub.image1, isZoomed: true);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('resets zoom when the displayed asset is replaced on reload', (tester) async {
|
||||
final timeline = _SwappableTimelineService(LocalAssetStub.image1);
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
timelineServiceProvider.overrideWithValue(timeline),
|
||||
assetViewerProvider.overrideWith(_ZoomedNotifier.new),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: EasyLocalization(
|
||||
supportedLocales: locales.values.toList(),
|
||||
path: translationsPath,
|
||||
startLocale: locales.values.first,
|
||||
fallbackLocale: locales.values.first,
|
||||
saveLocale: false,
|
||||
useFallbackTranslations: true,
|
||||
assetLoader: const CodegenLoader(),
|
||||
child: Builder(
|
||||
builder: (context) => MaterialApp(
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
home: const Material(child: AssetPage(index: 0, heroOffset: 0)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
// Ignore any image load errors
|
||||
tester.takeException();
|
||||
|
||||
expect(container.read(assetViewerProvider).isZoomed, isTrue);
|
||||
|
||||
// Swap the asset, simulating a delete
|
||||
timeline.asset = LocalAssetStub.image2;
|
||||
EventStream.shared.emit(const TimelineReloadEvent());
|
||||
|
||||
await tester.idle();
|
||||
await tester.pump();
|
||||
tester.takeException();
|
||||
|
||||
expect(container.read(assetViewerProvider).isZoomed, isFalse);
|
||||
});
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||
import 'package:immich_mobile/generated/codegen_loader.g.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/asset_viewer.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||
|
||||
import '../../../fixtures/asset.stub.dart';
|
||||
|
||||
class _SeededAssetViewerNotifier extends AssetViewerStateNotifier {
|
||||
@override
|
||||
AssetViewerState build() {
|
||||
super.build();
|
||||
return AssetViewerState(currentAsset: LocalAssetStub.image1);
|
||||
}
|
||||
}
|
||||
|
||||
TimelineService _stubTimelineService() {
|
||||
return TimelineService((
|
||||
assetSource: (index, count) async => [LocalAssetStub.image1],
|
||||
bucketSource: () => Stream.value(const [Bucket(assetCount: 1)]),
|
||||
origin: TimelineOrigin.main,
|
||||
));
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('status bar icons are light while the asset viewer is open in light mode', (tester) async {
|
||||
// Emulate arriving from a light-themed page whose AppBar set dark status
|
||||
// bar icons (the state the viewer is opened from in light mode).
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle.dark);
|
||||
|
||||
await tester.pumpWidget(
|
||||
EasyLocalization(
|
||||
supportedLocales: locales.values.toList(),
|
||||
path: translationsPath,
|
||||
startLocale: locales.values.first,
|
||||
fallbackLocale: locales.values.first,
|
||||
saveLocale: false,
|
||||
useFallbackTranslations: true,
|
||||
assetLoader: const CodegenLoader(),
|
||||
child: ProviderScope(
|
||||
overrides: [
|
||||
timelineServiceProvider.overrideWithValue(_stubTimelineService()),
|
||||
assetViewerProvider.overrideWith(_SeededAssetViewerNotifier.new),
|
||||
],
|
||||
child: Builder(
|
||||
builder: (context) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
localizationsDelegates: context.localizationDelegates,
|
||||
supportedLocales: context.supportedLocales,
|
||||
locale: context.locale,
|
||||
home: const Material(child: AssetViewer(initialIndex: 0)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Let post-frame callbacks and the initial frames run without waiting for
|
||||
// infinite animations (loading spinners) to settle.
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 600));
|
||||
|
||||
// Asset thumbnails cannot load in the test environment (no platform
|
||||
// channels / real HTTP). Those image errors are irrelevant to the system
|
||||
// chrome behaviour under test, so drain them.
|
||||
tester.takeException();
|
||||
|
||||
expect(
|
||||
SystemChrome.latestStyle?.statusBarIconBrightness,
|
||||
Brightness.light,
|
||||
reason:
|
||||
'The asset viewer draws over a black background, so status bar '
|
||||
'icons must be light regardless of the app theme',
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -118,24 +118,8 @@ void main() {
|
||||
expect(task, isNotNull);
|
||||
// For live photos, extension should be changed to match the video file
|
||||
expect(task!.fields['filename'], equals('OriginalLivePhoto.mov'));
|
||||
expect(task.fields['visibility'], equals('hidden'));
|
||||
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
||||
});
|
||||
|
||||
test('should not set visibility for a regular photo', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final mockEntity = MockAssetEntity();
|
||||
final mockFile = File('/path/to/file.jpg');
|
||||
|
||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
|
||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'Regular.jpg');
|
||||
|
||||
final task = await sut.getUploadTask(asset);
|
||||
expect(task, isNotNull);
|
||||
expect(task!.fields.containsKey('visibility'), isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('getLivePhotoUploadTask', () {
|
||||
@@ -156,7 +140,7 @@ void main() {
|
||||
expect(task, isNotNull);
|
||||
expect(task!.fields['filename'], equals('OriginalLivePhoto.HEIC'));
|
||||
expect(task.fields['livePhotoVideoId'], equals('video-id-123'));
|
||||
expect(task.fields.containsKey('visibility'), isFalse);
|
||||
expect(task.fields['visibility'], equals('hidden'));
|
||||
verify(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).called(1);
|
||||
});
|
||||
|
||||
@@ -350,7 +334,7 @@ void main() {
|
||||
expect(task, isNotNull);
|
||||
expect(task!.fields.containsKey('metadata'), isTrue);
|
||||
expect(task.fields['livePhotoVideoId'], equals('video-123'));
|
||||
expect(task.fields.containsKey('visibility'), isFalse);
|
||||
expect(task.fields['visibility'], equals('hidden'));
|
||||
|
||||
final metadata = jsonDecode(task.fields['metadata']!) as List;
|
||||
expect(metadata, hasLength(1));
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart' hide isNull, isNotNull;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/settings.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/repositories/upload.repository.dart';
|
||||
import 'package:immich_mobile/services/foreground_upload.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../api.mocks.dart';
|
||||
import '../fixtures/asset.stub.dart';
|
||||
import '../infrastructure/repository.mock.dart';
|
||||
import '../mocks/asset_entity.mock.dart';
|
||||
import '../repository.mocks.dart';
|
||||
|
||||
void main() {
|
||||
late ForegroundUploadService sut;
|
||||
late MockUploadRepository mockUploadRepository;
|
||||
late MockStorageRepository mockStorageRepository;
|
||||
late MockDriftBackupRepository mockBackupRepository;
|
||||
late MockConnectivityApi mockConnectivityApi;
|
||||
late MockAssetMediaRepository mockAssetMediaRepository;
|
||||
late Drift db;
|
||||
|
||||
setUpAll(() async {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
const MethodChannel('plugins.flutter.io/path_provider'),
|
||||
(MethodCall methodCall) async => 'test',
|
||||
);
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await StoreService.init(storeRepository: DriftStoreRepository(db));
|
||||
await SettingsRepository.ensureInitialized(db);
|
||||
|
||||
await Store.put(StoreKey.serverEndpoint, 'http://demo.immich.app');
|
||||
await Store.put(StoreKey.deviceId, 'device-id');
|
||||
|
||||
registerFallbackValue(File('file'));
|
||||
registerFallbackValue(<String, String>{});
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
mockUploadRepository = MockUploadRepository();
|
||||
mockStorageRepository = MockStorageRepository();
|
||||
mockBackupRepository = MockDriftBackupRepository();
|
||||
mockConnectivityApi = MockConnectivityApi();
|
||||
mockAssetMediaRepository = MockAssetMediaRepository();
|
||||
|
||||
sut = ForegroundUploadService(
|
||||
mockUploadRepository,
|
||||
mockStorageRepository,
|
||||
mockBackupRepository,
|
||||
mockConnectivityApi,
|
||||
mockAssetMediaRepository,
|
||||
);
|
||||
});
|
||||
|
||||
List<Map<String, String>> captureFields() {
|
||||
final captured = <Map<String, String>>[];
|
||||
when(
|
||||
() => mockUploadRepository.uploadFile(
|
||||
file: any(named: 'file'),
|
||||
originalFileName: any(named: 'originalFileName'),
|
||||
fields: any(named: 'fields'),
|
||||
cancelToken: any(named: 'cancelToken'),
|
||||
onProgress: any(named: 'onProgress'),
|
||||
logContext: any(named: 'logContext'),
|
||||
),
|
||||
).thenAnswer((invocation) async {
|
||||
final fields = invocation.namedArguments[#fields] as Map<String, String>;
|
||||
captured.add(Map.of(fields));
|
||||
return UploadResult.success(remoteAssetId: 'remote-${captured.length}');
|
||||
});
|
||||
return captured;
|
||||
}
|
||||
|
||||
group('uploadSingleAsset', () {
|
||||
test('should upload the motion part hidden and keep the still image visible', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final mockEntity = MockAssetEntity();
|
||||
final stillFile = File('/path/to/still.heic');
|
||||
final videoFile = File('/path/to/motion.mov');
|
||||
|
||||
when(() => mockEntity.isLivePhoto).thenReturn(true);
|
||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||
when(() => mockStorageRepository.isAssetAvailableLocally(asset.id)).thenAnswer((_) async => true);
|
||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => stillFile);
|
||||
when(() => mockStorageRepository.getMotionFileForAsset(asset)).thenAnswer((_) async => videoFile);
|
||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'live.heic');
|
||||
|
||||
final captured = captureFields();
|
||||
|
||||
await sut.uploadSingleAsset(asset, null, callbacks: const UploadCallbacks());
|
||||
|
||||
expect(captured, hasLength(2));
|
||||
expect(captured[0]['visibility'], equals('hidden'));
|
||||
expect(captured[0].containsKey('livePhotoVideoId'), isFalse);
|
||||
expect(captured[1].containsKey('visibility'), isFalse);
|
||||
expect(captured[1]['livePhotoVideoId'], equals('remote-1'));
|
||||
});
|
||||
|
||||
test('should not set visibility for a regular photo', () async {
|
||||
final asset = LocalAssetStub.image1;
|
||||
final mockEntity = MockAssetEntity();
|
||||
final stillFile = File('/path/to/photo.jpg');
|
||||
|
||||
when(() => mockEntity.isLivePhoto).thenReturn(false);
|
||||
when(() => mockStorageRepository.getAssetEntityForAsset(asset)).thenAnswer((_) async => mockEntity);
|
||||
when(() => mockStorageRepository.isAssetAvailableLocally(asset.id)).thenAnswer((_) async => true);
|
||||
when(() => mockStorageRepository.getFileForAsset(asset.id)).thenAnswer((_) async => stillFile);
|
||||
when(() => mockAssetMediaRepository.getOriginalFilename(asset.id)).thenAnswer((_) async => 'photo.jpg');
|
||||
|
||||
final captured = captureFields();
|
||||
|
||||
await sut.uploadSingleAsset(asset, null, callbacks: const UploadCallbacks());
|
||||
|
||||
expect(captured, hasLength(1));
|
||||
expect(captured[0].containsKey('visibility'), isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -7139,9 +7139,9 @@
|
||||
"in": "query",
|
||||
"description": "Filter by date",
|
||||
"schema": {
|
||||
"format": "date",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
|
||||
"example": "2024-01-01",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -7308,9 +7308,9 @@
|
||||
"in": "query",
|
||||
"description": "Filter by date",
|
||||
"schema": {
|
||||
"format": "date",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))$",
|
||||
"example": "2024-01-01",
|
||||
"format": "date-time",
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
@@ -16223,7 +16223,7 @@
|
||||
"info": {
|
||||
"title": "Immich",
|
||||
"description": "Immich API",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"contact": {}
|
||||
},
|
||||
"tags": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-monorepo",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"description": "Monorepo for Immich",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/sdk",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Immich
|
||||
* 3.0.3
|
||||
* 3.0.2
|
||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||
* See https://www.npmjs.com/package/oazapfts
|
||||
*/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607132058@sha256:968709a3668167f11ef8ece5962e72ea8a4231e841f98bd5e0f35da4cc295cc7 AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607061334@sha256:c83ef9c6d7f5e7f6a276fe96647484d46b7bf6cacc1e35b823dd1d7a1aeda3b8 AS builder
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp \
|
||||
@@ -80,7 +80,7 @@ RUN --mount=type=cache,id=pnpm-packages,target=/buildcache/pnpm-store \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
mise //:plugins
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202607132058@sha256:85cca651310842265015f26a6710bb7420a54a8fd1ec22d519b7b620f5f93bb1
|
||||
FROM ghcr.io/immich-app/base-server-prod:202607061334@sha256:c07ef6510ff2ccb6ead28434ade7c7a0c08aef33d7d3466b14beccdd431e86f7
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607132058@sha256:968709a3668167f11ef8ece5962e72ea8a4231e841f98bd5e0f35da4cc295cc7 AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202607061334@sha256:c83ef9c6d7f5e7f6a276fe96647484d46b7bf6cacc1e35b823dd1d7a1aeda3b8 AS dev
|
||||
|
||||
|
||||
COPY --from=ghcr.io/jdx/mise:2026.7.2@sha256:51d92371cc08e3c4f0e52f58bb3ec1975a6dc966b7430a4da37dcfc6ae772f30 /usr/local/bin/mise /usr/local/bin/mise
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -4,13 +4,13 @@ import { HistoryBuilder } from 'src/decorators';
|
||||
import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { AssetOrderWithRandomSchema, MemoryType, MemoryTypeSchema } from 'src/enum';
|
||||
import { isoDatetimeToDate, isoDateToDate, nonEmptyPartial, stringToBool } from 'src/validation';
|
||||
import { isoDatetimeToDate, nonEmptyPartial, stringToBool } from 'src/validation';
|
||||
import z from 'zod';
|
||||
|
||||
const MemorySearchSchema = z
|
||||
.object({
|
||||
type: MemoryTypeSchema.optional(),
|
||||
for: isoDateToDate.optional().describe('Filter by date'),
|
||||
for: isoDatetimeToDate.optional().describe('Filter by date'),
|
||||
isTrashed: stringToBool.optional().describe('Include trashed memories'),
|
||||
isSaved: stringToBool.optional().describe('Filter by saved status'),
|
||||
size: z.coerce.number().int().min(1).optional().describe('Number of memories to return'),
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
// (#29836) Reset the visibility of any still images that are hidden and have a motion part
|
||||
await sql`
|
||||
UPDATE "asset"
|
||||
SET "visibility" = 'timeline'
|
||||
WHERE "type" = 'IMAGE'
|
||||
AND "visibility" = 'hidden'
|
||||
AND "livePhotoVideoId" IS NOT NULL
|
||||
`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(): Promise<void> {
|
||||
// Not implemented: the previous 'hidden' value was itself the bug.
|
||||
}
|
||||
@@ -663,7 +663,6 @@ describe(AlbumService.name, () => {
|
||||
const album = AlbumFactory.from().albumUser({ userId: user.id }).build();
|
||||
const { user: owner } = album.albumUsers.find(({ role }) => role === AlbumUserRole.Owner)!;
|
||||
mocks.access.album.checkOwnerAccess.mockResolvedValue(new Set([album.id]));
|
||||
mocks.album.getById.mockResolvedValue(getForAlbum(album));
|
||||
mocks.albumUser.update.mockResolvedValue();
|
||||
|
||||
await sut.updateUser(AuthFactory.create(owner), album.id, user.id, { role: AlbumUserRole.Viewer });
|
||||
|
||||
@@ -335,14 +335,6 @@ export class AlbumService extends BaseService {
|
||||
|
||||
async updateUser(auth: AuthDto, id: string, userId: string, dto: UpdateAlbumUserDto): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });
|
||||
|
||||
const album = await this.findOrFail(id, userId, { withAssets: false });
|
||||
const owner = album.albumUsers[0];
|
||||
|
||||
if (owner.user.id === userId) {
|
||||
throw new BadRequestException('User is owner');
|
||||
}
|
||||
|
||||
await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role });
|
||||
}
|
||||
|
||||
|
||||
@@ -2232,26 +2232,6 @@ describe(MediaService.name, () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should include hevc tag when target is hevc and using hwa', async () => {
|
||||
mocks.assetJob.getForVideoConversion.mockResolvedValue({ ...asset, ...probeStub.videoStreamHDR10 });
|
||||
mocks.systemMetadata.get.mockResolvedValue({
|
||||
ffmpeg: {
|
||||
targetVideoCodec: VideoCodec.Hevc,
|
||||
accel: TranscodeHardwareAcceleration.Nvenc,
|
||||
},
|
||||
});
|
||||
await sut.handleVideoConversion({ id: 'video-id' });
|
||||
expect(mocks.media.transcode).toHaveBeenCalledWith(
|
||||
'/original/path.ext',
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
inputOptions: expect.any(Array),
|
||||
outputOptions: expect.arrayContaining(['-c:v', 'hevc_nvenc', '-tag:v', 'hvc1']),
|
||||
twoPass: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should copy audio stream when audio matches target', async () => {
|
||||
mocks.assetJob.getForVideoConversion.mockResolvedValue({ ...asset, ...probeStub.audioStreamAac });
|
||||
mocks.systemMetadata.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.Optimal } });
|
||||
|
||||
@@ -118,11 +118,7 @@ export class SearchService extends BaseService {
|
||||
}
|
||||
|
||||
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
|
||||
const items = await this.searchRepository.searchRandom(dto.size || 250, {
|
||||
...dto,
|
||||
visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
|
||||
userIds,
|
||||
});
|
||||
const items = await this.searchRepository.searchRandom(dto.size || 250, { ...dto, userIds });
|
||||
return items.map((item) => mapAsset(item, { auth }));
|
||||
}
|
||||
|
||||
|
||||
@@ -241,8 +241,7 @@ export class BaseConfig implements VideoCodecSWConfig {
|
||||
options.push('-keyint_min', `${this.getGopSize()}`);
|
||||
}
|
||||
}
|
||||
const isHvc =
|
||||
this.config.targetVideoCodec === VideoCodec.Hevc && (videoCodec !== 'copy' || videoStream.codecName === 'hevc');
|
||||
const isHvc = (videoCodec === 'copy' ? videoStream.codecName : videoCodec) === VideoCodec.Hevc;
|
||||
if (isHvc) {
|
||||
options.push('-tag:v', 'hvc1');
|
||||
}
|
||||
|
||||
@@ -196,19 +196,4 @@ describe(SearchService.name, () => {
|
||||
expect(suggestions).toEqual(['Canon', null]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('searchRandom', () => {
|
||||
it('should filter out locked assets in a default session', async () => {
|
||||
const { sut, ctx } = setup();
|
||||
const { user } = await ctx.newUser();
|
||||
|
||||
await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked });
|
||||
|
||||
const auth = factory.auth({ user: { id: user.id } });
|
||||
|
||||
const response = await sut.searchRandom(auth, {});
|
||||
|
||||
expect(response.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-web",
|
||||
"version": "3.0.3",
|
||||
"version": "3.0.2",
|
||||
"license": "GNU Affero General Public License version 3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { DateTime } from 'luxon';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { asLocalTimeISO } from '$lib/utils/date-time';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
|
||||
type MemoryIndex = {
|
||||
@@ -130,7 +131,7 @@ class MemoryManager {
|
||||
}
|
||||
|
||||
private async load() {
|
||||
const memories = await searchMemories({ $for: DateTime.now().toFormat('yyyy-MM-dd') });
|
||||
const memories = await searchMemories({ $for: asLocalTimeISO(DateTime.now()) });
|
||||
this.memories = memories.filter((memory) => memory.assets.length > 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
const { trigger, selectedKey, onClose }: Props = $props();
|
||||
</script>
|
||||
|
||||
<BasicModal title={$t('add_step')} onClose={() => onClose()} size="medium">
|
||||
<BasicModal title={$t('add_step')} {onClose} size="medium">
|
||||
{#await searchPluginMethods({ trigger })}
|
||||
<div class="flex w-full place-content-center place-items-center">
|
||||
<LoadingSpinner />
|
||||
|
||||
Reference in New Issue
Block a user