Compare commits

..

4 Commits

Author SHA1 Message Date
Alex Tran 476e73b0c5 feat: command for user pages 2026-05-21 23:05:34 -05:00
Mert 7b9dab872b fix(mobile): separate group ids for separate app installs (#28448)
* separate group ids

* remove pigeon method

* Revert "remove pigeon method"

This reverts commit d699ff2094.
2026-05-21 12:25:20 -05:00
Daniel Dietzler 6413495fb8 fix: mise lockfile (#28541) 2026-05-21 13:13:37 +02:00
Caltsic b414b3d32b fix: improve form control focus visibility (#28512)
* Improve form control focus visibility

* fix: align form input focus styles
2026-05-20 15:33:56 -05:00
26 changed files with 285 additions and 227 deletions
-1
View File
@@ -288,7 +288,6 @@ jobs:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
ENVIRONMENT: ${{ inputs.environment || 'development' }}
BUNDLE_ID_SUFFIX: ${{ inputs.environment == 'production' && '' || 'development' }}
GITHUB_REF: ${{ github.ref }}
FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT: 120
FASTLANE_XCODEBUILD_SETTINGS_RETRIES: 6
+5 -64
View File
@@ -1,5 +1,9 @@
# @generated - this file is auto-generated by `mise lock` https://mise.en.dev/dev-tools/mise-lock.html
[[tools."aqua:flutter/flutter"]]
version = "3.41.9"
backend = "aqua:flutter/flutter"
[[tools.flutter]]
version = "3.41.9-stable"
backend = "asdf:flutter"
@@ -12,43 +16,36 @@ backend = "github:CQLabs/homebrew-dcm"
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-arm64-musl"]
checksum = "sha256:253da2512b149913dfe345bf9a62a79acb2d730f66e71162ba4a92dfc4224b82"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543838"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.linux-x64-musl"]
checksum = "sha256:477e086d4099c12f21e5ccd83b005d5fb945dd4cac4fd127fd9a08d7649af1cf"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-linux-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543797"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-arm64"]
checksum = "sha256:30bede64367d09067093cc57af6ec9496d7717898138ded5cb98a16ac8dd9d93"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-arm-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543757"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.macos-x64"]
checksum = "sha256:e56cb99872be7445a4de1d37e5438ca70e3bcd83be7a2b9b385e3538881f8068"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-macos-x64-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543727"
github_attestations = "unavailable"
[tools."github:CQLabs/homebrew-dcm"."platforms.windows-x64"]
checksum = "sha256:f133470daa3fb0427f039b424392af7e917d7e7db6b556aa2a968ab0e31587da"
url = "https://github.com/CQLabs/homebrew-dcm/releases/download/1.37.0/dcm-windows-release.zip"
url_api = "https://api.github.com/repos/CQLabs/homebrew-dcm/releases/assets/404543660"
github_attestations = "unavailable"
[[tools."github:extism/cli"]]
version = "1.6.3"
@@ -58,43 +55,36 @@ backend = "github:extism/cli"
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.linux-arm64-musl"]
checksum = "sha256:d92f830c9be39637569feacb04e9750c28848df6d9a219db94152a9b4eb9452b"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694030"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.linux-x64"]
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.linux-x64-musl"]
checksum = "sha256:34e7ae9bfded6e2c32dee83f70a4e50d34f9d3e80d1762b09625fe82e214d02d"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-linux-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694025"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.macos-arm64"]
checksum = "sha256:b4ddbc575b5ac000115247f781723f9b9f284ed87b29c600539d72161b5b29fc"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-arm64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694029"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.macos-x64"]
checksum = "sha256:9a2f71b6e6009685a622cc3084e52d2a1a8e23c98d29ffa72e666e9dc699855f"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-darwin-amd64.tar.gz"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694026"
github_attestations = "unavailable"
[tools."github:extism/cli"."platforms.windows-x64"]
checksum = "sha256:47e4ed2782445b2b08a4d1ac127211588f8b4d1fc25fd6481d4cb65151b5213c"
url = "https://github.com/extism/cli/releases/download/v1.6.3/extism-v1.6.3-windows-amd64.zip"
url_api = "https://api.github.com/repos/extism/cli/releases/assets/275694035"
github_attestations = "unavailable"
[[tools."github:extism/js-pdk"]]
version = "1.6.0"
@@ -104,43 +94,36 @@ backend = "github:extism/js-pdk"
checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.linux-arm64-musl"]
checksum = "sha256:15a186250e68d6bff4ec839fff275d45a90e383a69209dcc1239eb9e3aee6e1b"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223214"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.linux-x64"]
checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.linux-x64-musl"]
checksum = "sha256:4ded271ccf465031ccd0dc35e7a140e134d7f30721671cc4a8e1ff805d4aad68"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-linux-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223119"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.macos-arm64"]
checksum = "sha256:548e25bda3971a07c32d78a249135cf8cb7b3eede101e878e06e53e01ac2e0ce"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-aarch64-macos-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223215"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.macos-x64"]
checksum = "sha256:d85a875c2a071f0c29fe572764c52c3a499f157ab7f9efac8939a4364390e29b"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-macos-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353223239"
github_attestations = "unavailable"
[tools."github:extism/js-pdk"."platforms.windows-x64"]
checksum = "sha256:97b7b746141e4777e1ca2b76febdeb16dc9d314ff6a4257df05a476b67228acc"
url = "https://github.com/extism/js-pdk/releases/download/v1.6.0/extism-js-x86_64-windows-v1.6.0.gz"
url_api = "https://api.github.com/repos/extism/js-pdk/releases/assets/353224133"
github_attestations = "unavailable"
[[tools."github:jellyfin/jellyfin-ffmpeg"]]
version = "7.1.3-6"
@@ -150,43 +133,36 @@ backend = "github:jellyfin/jellyfin-ffmpeg"
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-arm64-musl"]
checksum = "sha256:bea03c670e8cc5bfe9edc0c5d624d4735421610cef5e808db93e7d8596952886"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linuxarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048876"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.linux-x64-musl"]
checksum = "sha256:39e99a7927468a6abec5f65d00f55010e8ff2ae3c2605294f179c94f6ae21af2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_linux64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409048879"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-arm64"]
checksum = "sha256:e024d5e78d5414e75f0181036cd21373fafb9270c72894dfd7dbda2572439820"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_macarm64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995838"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.macos-x64"]
checksum = "sha256:066ede9774aaae97a18098aaeea8b7e0d286653eb8618f640476e99c59a536c2"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_mac64-gpl.tar.xz"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/408995889"
github_attestations = "unavailable"
[tools."github:jellyfin/jellyfin-ffmpeg"."platforms.windows-x64"]
checksum = "sha256:7b7168149689610296f3a187c717056ce0786cc125a31caf28056737e9ba1cc1"
url = "https://github.com/jellyfin/jellyfin-ffmpeg/releases/download/v7.1.3-6/jellyfin-ffmpeg_7.1.3-6_portable_win64-clang-gpl.zip"
url_api = "https://api.github.com/repos/jellyfin/jellyfin-ffmpeg/releases/assets/409036094"
github_attestations = "unavailable"
[[tools."github:webassembly/binaryen"]]
version = "version_124"
@@ -196,43 +172,36 @@ backend = "github:webassembly/binaryen"
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.linux-arm64-musl"]
checksum = "sha256:6291bd9a57d8e046f3bc099a4db386c147433a87f71c783a901c5b1792e38de3"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-aarch64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288927659"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.linux-x64"]
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.linux-x64-musl"]
checksum = "sha256:0290c3779fedf592b8da0ded3032ff55c41a2b7bfa2d6bf7b7bac6f0e6e28963"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-linux.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926769"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.macos-arm64"]
checksum = "sha256:86a2c960ff62c6d2ea6009d1f89745c22c70100d394a095eab45eb941bdaa24c"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-arm64-macos.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926134"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.macos-x64"]
checksum = "sha256:b389bb0731758d86c3cb266d01d28a12725c23bd3cabc3df34faa162af0887e9"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-macos.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288926135"
github_attestations = "unavailable"
[tools."github:webassembly/binaryen"."platforms.windows-x64"]
checksum = "sha256:b5e1d2a1ad3c03229ddc89823848f4a1c11f9c6402a51fa26f0aaa5f1d7a2203"
url = "https://github.com/WebAssembly/binaryen/releases/download/version_124/binaryen-version_124-x86_64-windows.tar.gz"
url_api = "https://api.github.com/repos/WebAssembly/binaryen/releases/assets/288925833"
github_attestations = "unavailable"
[[tools.java]]
version = "21.0.2"
@@ -327,37 +296,9 @@ checksum = "sha256:27323f70c875b8251bfd7e61a4cffc3ebff4e56ed1e611b955016f0c70773
url = "https://github.com/opentofu/opentofu/releases/download/v1.11.6/tofu_1.11.6_windows_amd64.tar.gz"
[[tools.pnpm]]
version = "10.33.1"
version = "10.33.4"
backend = "aqua:pnpm/pnpm"
[tools.pnpm."platforms.linux-arm64"]
checksum = "sha256:ed8aa7901cf325f4cf5019405bdd6bf988426e4b23d08fe9b12ea4df7046f23e"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-arm64"
[tools.pnpm."platforms.linux-arm64-musl"]
checksum = "sha256:ed8aa7901cf325f4cf5019405bdd6bf988426e4b23d08fe9b12ea4df7046f23e"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-arm64"
[tools.pnpm."platforms.linux-x64"]
checksum = "sha256:fba950842532edd365e949b74643b64e6311089a45532dbe1e8f909a247fe3e9"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-x64"
[tools.pnpm."platforms.linux-x64-musl"]
checksum = "sha256:fba950842532edd365e949b74643b64e6311089a45532dbe1e8f909a247fe3e9"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-linux-x64"
[tools.pnpm."platforms.macos-arm64"]
checksum = "sha256:909ced0038b00881d4d620ba2018c5d9691de373deea8e3c84b722b44324e47c"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-macos-arm64"
[tools.pnpm."platforms.macos-x64"]
checksum = "sha256:afdad60b83f4f482f4c95cc79325f29aef776d0922a324f023a312f40e0cc7d3"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-macos-x64"
[tools.pnpm."platforms.windows-x64"]
checksum = "sha256:67b23fd8c6800566b1cc04c446b170ff6e7977250084e4d8df9bfdbd8e6f4d02"
url = "https://github.com/pnpm/pnpm/releases/download/v10.33.1/pnpm-win-x64.exe"
[[tools.terragrunt]]
version = "1.0.3"
backend = "aqua:gruntwork-io/terragrunt"
@@ -315,6 +315,7 @@ interface NetworkApi {
fun hasCertificate(): Boolean
fun getClientPointer(): Long
fun setRequestHeaders(headers: Map<String, String>, serverUrls: List<String>, token: String?)
fun getAppGroupId(): String
companion object {
/** The codec used by NetworkApi. */
@@ -430,6 +431,21 @@ interface NetworkApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { _, reply ->
val wrapped: List<Any?> = try {
listOf(api.getAppGroupId())
} catch (exception: Throwable) {
NetworkPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
@@ -13,7 +13,7 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware {
private var networkApi: NetworkApiImpl? = null
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
networkApi = NetworkApiImpl()
networkApi = NetworkApiImpl(binding.applicationContext)
NetworkApi.setUp(binding.binaryMessenger, networkApi)
}
@@ -39,9 +39,11 @@ class NetworkApiPlugin : FlutterPlugin, ActivityAware {
}
}
private class NetworkApiImpl : NetworkApi {
private class NetworkApiImpl(private val context: Context) : NetworkApi {
var activity: Activity? = null
override fun getAppGroupId(): String = context.packageName
override fun addCertificate(clientData: ClientCertData, callback: (Result<Unit>) -> Unit) {
try {
HttpClientManager.setKeyEntry(clientData.data, clientData.password.toCharArray())
+3 -6
View File
@@ -718,6 +718,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CUSTOM_GROUP_ID = group.app.immich.share.profile;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -750,7 +751,6 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -801,6 +801,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CUSTOM_GROUP_ID = group.app.immich.share.debug;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
@@ -860,6 +861,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
CUSTOM_GROUP_ID = group.app.immich.share;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
@@ -894,7 +896,6 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -924,7 +925,6 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -1080,7 +1080,6 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1124,7 +1123,6 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
@@ -1165,7 +1163,6 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 240;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2W7AC6T8T5;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
+14
View File
@@ -288,6 +288,7 @@ protocol NetworkApi {
func hasCertificate() throws -> Bool
func getClientPointer() throws -> Int64
func setRequestHeaders(headers: [String: String], serverUrls: [String], token: String?) throws
func getAppGroupId() throws -> String
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
@@ -388,5 +389,18 @@ class NetworkApiSetup {
} else {
setRequestHeadersChannel.setMessageHandler(nil)
}
let getAppGroupIdChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
getAppGroupIdChannel.setMessageHandler { _, reply in
do {
let result = try api.getAppGroupId()
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getAppGroupIdChannel.setMessageHandler(nil)
}
}
}
@@ -61,6 +61,10 @@ class NetworkApiImpl: NetworkApi {
return Int64(Int(bitPattern: pointer))
}
func getAppGroupId() throws -> String {
return Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
}
func setRequestHeaders(headers: [String : String], serverUrls: [String], token: String?) throws {
URLSessionManager.setServerUrls(serverUrls)
@@ -4,7 +4,7 @@ import native_video_player
let CLIENT_CERT_LABEL = "app.alextran.immich.client_identity"
let HEADERS_KEY = "immich.request_headers"
let SERVER_URLS_KEY = "immich.server_urls"
let APP_GROUP = "group.app.immich.share"
let APP_GROUP = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
let COOKIE_EXPIRY_DAYS: TimeInterval = 400
enum AuthCookie: CaseIterable {
+1 -1
View File
@@ -10,7 +10,7 @@
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
<string>$(CUSTOM_GROUP_ID)</string>
</array>
</dict>
</plist>
+1 -1
View File
@@ -12,7 +12,7 @@
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
<string>$(CUSTOM_GROUP_ID)</string>
</array>
</dict>
</plist>
@@ -4,7 +4,7 @@
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
<string>$(CUSTOM_GROUP_ID)</string>
</array>
</dict>
</plist>
+1 -1
View File
@@ -2,7 +2,7 @@ import Foundation
import SwiftUI
import WidgetKit
let IMMICH_SHARE_GROUP = "group.app.immich.share"
let IMMICH_SHARE_GROUP = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as! String
enum WidgetError: Error, Codable {
case noLogin
+2
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
@@ -4,7 +4,7 @@
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
<string>$(CUSTOM_GROUP_ID)</string>
</array>
</dict>
</plist>
+16 -6
View File
@@ -21,6 +21,7 @@ platform :ios do
CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})"
BASE_BUNDLE_ID = "app.alextran.immich"
DEV_BUNDLE_ID = "tech.futo.immich.testflight"
DEV_GROUP_ID = "group.app.immich.share.testflight"
# Helper method to get App Store Connect API key
def get_api_key
@@ -33,6 +34,13 @@ platform :ios do
)
end
# Helper method to assemble xcargs with optional CUSTOM_GROUP_ID override
def build_xcargs(group_id: nil)
args = "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual"
args += " CUSTOM_GROUP_ID='#{group_id}'" if group_id
args
end
# Helper method to get version from pubspec.yaml
def get_version_from_pubspec
require 'yaml'
@@ -89,7 +97,8 @@ end
version_number: nil,
profile_name_main:,
profile_name_share:,
profile_name_widget:
profile_name_widget:,
group_id: nil
)
app_identifier = base_bundle_id
@@ -97,7 +106,7 @@ end
if version_number
increment_version_number(version_number: version_number)
end
# Increment build number
increment_build_number(
build_number: latest_testflight_build_number(
@@ -106,14 +115,14 @@ end
) + 1,
xcodeproj: "./Runner.xcodeproj"
)
# Build the app
build_app(
scheme: "Runner",
workspace: "Runner.xcworkspace",
configuration: configuration,
export_method: "app-store",
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
xcargs: build_xcargs(group_id: group_id),
export_options: {
provisioningProfiles: {
"#{app_identifier}" => profile_name_main,
@@ -165,7 +174,8 @@ end
distribute_external: false,
profile_name_main: main_profile_name,
profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name
profile_name_widget: widget_profile_name,
group_id: DEV_GROUP_ID
)
end
@@ -274,7 +284,7 @@ end
configuration: "Release",
export_method: "app-store",
skip_package_ipa: true,
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
xcargs: build_xcargs(group_id: DEV_GROUP_ID),
export_options: {
provisioningProfiles: {
DEV_BUNDLE_ID => main_profile_name,
-2
View File
@@ -23,7 +23,6 @@ const String kBackupLivePhotoGroup = 'backup_live_photo_group';
const String kDownloadGroupImage = 'group_image';
const String kDownloadGroupVideo = 'group_video';
const String kDownloadGroupLivePhoto = 'group_livephoto';
const String kShareDownloadGroup = 'group_share';
// Timeline constants
const int kTimelineNoneSegmentSize = 120;
@@ -31,7 +30,6 @@ const int kTimelineAssetLoadBatchSize = 1024;
const int kTimelineAssetLoadOppositeSize = 64;
// Widget keys
const String appShareGroupId = "group.app.immich.share";
const String kWidgetAuthToken = "widget_auth_token";
const String kWidgetServerEndpoint = "widget_server_url";
const String kWidgetCustomHeaders = "widget_custom_headers";
+19
View File
@@ -309,4 +309,23 @@ class NetworkApi {
_extractReplyValueOrThrow(pigeonVar_replyList, pigeonVar_channelName, isNullValid: true);
}
Future<String> getAppGroupId() async {
final pigeonVar_channelName =
'dev.flutter.pigeon.immich_mobile.NetworkApi.getAppGroupId$pigeonVar_messageChannelSuffix';
final pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(null);
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
final Object? pigeonVar_replyValue = _extractReplyValueOrThrow(
pigeonVar_replyList,
pigeonVar_channelName,
isNullValid: false,
);
return pigeonVar_replyValue! as String;
}
}
@@ -14,9 +14,7 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class _SharePreparingDialog extends StatelessWidget {
final ValueNotifier<double?> progress;
const _SharePreparingDialog({required this.progress});
const _SharePreparingDialog();
@override
Widget build(BuildContext context) {
@@ -24,24 +22,8 @@ class _SharePreparingDialog extends StatelessWidget {
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(margin: const EdgeInsets.only(bottom: 12), child: const Text('share_dialog_preparing').tr()),
SizedBox(
width: 240,
child: ValueListenableBuilder<double?>(
valueListenable: progress,
builder: (context, value, _) {
final percent = value == null ? null : (value * 100).clamp(0, 100);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
LinearProgressIndicator(value: value, minHeight: 8.0),
if (percent != null)
Container(margin: const EdgeInsets.only(top: 8), child: Text('${percent.toStringAsFixed(0)}%')),
],
);
},
),
),
const CircularProgressIndicator(),
Container(margin: const EdgeInsets.only(top: 12), child: const Text('share_dialog_preparing').tr()),
],
),
);
@@ -61,39 +43,32 @@ class ShareActionButton extends ConsumerWidget {
}
final cancelCompleter = Completer<void>();
final progress = ValueNotifier<double?>(null);
final preparingDialog = _SharePreparingDialog(progress: progress);
const preparingDialog = _SharePreparingDialog();
await showDialog(
context: context,
builder: (BuildContext buildContext) {
ref
.read(actionProvider.notifier)
.shareAssets(
source,
context,
cancelCompleter: cancelCompleter,
onAssetDownloadProgress: (value) => progress.value = value,
)
.then((ActionResult result) {
if (cancelCompleter.isCompleted || !context.mounted) {
return;
}
ref.read(actionProvider.notifier).shareAssets(source, context, cancelCompleter: cancelCompleter).then((
ActionResult result,
) {
if (cancelCompleter.isCompleted || !context.mounted) {
return;
}
ref.read(multiSelectProvider.notifier).reset();
ref.read(multiSelectProvider.notifier).reset();
if (!result.success) {
ImmichToast.show(
context: context,
msg: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
}
if (!result.success) {
ImmichToast.show(
context: context,
msg: 'scaffold_body_error_occurred'.t(context: context),
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
);
}
buildContext.pop();
});
buildContext.pop();
});
// Show download progress with a "Preparing" message
// show a loading spinner with a "Preparing" message
return preparingDialog;
},
barrierDismissible: false,
@@ -102,7 +77,6 @@ class ShareActionButton extends ConsumerWidget {
if (!cancelCompleter.isCompleted) {
cancelCompleter.complete();
}
progress.dispose();
});
}
@@ -463,17 +463,11 @@ class ActionNotifier extends Notifier<void> {
ActionSource source,
BuildContext context, {
Completer<void>? cancelCompleter,
void Function(double progress)? onAssetDownloadProgress,
}) async {
final ids = _getAssets(source).toList(growable: false);
try {
await _service.shareAssets(
ids,
context,
cancelCompleter: cancelCompleter,
onAssetDownloadProgress: onAssetDownloadProgress,
);
await _service.shareAssets(ids, context, cancelCompleter: cancelCompleter);
return ActionResult(count: ids.length, success: true);
} catch (error, stack) {
_logger.severe('Failed to share assets', error, stack);
@@ -1,26 +1,26 @@
import 'dart:async';
import 'dart:io';
import 'package:background_downloader/background_downloader.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/constants.dart';
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/platform_extensions.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/image_url_builder.dart';
import 'package:immich_mobile/extensions/response_extensions.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:logging/logging.dart';
import 'package:path_provider/path_provider.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:share_plus/share_plus.dart';
final assetMediaRepositoryProvider = Provider((ref) => const AssetMediaRepository());
final assetMediaRepositoryProvider = Provider((ref) => AssetMediaRepository(ref.watch(assetApiRepositoryProvider)));
class AssetMediaRepository {
final AssetApiRepository _assetApiRepository;
static final Logger _log = Logger("AssetMediaRepository");
const AssetMediaRepository();
const AssetMediaRepository(this._assetApiRepository);
Future<bool> _androidSupportsTrash() async {
if (Platform.isAndroid) {
@@ -81,29 +81,10 @@ class AssetMediaRepository {
);
}
Future<int> shareAssets(
List<BaseAsset> assets,
BuildContext context, {
Completer<void>? cancelCompleter,
void Function(double progress)? onAssetDownloadProgress,
}) async {
// TODO: make this more efficient
Future<int> shareAssets(List<BaseAsset> assets, BuildContext context, {Completer<void>? cancelCompleter}) async {
final downloadedXFiles = <XFile>[];
final tempFiles = <File>[];
final totalAssets = assets.length;
var processedAssets = 0;
void updateProgress([double currentAssetProgress = 0.0]) {
if (totalAssets <= 0) {
onAssetDownloadProgress?.call(1.0);
return;
}
final normalizedAssetProgress = currentAssetProgress.clamp(0.0, 1.0);
final overallProgress = ((processedAssets + normalizedAssetProgress) / totalAssets).clamp(0.0, 1.0);
onAssetDownloadProgress?.call(overallProgress);
}
updateProgress();
for (var asset in assets) {
if (cancelCompleter != null && cancelCompleter.isCompleted) {
@@ -120,8 +101,6 @@ class AssetMediaRepository {
if (localId != null && !asset.isEdited) {
File? f = await AssetEntity(id: localId, width: 1, height: 1, typeInt: 0).originFile;
downloadedXFiles.add(XFile(f!.path));
processedAssets++;
updateProgress();
if (CurrentPlatform.isIOS) {
tempFiles.add(f);
}
@@ -129,50 +108,22 @@ class AssetMediaRepository {
final remoteId = (asset is RemoteAsset) ? asset.id : asset.remoteId;
if (remoteId == null) {
_log.warning("Asset has no remote ID for sharing: $asset");
processedAssets++;
updateProgress();
continue;
}
final taskId = 'share-$remoteId-${DateTime.now().microsecondsSinceEpoch}';
final sanitizedFilename = asset.name.replaceAll(RegExp(r'[\\/]'), '_');
final task = DownloadTask(
taskId: taskId,
url: getOriginalUrlForRemoteId(remoteId, edited: asset.isEdited),
headers: ApiService.getRequestHeaders(),
filename: sanitizedFilename,
baseDirectory: BaseDirectory.temporary,
group: kShareDownloadGroup,
updates: Updates.statusAndProgress,
);
final statusUpdate = await FileDownloader().download(
task,
onProgress: (value) {
if (cancelCompleter != null && cancelCompleter.isCompleted) {
unawaited(FileDownloader().cancelTaskWithId(taskId));
return;
}
updateProgress(value);
},
);
final tempDir = await getTemporaryDirectory();
final name = asset.name;
final tempFile = await File('${tempDir.path}/$name').create();
final res = await _assetApiRepository.downloadAsset(remoteId, edited: true);
if (cancelCompleter != null && cancelCompleter.isCompleted) {
await _cleanupTempFiles(tempFiles);
return 0;
}
if (statusUpdate.status == TaskStatus.complete) {
final filePath = await task.filePath();
final file = File(filePath);
tempFiles.add(file);
downloadedXFiles.add(XFile(filePath));
processedAssets++;
updateProgress();
if (res.statusCode != 200) {
_log.severe("Download for $name failed", res.toLoggerString());
continue;
}
_log.severe("Download for ${asset.name} failed with status ${statusUpdate.status}", statusUpdate.exception);
processedAssets++;
updateProgress();
await tempFile.writeAsBytes(res.bodyBytes);
downloadedXFiles.add(XFile(tempFile.path));
tempFiles.add(tempFile);
}
}
@@ -1,5 +1,6 @@
import 'package:home_widget/home_widget.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart';
final widgetRepositoryProvider = Provider((_) => const WidgetRepository());
@@ -14,7 +15,7 @@ class WidgetRepository {
await HomeWidget.updateWidget(iOSName: iosName, qualifiedAndroidName: androidName);
}
Future<void> setAppGroupId(String appGroupId) async {
await HomeWidget.setAppGroupId(appGroupId);
Future<void> setAppGroupId() async {
await HomeWidget.setAppGroupId(await networkApi.getAppGroupId());
}
}
+2 -12
View File
@@ -269,18 +269,8 @@ class ActionService {
await _assetApiRepository.unStack(stackIds);
}
Future<int> shareAssets(
List<BaseAsset> assets,
BuildContext context, {
Completer<void>? cancelCompleter,
void Function(double progress)? onAssetDownloadProgress,
}) {
return _assetMediaRepository.shareAssets(
assets,
context,
cancelCompleter: cancelCompleter,
onAssetDownloadProgress: onAssetDownloadProgress,
);
Future<int> shareAssets(List<BaseAsset> assets, BuildContext context, {Completer<void>? cancelCompleter}) {
return _assetMediaRepository.shareAssets(assets, context, cancelCompleter: cancelCompleter);
}
Future<List<bool>> downloadAll(List<RemoteAsset> assets) {
+2 -2
View File
@@ -12,7 +12,7 @@ class WidgetService {
const WidgetService(this._repository);
Future<void> writeCredentials(String serverURL, String sessionKey, String? customHeaders) async {
await _repository.setAppGroupId(appShareGroupId);
await _repository.setAppGroupId();
await _repository.saveData(kWidgetServerEndpoint, serverURL);
await _repository.saveData(kWidgetAuthToken, sessionKey);
@@ -25,7 +25,7 @@ class WidgetService {
}
Future<void> clearCredentials() async {
await _repository.setAppGroupId(appShareGroupId);
await _repository.setAppGroupId();
await _repository.saveData(kWidgetServerEndpoint, "");
await _repository.saveData(kWidgetAuthToken, "");
await _repository.saveData(kWidgetCustomHeaders, "");
+2
View File
@@ -44,4 +44,6 @@ abstract class NetworkApi {
int getClientPointer();
void setRequestHeaders(Map<String, String> headers, List<String> serverUrls, String? token);
String getAppGroupId();
}
+1 -1
View File
@@ -34,7 +34,7 @@
}
@utility immich-form-input {
@apply bg-gray-100 ring-1 ring-gray-200 transition outline-none focus-within:ring-1 disabled:cursor-not-allowed dark:bg-gray-800 dark:ring-neutral-900 flex w-full items-center rounded-lg disabled:bg-gray-300 disabled:text-dark dark:disabled:bg-gray-900 dark:disabled:text-gray-200 flex-1 py-2.5 text-base pl-4 pr-4;
@apply bg-gray-100 ring-1 ring-gray-200 transition outline-none focus-within:ring-primary focus-within:ring-1 disabled:cursor-not-allowed dark:bg-gray-800 dark:ring-neutral-900 dark:focus-within:ring-primary flex w-full items-center rounded-lg disabled:bg-gray-300 disabled:text-dark dark:disabled:bg-gray-900 dark:disabled:text-gray-200 flex-1 py-2.5 text-base pl-4 pr-4;
}
@utility immich-form-label {
+145 -1
View File
@@ -1,17 +1,35 @@
import { defaultProvider, screencastManager, themeManager, ThemePreference, type ActionItem } from '@immich/ui';
import {
mdiAccountMultipleOutline,
mdiAccountOutline,
mdiArchiveArrowDownOutline,
mdiBookshelf,
mdiCog,
mdiContentDuplicate,
mdiCrosshairsGps,
mdiFolderOutline,
mdiHeartOutline,
mdiImageAlbum,
mdiImageMultipleOutline,
mdiImageSizeSelectLarge,
mdiKeyboard,
mdiLink,
mdiLockOutline,
mdiMagnify,
mdiMapOutline,
mdiServer,
mdiStateMachine,
mdiSync,
mdiTagMultipleOutline,
mdiThemeLightDark,
mdiToolboxOutline,
mdiTrashCanOutline,
} from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n';
import { goto } from '$app/navigation';
import { page } from '$app/state';
import { authManager } from '$lib/managers/auth-manager.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { Route } from '$lib/route';
import { copyToClipboard } from '$lib/utils';
@@ -49,7 +67,133 @@ export const getPagesProvider = ($t: MessageFormatter) => {
},
].map((route) => ({ ...route, $if: () => authManager.authenticated && authManager.user.isAdmin }));
return defaultProvider({ name: $t('page'), actions: adminPages });
const userPages: ActionItem[] = [
{
title: $t('photos'),
icon: mdiImageMultipleOutline,
onAction: () => goto(Route.photos()),
},
{
title: $t('explore'),
icon: mdiMagnify,
onAction: () => goto(Route.explore()),
$if: () => authManager.authenticated && featureFlagsManager.value.search,
},
{
title: $t('map'),
icon: mdiMapOutline,
onAction: () => goto(Route.map()),
$if: () => authManager.authenticated && featureFlagsManager.value.map,
},
{
title: $t('people'),
description: $t('people_feature_description'),
icon: mdiAccountOutline,
onAction: () => goto(Route.people()),
$if: () => authManager.authenticated && authManager.preferences.people.enabled,
},
{
title: $t('shared_links'),
icon: mdiLink,
onAction: () => goto(Route.sharedLinks()),
$if: () => authManager.authenticated && authManager.preferences.sharedLinks.enabled,
},
{
title: $t('recently_added'),
icon: mdiMagnify,
onAction: () => goto(Route.recentlyAdded()),
$if: () => authManager.authenticated,
},
{
title: $t('sharing'),
icon: mdiAccountMultipleOutline,
onAction: () => goto(Route.sharing()),
$if: () => authManager.authenticated,
},
{
title: $t('favorites'),
icon: mdiHeartOutline,
onAction: () => goto(Route.favorites()),
$if: () => authManager.authenticated,
},
{
title: $t('albums'),
description: $t('albums_feature_description'),
icon: mdiImageAlbum,
onAction: () => goto(Route.albums()),
$if: () => authManager.authenticated,
},
{
title: $t('tags'),
description: $t('tag_feature_description'),
icon: mdiTagMultipleOutline,
onAction: () => goto(Route.tags()),
$if: () => authManager.authenticated && authManager.preferences.tags.enabled,
},
{
title: $t('folders'),
description: $t('folders_feature_description'),
icon: mdiFolderOutline,
onAction: () => goto(Route.folders()),
$if: () => authManager.authenticated && authManager.preferences.folders.enabled,
},
{
title: $t('utilities'),
icon: mdiToolboxOutline,
onAction: () => goto(Route.utilities()),
$if: () => authManager.authenticated,
},
{
title: $t('archive'),
icon: mdiArchiveArrowDownOutline,
onAction: () => goto(Route.archive()),
$if: () => authManager.authenticated,
},
{
title: $t('locked_folder'),
icon: mdiLockOutline,
onAction: () => goto(Route.locked()),
$if: () => authManager.authenticated,
},
{
title: $t('trash'),
icon: mdiTrashCanOutline,
onAction: () => goto(Route.trash()),
$if: () => authManager.authenticated && featureFlagsManager.value.trash,
},
{
title: $t('admin.user_settings'),
icon: mdiCog,
onAction: () => goto(Route.userSettings()),
$if: () => authManager.authenticated,
},
].map((route) => ({ $if: () => authManager.authenticated, ...route }));
const utilityPages: ActionItem[] = [
{
title: $t('review_duplicates'),
icon: mdiContentDuplicate,
onAction: () => goto(Route.duplicatesUtility()),
},
{
title: $t('review_large_files'),
icon: mdiImageSizeSelectLarge,
onAction: () => goto(Route.largeFileUtility()),
},
{
title: $t('manage_geolocation'),
icon: mdiCrosshairsGps,
onAction: () => goto(Route.geolocationUtility()),
},
{
title: $t('workflows'),
icon: mdiStateMachine,
onAction: () => goto(Route.workflows()),
},
].map((route) => ({ ...route, $if: () => authManager.authenticated }));
return defaultProvider({ name: $t('page'), actions: [...userPages, ...utilityPages, ...adminPages] });
};
const getMyImmichLink = () => {