Compare commits

...

10 Commits

Author SHA1 Message Date
Daniel Dietzler 06eae08d44 Merge branch 'main' into apk-build-deployment 2026-06-22 16:02:42 +02:00
Daniel Dietzler c484bd99b6 fix: ignore external libraries for integrity report checksum check (#29248) 2026-06-22 13:56:24 +00:00
Anthony Clerici c0bf5a4c56 fix(server): use VBR for QSV so the max bitrate is respected (#29240)
* fix(server): use VBR for QSV so the max bitrate is respected

* update test
2026-06-22 09:56:20 -04:00
MuySup d9d50d2848 fix: turkish readme translation (#29234)
* Translation completed

3-2-1 rule translated

* Fix formatting of warning message in Turkish README
2026-06-22 09:55:58 -04:00
Daniel Dietzler c7453a67fd fix: detail panel people reactivity and iterator consumption (#29250) 2026-06-22 15:47:09 +02:00
Daniel Dietzler e918e3a313 feat: keyboard seeking for new video player (#29208) 2026-06-22 09:42:59 -04:00
Daniel Dietzler 92293eba19 Merge branch 'main' into apk-build-deployment 2026-06-16 15:21:18 +02:00
Alex e93f0db224 Merge branch 'main' into apk-build-deployment 2026-06-12 14:10:24 -05:00
Alex Tran 46d8be8ffc different app name for staging 2026-06-05 16:16:03 -05:00
Alex Tran df051c24b3 chore: apk build deployment 2026-06-05 15:31:20 -05:00
11 changed files with 110 additions and 6 deletions
+54
View File
@@ -73,6 +73,7 @@ jobs:
needs: pre-job
permissions:
contents: read
deployments: write
pull-requests: write
if: ${{ github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: mich
@@ -142,9 +143,18 @@ jobs:
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
IS_MAIN_DEPLOYMENT: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
IS_DEPLOYMENT_BUILD: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [[ $IS_DEPLOYMENT_BUILD == 'true' ]]; then
export ANDROID_APP_LABEL='Immich Staging'
fi
if [[ $IS_MAIN == 'true' ]]; then
if [[ $IS_MAIN_DEPLOYMENT == 'true' ]]; then
export ANDROID_APPLICATION_ID=app.immich.main
fi
flutter build apk --release
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else
@@ -158,6 +168,50 @@ jobs:
name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/*.apk
- name: Publish Android APK deployment
if: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || (github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork) }}
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
with:
script: |
const artifactUrl = process.env.APK_URL;
const isPullRequest = context.eventName === "pull_request";
const pullNumber = context.payload.pull_request?.number;
const environment = isPullRequest ? `mobile-android-apk-pr-${pullNumber}` : "mobile-android-apk";
const description = isPullRequest
? `Signed Android APK for PR #${pullNumber}`
: "Latest signed Android APK from main";
const ref = isPullRequest ? context.payload.pull_request.head.sha : context.sha;
if (!artifactUrl) {
throw new Error("The Android APK artifact URL was not generated");
}
const runUrl = `${process.env.GITHUB_SERVER_URL}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
const deployment = await github.rest.repos.createDeployment({
owner: context.repo.owner,
repo: context.repo.repo,
ref,
environment,
description,
auto_merge: false,
required_contexts: [],
production_environment: false,
transient_environment: isPullRequest,
});
await github.rest.repos.createDeploymentStatus({
owner: context.repo.owner,
repo: context.repo.repo,
deployment_id: deployment.data.id,
state: "success",
environment,
environment_url: artifactUrl,
log_url: runUrl,
description: "Signed APK artifact is ready for testing",
});
- name: Comment APK download link on PR
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
uses: immich-app/devtools/actions/sticky-comment@0135acd12ad9f3369b94a2aa3c0ae8c835a4e926 # sticky-comment-action-v1.0.0
+12 -1
View File
@@ -13,6 +13,16 @@ if (keystorePropertiesFile.exists()) {
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
}
def androidApplicationId = System.getenv("ANDROID_APPLICATION_ID")?.trim()
if (!androidApplicationId) {
androidApplicationId = "app.alextran.immich"
}
def androidAppLabel = System.getenv("ANDROID_APP_LABEL")?.trim()
if (!androidAppLabel) {
androidAppLabel = "Immich"
}
android {
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
@@ -37,11 +47,12 @@ android {
}
defaultConfig {
applicationId "app.alextran.immich"
applicationId androidApplicationId
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode flutter.versionCode
versionName flutter.versionName
manifestPlaceholders = [appLabel: androidAppLabel]
}
signingConfigs {
@@ -25,7 +25,7 @@
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
<application android:label="${appLabel}" android:name=".ImmichApp" android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"
android:largeHeap="true" android:enableOnBackInvokedCallback="false" android:allowBackup="false"
android:networkSecurityConfig="@xml/network_security_config">
+2 -2
View File
@@ -38,8 +38,8 @@
</p>
> [!WARNING]
> ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos!
>
> ⚠️ Değerli fotoğraflarınız ve videolarınız için daima [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) yedekleme planını uygulayın!
>
> [!NOTE]
> Kurulum dahil olmak üzere resmi belgeleri https://immich.app/ adresinde bulabilirsiniz.
@@ -129,6 +129,7 @@ from
and "integrity_report"."type" = $1
where
"asset"."deletedAt" is null
and "asset"."isExternal" = false
and "integrity_report"."createdAt" >= $2
and "integrity_report"."createdAt" <= $3
order by
@@ -177,6 +177,7 @@ export class IntegrityRepository {
'asset.id as assetId',
'integrity_report.id as reportId',
])
.where('asset.isExternal', '=', sql.lit(false))
.$if(startMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '>=', startMarker!))
.$if(endMarker !== undefined, (qb) => qb.where('integrity_report.createdAt', '<=', endMarker!))
.orderBy('integrity_report.createdAt', 'asc')
@@ -2939,6 +2939,8 @@ describe(MediaService.name, () => {
'7',
'-global_quality:v',
'23',
'-b:v',
'6897k',
'-maxrate',
'10000k',
'-bufsize',
+6
View File
@@ -788,6 +788,12 @@ export class QsvSwDecodeConfig extends BaseHWConfig {
const options = [`-${this.useCQP() ? 'q:v' : 'global_quality:v'}`, `${this.config.crf}`];
const bitrates = this.getBitrateDistribution();
if (bitrates.max > 0) {
// Workaround for https://github.com/immich-app/immich/issues/29220, to be revisited
// QSV seems to ignore -maxrate without -b:v
// -b:v alongside global_quality uses QVBR
if (!this.useCQP()) {
options.push('-b:v', `${bitrates.target}${bitrates.unit}`);
}
options.push('-maxrate', `${bitrates.max}${bitrates.unit}`, '-bufsize', `${bitrates.max * 2}${bitrates.unit}`);
}
return options;
@@ -686,6 +686,22 @@ describe(IntegrityService.name, () => {
nextCursor: undefined,
});
});
it('should skip external library files', async () => {
const { sut, ctx } = setup();
const job = ctx.getMock(JobRepository);
job.queue.mockResolvedValue(void 0);
const { user } = await ctx.newUser();
await ctx.newAsset({ ownerId: user.id, isExternal: true });
await sut.handleChecksumFiles({ refreshOnly: false });
await expect(
ctx.get(IntegrityRepository).getIntegrityReport({ limit: 100 }, IntegrityReport.ChecksumFail),
).resolves.toEqual({ items: [], nextCursor: undefined });
});
});
describe('handleChecksumRefresh', () => {
@@ -324,6 +324,18 @@
shortcut: { key: ' ' },
onShortcut: () => (videoPlayer?.paused ? videoPlayer?.play() : videoPlayer?.pause()),
},
{
shortcut: { shift: true, key: 'ArrowLeft' },
onShortcut: () =>
videoPlayer ? (videoPlayer.currentTime = Math.max(videoPlayer.currentTime - 0.4, 0)) : undefined,
},
{
shortcut: { shift: true, key: 'ArrowRight' },
onShortcut: () =>
videoPlayer
? (videoPlayer.currentTime = Math.min(videoPlayer.currentTime + 0.4, videoPlayer.duration))
: undefined,
},
]}
/>
+3 -2
View File
@@ -24,7 +24,8 @@ class FaceManager {
});
readonly people = $derived.by(() => {
const people = new SvelteMap<string, PersonResponseDto>();
// eslint-disable-next-line svelte/prefer-svelte-reactivity
const people = new Map<string, PersonResponseDto>();
for (const face of this.data) {
if (face.person) {
@@ -32,7 +33,7 @@ class FaceManager {
}
}
return people.values();
return Array.from(people.values());
});
readonly facesByPersonId = $derived.by(() => {