Compare commits

..

25 Commits

Author SHA1 Message Date
Yaros
801f01fc95 chore: remove hasLocal 2025-12-11 20:27:05 +01:00
Yaros
8c1c11b9df fix(mobile): local delete missing from album sheet 2025-12-10 19:28:12 +01:00
Alex
6d499c782a chore: update ui lib (#24483) 2025-12-09 17:27:01 -06:00
idubnori
7af99b8606 feat(mobile): move top bar buttons into kebabu menu in AssetViewer (#24461)
* chore(mobile):  i18n: "open_asset_info" in viewer kebab menu

* feat(mobile): move some top buttons into kebabu menu

* refactor(mobile): viewer kebab menu to use context-based button generation

* feat(mobile): refactor action button and kebab menu to use ConsumerWidget for improved state management

* feat(mobile): pass original theme to ViewerKebabMenu for consistent styling

* chore: styling

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-12-09 18:26:28 +00:00
Arnau Mora
01e39277e0 feat(mobile): Localized backup upload details page (#21136)
* Localized backup details page

# Conflicts:
#	i18n/en.json

* Format

* format fix

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-12-09 11:23:01 -06:00
Yaros
06e79703da fix(mobile): timeline bottom padding on selection (#24480) 2025-12-09 09:19:41 -06:00
Yaros
c360781565 fix(mobile): fix overflow text in backup card (#24448)
* fix(mobile): fix overflow text in backup card

* refactor: use intrinsicheight

* chore: fix spelling of entitycounttile
2025-12-09 09:03:29 -06:00
idubnori
287f6d5c94 fix(mobile): buttons inside AddActionButton color is the same as background color (#24460)
* fix: icon & text color in AddActionButton

* fix: use Divider
2025-12-08 14:29:31 -06:00
Simon Kubiak
fe9125a3d1 fix(web): [album table view] long album title overflows table row (#24450)
fix(web): long album title overflows vertically on album page in table view
2025-12-08 15:35:58 +00:00
Yaros
8b31936bb6 fix(mobile): cannot create album while name field is focused (#24449)
fix(mobile): create album disabled when focused
2025-12-08 09:33:01 -06:00
Sergey Katsubo
19958dfd83 fix(server): building docker image for different platforms on the same host (#24459)
Fix building docker image for different platforms on the same host

Use per-platform mise cache to avoid 'sh: 1: extism-js: not found'
This happens due to re-using cached installed binary for another platform
2025-12-08 09:15:43 -06:00
Alex
1e1cf0d1fe fix: build iOS fastlane installation (#24408) 2025-12-06 14:55:53 -06:00
Min Idzelis
879e0ea131 fix: thumbnail doesnt send mouseLeave events properly (#24423) 2025-12-06 21:52:06 +01:00
Sergey Katsubo
42136f9091 fix(server): update exiftool-vendored to v34 for more robust metadata extraction (#24424) 2025-12-06 14:45:59 -06:00
Harrison
1109c32891 fix(docs): websockets in nginx example (#24411)
Co-authored-by: Harrison <frith.harry@gmail.com>
2025-12-06 16:28:12 +00:00
idubnori
3c80049192 chore(mobile): add kebabu menu in asset viewer (#24387)
* feat(mobile): implement viewer kebab menu with about option

* feat: revert exisitng buttons, adjust label name

* unify MenuAnchor usage

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-12-05 19:51:59 +00:00
Hai Sullivan
8f1669efbe chore(mobile): smoother UI experience for iOS devices (#24397)
allows the tab pages to use the standard Material page transition during push/pop navigation
2025-12-05 11:02:04 -06:00
Robert Schäfer
146bf65d02 refactor(dev): remove ulimits for rootless docker (#24393)
Description
-----------

When I follow the [developer setup](https://docs.immich.app/developer/setup) I run into a permission error using rootless docker. A while ago I asked on Discord in [#contributing](https://discord.com/channels/979116623879368755/1071165397228855327/1442974448776122592) about these ulimits.

I suggest to remove the `ulimits` altogether. It seems that @ItalyPaleAle has left the setting just hoping that it could help somebody in the future. See the [PR description](https://github.com/immich-app/immich/pull/4556).

How Has This Been Tested?
-------------------------

Using rootless docker:

```
$ docker context ls
NAME         DESCRIPTION                               DOCKER ENDPOINT                     ERROR
default                                                unix:///var/run/docker.sock
rootless *                                             unix:///run/user/1000/docker.sock
```

Running `make` will fail because of permission errors:
```
$  docker compose -f ./docker/docker-compose.dev.yml up --remove-orphans
...
Error response from daemon: failed to create task for container: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: error during container init: error setting rlimits for ready process: error setting rlimit type 7: operation not permitted
```

On my machine I have the following hard limit for "Maximum number of open file descriptors":
```
$ ulimit -nH
524288
```

I can confirm that the permission error is caused by the security restrictions of the operating system mentioned above:

Changing `docker/docker-compose.dev.yml` like ..

```
    ulimits:
      nofile:
        soft: 524289
        hard: 524289
```

.. will lead to a permission error whereas this ..

```
    ulimits:
      nofile:
        soft: 524288
        hard: 524288
```

.. starts fine.

Apparently the defaults for these limits are coming from [systemd](26b2085d54/man/systemd.exec.xml (L1122)) which is used on nearly every linux distribution. So my assumption is that almost any linux user who uses rootless docker will run into a permission error when starting the development setup.

Checklist:
----------

- [x] I have performed a self-review of my own code
- [x] I have made corresponding changes to the documentation if applicable
- [x] I have no unrelated changes in the PR.
- [ ] I have confirmed that any new dependencies are strictly necessary.
- [ ] I have written tests for new code (if applicable)
- [ ] I have followed naming conventions/patterns in the surrounding code
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
2025-12-05 09:26:20 -05:00
Daniel Dietzler
75a7c9c06c feat: sql tools array as default value (#24389) 2025-12-04 12:54:20 -05:00
Daniel Dietzler
ae8f5a6673 fix: prettier (#24386) 2025-12-04 16:10:42 +00:00
Jason Rasmussen
31f2c7b505 feat: header context menu (#24374) 2025-12-04 11:09:38 -05:00
Yaros
ba6687dde9 feat(web): search type selection dropdown (#24091)
* feat(web): search type selection dropdown

* chore: implement suggestions

* lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-12-04 04:10:12 +00:00
shenlong
bbba1bfe8c fix: use adjustment time in iOS for hash reset (#24047)
* use adjustment time in iOS for hash reset

* migration

* fix equals check

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-12-03 21:15:58 -06:00
Robert Schäfer
4be9a5ebf8 fix(docs): obsolete docs about rootless docker (#24376)
Description
-----------

The documentation lies about comments in `docker/docker-compose.dev.yml`.

Reason: in 689c6aa276 these docs were added
but the comments in this file are removed in
b9e2590752 and the docs weren't updated.

How Has This Been Tested?
-------------------------
```
$ git log -S rootless

commit b9e2590752
Author: Jason Rasmussen <jason@rasm.me>
Date:   Tue Sep 16 12:48:44 2025 -0400

    chore: simplify (#22082)

commit 689c6aa276
Author: Rudolf Horváth <R-Rudolf@users.noreply.github.com>
Date:   Thu Nov 21 13:25:45 2024 +0100

    docs: add developer notes about rootless docker setup (#13250)
```

Checklist:
----------

- [x] I have performed a self-review of my own code
- [x] I have made corresponding changes to the documentation if applicable
- [x] I have no unrelated changes in the PR.
- [ ] I have confirmed that any new dependencies are strictly necessary.
- [ ] I have written tests for new code (if applicable)
- [ ] I have followed naming conventions/patterns in the surrounding code
- [ ] All code in `src/services/` uses repositories implementations for database calls, filesystem operations, etc.
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services/`)
2025-12-03 18:34:08 -06:00
Omar I
d41921247b fix(web): Add minimum content size to logo for consistent visual on small screens (#24372) 2025-12-03 21:35:48 +00:00
62 changed files with 9430 additions and 394 deletions

View File

@@ -4,6 +4,6 @@
"format:fix": "prettier --write ." "format:fix": "prettier --write ."
}, },
"devDependencies": { "devDependencies": {
"prettier": "^3.5.3" "prettier": "^3.7.4"
} }
} }

View File

@@ -222,6 +222,7 @@ jobs:
uses: ruby/setup-ruby@v1 uses: ruby/setup-ruby@v1
with: with:
ruby-version: '3.3' ruby-version: '3.3'
bundler-cache: true
working-directory: ./mobile/ios working-directory: ./mobile/ios
- name: Install CocoaPods dependencies - name: Install CocoaPods dependencies
@@ -229,13 +230,6 @@ jobs:
run: | run: |
pod install pod install
- name: Install Fastlane
working-directory: ./mobile/ios
run: |
gem install bundler
bundle config set --local path 'vendor/bundle'
bundle install
- name: Create API Key - name: Create API Key
env: env:
API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }} API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}

View File

@@ -31,7 +31,7 @@
"eslint-plugin-unicorn": "^62.0.0", "eslint-plugin-unicorn": "^62.0.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"prettier": "^3.2.5", "prettier": "^3.7.4",
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"typescript-eslint": "^8.28.0", "typescript-eslint": "^8.28.0",

View File

@@ -58,10 +58,6 @@ services:
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://docs.immich.app IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://docs.immich.app
IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides IMMICH_THIRD_PARTY_SUPPORT_URL: https://docs.immich.app/community-guides
ulimits:
nofile:
soft: 1048576
hard: 1048576
ports: ports:
- 9230:9230 - 9230:9230
- 9231:9231 - 9231:9231
@@ -100,10 +96,6 @@ services:
- app-node_modules:/usr/src/app/node_modules - app-node_modules:/usr/src/app/node_modules
- sveltekit:/usr/src/app/web/.svelte-kit - sveltekit:/usr/src/app/web/.svelte-kit
- coverage:/usr/src/app/web/coverage - coverage:/usr/src/app/web/coverage
ulimits:
nofile:
soft: 1048576
hard: 1048576
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
immich-server: immich-server:

View File

@@ -32,8 +32,6 @@ server {
# enable websockets: http://nginx.org/en/docs/http/websocket.html # enable websockets: http://nginx.org/en/docs/http/websocket.html
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_redirect off; proxy_redirect off;
# set timeout # set timeout
@@ -43,6 +41,8 @@ server {
location / { location / {
proxy_pass http://<backend_url>:2283; proxy_pass http://<backend_url>:2283;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
} }
# useful when using Let's Encrypt http-01 challenge # useful when using Let's Encrypt http-01 challenge

View File

@@ -48,7 +48,6 @@ You can access the web from `http://your-machine-ip:3000` or `http://localhost:3
**Notes:** **Notes:**
- The "web" development container runs with uid 1000. If that uid does not have read/write permissions on the mounted volumes, you may encounter errors - The "web" development container runs with uid 1000. If that uid does not have read/write permissions on the mounted volumes, you may encounter errors
- In case of rootless docker setup, you need to use root within the container, otherwise you will encounter read/write permission related errors, see comments in `docker/docker-compose.dev.yml`.
#### Connect web to a remote backend #### Connect web to a remote backend

View File

@@ -38,7 +38,7 @@
"@docusaurus/module-type-aliases": "~3.9.0", "@docusaurus/module-type-aliases": "~3.9.0",
"@docusaurus/tsconfig": "^3.7.0", "@docusaurus/tsconfig": "^3.7.0",
"@docusaurus/types": "^3.7.0", "@docusaurus/types": "^3.7.0",
"prettier": "^3.2.4", "prettier": "^3.7.4",
"typescript": "^5.1.6" "typescript": "^5.1.6"
}, },
"browserslist": { "browserslist": {

View File

@@ -36,14 +36,14 @@
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^62.0.0", "eslint-plugin-unicorn": "^62.0.0",
"exiftool-vendored": "^33.0.0", "exiftool-vendored": "^34.0.0",
"globals": "^16.0.0", "globals": "^16.0.0",
"jose": "^5.6.3", "jose": "^5.6.3",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"oidc-provider": "^9.0.0", "oidc-provider": "^9.0.0",
"pg": "^8.11.3", "pg": "^8.11.3",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"prettier": "^3.2.5", "prettier": "^3.7.4",
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"sharp": "^0.34.5", "sharp": "^0.34.5",
"socket.io-client": "^4.7.4", "socket.io-client": "^4.7.4",

View File

@@ -652,6 +652,7 @@
"backup_options_page_title": "Backup options", "backup_options_page_title": "Backup options",
"backup_setting_subtitle": "Manage background and foreground upload settings", "backup_setting_subtitle": "Manage background and foreground upload settings",
"backup_settings_subtitle": "Manage upload settings", "backup_settings_subtitle": "Manage upload settings",
"backup_upload_details_page_more_details": "Tap for more details",
"backward": "Backward", "backward": "Backward",
"biometric_auth_enabled": "Biometric authentication enabled", "biometric_auth_enabled": "Biometric authentication enabled",
"biometric_locked_out": "You are locked out of biometric authentication", "biometric_locked_out": "You are locked out of biometric authentication",
@@ -718,6 +719,7 @@
"check_corrupt_asset_backup_button": "Perform check", "check_corrupt_asset_backup_button": "Perform check",
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.", "check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
"check_logs": "Check Logs", "check_logs": "Check Logs",
"checksum": "Checksum",
"choose_matching_people_to_merge": "Choose matching people to merge", "choose_matching_people_to_merge": "Choose matching people to merge",
"city": "City", "city": "City",
"clear": "Clear", "clear": "Clear",
@@ -1166,6 +1168,7 @@
"header_settings_header_name_input": "Header name", "header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value", "header_settings_header_value_input": "Header value",
"headers_settings_tile_title": "Custom proxy headers", "headers_settings_tile_title": "Custom proxy headers",
"height": "Height",
"hi_user": "Hi {name} ({email})", "hi_user": "Hi {name} ({email})",
"hide_all_people": "Hide all people", "hide_all_people": "Hide all people",
"hide_gallery": "Hide gallery", "hide_gallery": "Hide gallery",
@@ -1288,6 +1291,7 @@
"local": "Local", "local": "Local",
"local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server", "local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server",
"local_assets": "Local Assets", "local_assets": "Local Assets",
"local_id": "Local ID",
"local_media_summary": "Local Media Summary", "local_media_summary": "Local Media Summary",
"local_network": "Local network", "local_network": "Local network",
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network", "local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
@@ -2218,6 +2222,7 @@
"week": "Week", "week": "Week",
"welcome": "Welcome", "welcome": "Welcome",
"welcome_to_immich": "Welcome to Immich", "welcome_to_immich": "Welcome to Immich",
"width": "Width",
"wifi_name": "Wi-Fi Name", "wifi_name": "Wi-Fi Name",
"workflow": "Workflow", "workflow": "Workflow",
"wrong_pin_code": "Wrong PIN code", "wrong_pin_code": "Wrong PIN code",

View File

@@ -89,7 +89,10 @@ data class PlatformAsset (
val height: Long? = null, val height: Long? = null,
val durationInSeconds: Long, val durationInSeconds: Long,
val orientation: Long, val orientation: Long,
val isFavorite: Boolean val isFavorite: Boolean,
val adjustmentTime: Long? = null,
val latitude: Double? = null,
val longitude: Double? = null
) )
{ {
companion object { companion object {
@@ -104,7 +107,10 @@ data class PlatformAsset (
val durationInSeconds = pigeonVar_list[7] as Long val durationInSeconds = pigeonVar_list[7] as Long
val orientation = pigeonVar_list[8] as Long val orientation = pigeonVar_list[8] as Long
val isFavorite = pigeonVar_list[9] as Boolean val isFavorite = pigeonVar_list[9] as Boolean
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite) val adjustmentTime = pigeonVar_list[10] as Long?
val latitude = pigeonVar_list[11] as Double?
val longitude = pigeonVar_list[12] as Double?
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude)
} }
} }
fun toList(): List<Any?> { fun toList(): List<Any?> {
@@ -119,6 +125,9 @@ data class PlatformAsset (
durationInSeconds, durationInSeconds,
orientation, orientation,
isFavorite, isFavorite,
adjustmentTime,
latitude,
longitude,
) )
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

View File

@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
import android.content.ContentUris import android.content.ContentUris
import android.content.Context import android.content.Context
import android.database.Cursor import android.database.Cursor
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.MediaStore import android.provider.MediaStore
import android.util.Base64 import android.util.Base64

File diff suppressed because one or more lines are too long

View File

@@ -140,6 +140,9 @@ struct PlatformAsset: Hashable {
var durationInSeconds: Int64 var durationInSeconds: Int64
var orientation: Int64 var orientation: Int64
var isFavorite: Bool var isFavorite: Bool
var adjustmentTime: Int64? = nil
var latitude: Double? = nil
var longitude: Double? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase // swift-format-ignore: AlwaysUseLowerCamelCase
@@ -154,6 +157,9 @@ struct PlatformAsset: Hashable {
let durationInSeconds = pigeonVar_list[7] as! Int64 let durationInSeconds = pigeonVar_list[7] as! Int64
let orientation = pigeonVar_list[8] as! Int64 let orientation = pigeonVar_list[8] as! Int64
let isFavorite = pigeonVar_list[9] as! Bool let isFavorite = pigeonVar_list[9] as! Bool
let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10])
let latitude: Double? = nilOrValue(pigeonVar_list[11])
let longitude: Double? = nilOrValue(pigeonVar_list[12])
return PlatformAsset( return PlatformAsset(
id: id, id: id,
@@ -165,7 +171,10 @@ struct PlatformAsset: Hashable {
height: height, height: height,
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds,
orientation: orientation, orientation: orientation,
isFavorite: isFavorite isFavorite: isFavorite,
adjustmentTime: adjustmentTime,
latitude: latitude,
longitude: longitude
) )
} }
func toList() -> [Any?] { func toList() -> [Any?] {
@@ -180,6 +189,9 @@ struct PlatformAsset: Hashable {
durationInSeconds, durationInSeconds,
orientation, orientation,
isFavorite, isFavorite,
adjustmentTime,
latitude,
longitude,
] ]
} }
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool { static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {

View File

@@ -12,7 +12,10 @@ extension PHAsset {
height: Int64(pixelHeight), height: Int64(pixelHeight),
durationInSeconds: Int64(duration), durationInSeconds: Int64(duration),
orientation: 0, orientation: 0,
isFavorite: isFavorite isFavorite: isFavorite,
adjustmentTime: adjustmentTimestamp,
latitude: location?.coordinate.latitude,
longitude: location?.coordinate.longitude
) )
} }
@@ -23,6 +26,13 @@ extension PHAsset {
var filename: String? { var filename: String? {
return value(forKey: "filename") as? String return value(forKey: "filename") as? String
} }
var adjustmentTimestamp: Int64? {
if let date = value(forKey: "adjustmentTimestamp") as? Date {
return Int64(date.timeIntervalSince1970)
}
return nil
}
// This method is expected to be slow as it goes through the asset resources to fetch the originalFilename // This method is expected to be slow as it goes through the asset resources to fetch the originalFilename
var originalFilename: String? { var originalFilename: String? {

View File

@@ -5,6 +5,10 @@ class LocalAsset extends BaseAsset {
final String? remoteAssetId; final String? remoteAssetId;
final int orientation; final int orientation;
final DateTime? adjustmentTime;
final double? latitude;
final double? longitude;
const LocalAsset({ const LocalAsset({
required this.id, required this.id,
String? remoteId, String? remoteId,
@@ -19,6 +23,9 @@ class LocalAsset extends BaseAsset {
super.isFavorite = false, super.isFavorite = false,
super.livePhotoVideoId, super.livePhotoVideoId,
this.orientation = 0, this.orientation = 0,
this.adjustmentTime,
this.latitude,
this.longitude,
}) : remoteAssetId = remoteId; }) : remoteAssetId = remoteId;
@override @override
@@ -33,6 +40,8 @@ class LocalAsset extends BaseAsset {
@override @override
String get heroTag => '${id}_${remoteId ?? checksum}'; String get heroTag => '${id}_${remoteId ?? checksum}';
bool get hasCoordinates => latitude != null && longitude != null && latitude != 0 && longitude != 0;
@override @override
String toString() { String toString() {
return '''LocalAsset { return '''LocalAsset {
@@ -47,6 +56,9 @@ class LocalAsset extends BaseAsset {
remoteId: ${remoteId ?? "<NA>"} remoteId: ${remoteId ?? "<NA>"}
isFavorite: $isFavorite, isFavorite: $isFavorite,
orientation: $orientation, orientation: $orientation,
adjustmentTime: $adjustmentTime,
latitude: ${latitude ?? "<NA>"},
longitude: ${longitude ?? "<NA>"},
}'''; }''';
} }
@@ -55,11 +67,23 @@ class LocalAsset extends BaseAsset {
bool operator ==(Object other) { bool operator ==(Object other) {
if (other is! LocalAsset) return false; if (other is! LocalAsset) return false;
if (identical(this, other)) return true; if (identical(this, other)) return true;
return super == other && id == other.id && orientation == other.orientation; return super == other &&
id == other.id &&
orientation == other.orientation &&
adjustmentTime == other.adjustmentTime &&
latitude == other.latitude &&
longitude == other.longitude;
} }
@override @override
int get hashCode => super.hashCode ^ id.hashCode ^ remoteId.hashCode ^ orientation.hashCode; int get hashCode =>
super.hashCode ^
id.hashCode ^
remoteId.hashCode ^
orientation.hashCode ^
adjustmentTime.hashCode ^
latitude.hashCode ^
longitude.hashCode;
LocalAsset copyWith({ LocalAsset copyWith({
String? id, String? id,
@@ -74,6 +98,9 @@ class LocalAsset extends BaseAsset {
int? durationInSeconds, int? durationInSeconds,
bool? isFavorite, bool? isFavorite,
int? orientation, int? orientation,
DateTime? adjustmentTime,
double? latitude,
double? longitude,
}) { }) {
return LocalAsset( return LocalAsset(
id: id ?? this.id, id: id ?? this.id,
@@ -88,6 +115,9 @@ class LocalAsset extends BaseAsset {
durationInSeconds: durationInSeconds ?? this.durationInSeconds, durationInSeconds: durationInSeconds ?? this.durationInSeconds,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation, orientation: orientation ?? this.orientation,
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
); );
} }
} }

View File

@@ -286,11 +286,23 @@ class LocalSyncService {
} }
bool _assetsEqual(LocalAsset a, LocalAsset b) { bool _assetsEqual(LocalAsset a, LocalAsset b) {
return a.updatedAt.isAtSameMomentAs(b.updatedAt) && if (CurrentPlatform.isAndroid) {
return a.updatedAt.isAtSameMomentAs(b.updatedAt) &&
a.createdAt.isAtSameMomentAs(b.createdAt) &&
a.width == b.width &&
a.height == b.height &&
a.durationInSeconds == b.durationInSeconds;
}
final firstAdjustment = a.adjustmentTime?.millisecondsSinceEpoch ?? 0;
final secondAdjustment = b.adjustmentTime?.millisecondsSinceEpoch ?? 0;
return firstAdjustment == secondAdjustment &&
a.createdAt.isAtSameMomentAs(b.createdAt) && a.createdAt.isAtSameMomentAs(b.createdAt) &&
a.width == b.width && a.width == b.width &&
a.height == b.height && a.height == b.height &&
a.durationInSeconds == b.durationInSeconds; a.durationInSeconds == b.durationInSeconds &&
a.latitude == b.latitude &&
a.longitude == b.longitude;
} }
bool _albumsEqual(LocalAlbum a, LocalAlbum b) { bool _albumsEqual(LocalAlbum a, LocalAlbum b) {
@@ -376,5 +388,8 @@ extension PlatformToLocalAsset on PlatformAsset {
durationInSeconds: durationInSeconds, durationInSeconds: durationInSeconds,
isFavorite: isFavorite, isFavorite: isFavorite,
orientation: orientation, orientation: orientation,
adjustmentTime: tryFromSecondsSinceEpoch(adjustmentTime, isUtc: true),
latitude: latitude,
longitude: longitude,
); );
} }

View File

@@ -16,6 +16,12 @@ class LocalAssetEntity extends Table with DriftDefaultsMixin, AssetEntityMixin {
IntColumn get orientation => integer().withDefault(const Constant(0))(); IntColumn get orientation => integer().withDefault(const Constant(0))();
DateTimeColumn get adjustmentTime => dateTime().nullable()();
RealColumn get latitude => real().nullable()();
RealColumn get longitude => real().nullable()();
@override @override
Set<Column> get primaryKey => {id}; Set<Column> get primaryKey => {id};
} }
@@ -34,5 +40,8 @@ extension LocalAssetEntityDataDomainExtension on LocalAssetEntityData {
width: width, width: width,
remoteId: remoteId, remoteId: remoteId,
orientation: orientation, orientation: orientation,
adjustmentTime: adjustmentTime,
latitude: latitude,
longitude: longitude,
); );
} }

View File

@@ -21,6 +21,9 @@ typedef $$LocalAssetEntityTableCreateCompanionBuilder =
i0.Value<String?> checksum, i0.Value<String?> checksum,
i0.Value<bool> isFavorite, i0.Value<bool> isFavorite,
i0.Value<int> orientation, i0.Value<int> orientation,
i0.Value<DateTime?> adjustmentTime,
i0.Value<double?> latitude,
i0.Value<double?> longitude,
}); });
typedef $$LocalAssetEntityTableUpdateCompanionBuilder = typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i1.LocalAssetEntityCompanion Function({ i1.LocalAssetEntityCompanion Function({
@@ -35,6 +38,9 @@ typedef $$LocalAssetEntityTableUpdateCompanionBuilder =
i0.Value<String?> checksum, i0.Value<String?> checksum,
i0.Value<bool> isFavorite, i0.Value<bool> isFavorite,
i0.Value<int> orientation, i0.Value<int> orientation,
i0.Value<DateTime?> adjustmentTime,
i0.Value<double?> latitude,
i0.Value<double?> longitude,
}); });
class $$LocalAssetEntityTableFilterComposer class $$LocalAssetEntityTableFilterComposer
@@ -101,6 +107,21 @@ class $$LocalAssetEntityTableFilterComposer
column: $table.orientation, column: $table.orientation,
builder: (column) => i0.ColumnFilters(column), builder: (column) => i0.ColumnFilters(column),
); );
i0.ColumnFilters<DateTime> get adjustmentTime => $composableBuilder(
column: $table.adjustmentTime,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<double> get latitude => $composableBuilder(
column: $table.latitude,
builder: (column) => i0.ColumnFilters(column),
);
i0.ColumnFilters<double> get longitude => $composableBuilder(
column: $table.longitude,
builder: (column) => i0.ColumnFilters(column),
);
} }
class $$LocalAssetEntityTableOrderingComposer class $$LocalAssetEntityTableOrderingComposer
@@ -166,6 +187,21 @@ class $$LocalAssetEntityTableOrderingComposer
column: $table.orientation, column: $table.orientation,
builder: (column) => i0.ColumnOrderings(column), builder: (column) => i0.ColumnOrderings(column),
); );
i0.ColumnOrderings<DateTime> get adjustmentTime => $composableBuilder(
column: $table.adjustmentTime,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<double> get latitude => $composableBuilder(
column: $table.latitude,
builder: (column) => i0.ColumnOrderings(column),
);
i0.ColumnOrderings<double> get longitude => $composableBuilder(
column: $table.longitude,
builder: (column) => i0.ColumnOrderings(column),
);
} }
class $$LocalAssetEntityTableAnnotationComposer class $$LocalAssetEntityTableAnnotationComposer
@@ -215,6 +251,17 @@ class $$LocalAssetEntityTableAnnotationComposer
column: $table.orientation, column: $table.orientation,
builder: (column) => column, builder: (column) => column,
); );
i0.GeneratedColumn<DateTime> get adjustmentTime => $composableBuilder(
column: $table.adjustmentTime,
builder: (column) => column,
);
i0.GeneratedColumn<double> get latitude =>
$composableBuilder(column: $table.latitude, builder: (column) => column);
i0.GeneratedColumn<double> get longitude =>
$composableBuilder(column: $table.longitude, builder: (column) => column);
} }
class $$LocalAssetEntityTableTableManager class $$LocalAssetEntityTableTableManager
@@ -268,6 +315,9 @@ class $$LocalAssetEntityTableTableManager
i0.Value<String?> checksum = const i0.Value.absent(), i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(), i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<int> orientation = const i0.Value.absent(), i0.Value<int> orientation = const i0.Value.absent(),
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
i0.Value<double?> latitude = const i0.Value.absent(),
i0.Value<double?> longitude = const i0.Value.absent(),
}) => i1.LocalAssetEntityCompanion( }) => i1.LocalAssetEntityCompanion(
name: name, name: name,
type: type, type: type,
@@ -280,6 +330,9 @@ class $$LocalAssetEntityTableTableManager
checksum: checksum, checksum: checksum,
isFavorite: isFavorite, isFavorite: isFavorite,
orientation: orientation, orientation: orientation,
adjustmentTime: adjustmentTime,
latitude: latitude,
longitude: longitude,
), ),
createCompanionCallback: createCompanionCallback:
({ ({
@@ -294,6 +347,9 @@ class $$LocalAssetEntityTableTableManager
i0.Value<String?> checksum = const i0.Value.absent(), i0.Value<String?> checksum = const i0.Value.absent(),
i0.Value<bool> isFavorite = const i0.Value.absent(), i0.Value<bool> isFavorite = const i0.Value.absent(),
i0.Value<int> orientation = const i0.Value.absent(), i0.Value<int> orientation = const i0.Value.absent(),
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
i0.Value<double?> latitude = const i0.Value.absent(),
i0.Value<double?> longitude = const i0.Value.absent(),
}) => i1.LocalAssetEntityCompanion.insert( }) => i1.LocalAssetEntityCompanion.insert(
name: name, name: name,
type: type, type: type,
@@ -306,6 +362,9 @@ class $$LocalAssetEntityTableTableManager
checksum: checksum, checksum: checksum,
isFavorite: isFavorite, isFavorite: isFavorite,
orientation: orientation, orientation: orientation,
adjustmentTime: adjustmentTime,
latitude: latitude,
longitude: longitude,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), i0.BaseReferences(db, table, e))) .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
@@ -473,6 +532,39 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
requiredDuringInsert: false, requiredDuringInsert: false,
defaultValue: const i4.Constant(0), defaultValue: const i4.Constant(0),
); );
static const i0.VerificationMeta _adjustmentTimeMeta =
const i0.VerificationMeta('adjustmentTime');
@override
late final i0.GeneratedColumn<DateTime> adjustmentTime =
i0.GeneratedColumn<DateTime>(
'adjustment_time',
aliasedName,
true,
type: i0.DriftSqlType.dateTime,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _latitudeMeta = const i0.VerificationMeta(
'latitude',
);
@override
late final i0.GeneratedColumn<double> latitude = i0.GeneratedColumn<double>(
'latitude',
aliasedName,
true,
type: i0.DriftSqlType.double,
requiredDuringInsert: false,
);
static const i0.VerificationMeta _longitudeMeta = const i0.VerificationMeta(
'longitude',
);
@override
late final i0.GeneratedColumn<double> longitude = i0.GeneratedColumn<double>(
'longitude',
aliasedName,
true,
type: i0.DriftSqlType.double,
requiredDuringInsert: false,
);
@override @override
List<i0.GeneratedColumn> get $columns => [ List<i0.GeneratedColumn> get $columns => [
name, name,
@@ -486,6 +578,9 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
checksum, checksum,
isFavorite, isFavorite,
orientation, orientation,
adjustmentTime,
latitude,
longitude,
]; ];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@@ -566,6 +661,27 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
), ),
); );
} }
if (data.containsKey('adjustment_time')) {
context.handle(
_adjustmentTimeMeta,
adjustmentTime.isAcceptableOrUnknown(
data['adjustment_time']!,
_adjustmentTimeMeta,
),
);
}
if (data.containsKey('latitude')) {
context.handle(
_latitudeMeta,
latitude.isAcceptableOrUnknown(data['latitude']!, _latitudeMeta),
);
}
if (data.containsKey('longitude')) {
context.handle(
_longitudeMeta,
longitude.isAcceptableOrUnknown(data['longitude']!, _longitudeMeta),
);
}
return context; return context;
} }
@@ -624,6 +740,18 @@ class $LocalAssetEntityTable extends i3.LocalAssetEntity
i0.DriftSqlType.int, i0.DriftSqlType.int,
data['${effectivePrefix}orientation'], data['${effectivePrefix}orientation'],
)!, )!,
adjustmentTime: attachedDatabase.typeMapping.read(
i0.DriftSqlType.dateTime,
data['${effectivePrefix}adjustment_time'],
),
latitude: attachedDatabase.typeMapping.read(
i0.DriftSqlType.double,
data['${effectivePrefix}latitude'],
),
longitude: attachedDatabase.typeMapping.read(
i0.DriftSqlType.double,
data['${effectivePrefix}longitude'],
),
); );
} }
@@ -653,6 +781,9 @@ class LocalAssetEntityData extends i0.DataClass
final String? checksum; final String? checksum;
final bool isFavorite; final bool isFavorite;
final int orientation; final int orientation;
final DateTime? adjustmentTime;
final double? latitude;
final double? longitude;
const LocalAssetEntityData({ const LocalAssetEntityData({
required this.name, required this.name,
required this.type, required this.type,
@@ -665,6 +796,9 @@ class LocalAssetEntityData extends i0.DataClass
this.checksum, this.checksum,
required this.isFavorite, required this.isFavorite,
required this.orientation, required this.orientation,
this.adjustmentTime,
this.latitude,
this.longitude,
}); });
@override @override
Map<String, i0.Expression> toColumns(bool nullToAbsent) { Map<String, i0.Expression> toColumns(bool nullToAbsent) {
@@ -692,6 +826,15 @@ class LocalAssetEntityData extends i0.DataClass
} }
map['is_favorite'] = i0.Variable<bool>(isFavorite); map['is_favorite'] = i0.Variable<bool>(isFavorite);
map['orientation'] = i0.Variable<int>(orientation); map['orientation'] = i0.Variable<int>(orientation);
if (!nullToAbsent || adjustmentTime != null) {
map['adjustment_time'] = i0.Variable<DateTime>(adjustmentTime);
}
if (!nullToAbsent || latitude != null) {
map['latitude'] = i0.Variable<double>(latitude);
}
if (!nullToAbsent || longitude != null) {
map['longitude'] = i0.Variable<double>(longitude);
}
return map; return map;
} }
@@ -714,6 +857,9 @@ class LocalAssetEntityData extends i0.DataClass
checksum: serializer.fromJson<String?>(json['checksum']), checksum: serializer.fromJson<String?>(json['checksum']),
isFavorite: serializer.fromJson<bool>(json['isFavorite']), isFavorite: serializer.fromJson<bool>(json['isFavorite']),
orientation: serializer.fromJson<int>(json['orientation']), orientation: serializer.fromJson<int>(json['orientation']),
adjustmentTime: serializer.fromJson<DateTime?>(json['adjustmentTime']),
latitude: serializer.fromJson<double?>(json['latitude']),
longitude: serializer.fromJson<double?>(json['longitude']),
); );
} }
@override @override
@@ -733,6 +879,9 @@ class LocalAssetEntityData extends i0.DataClass
'checksum': serializer.toJson<String?>(checksum), 'checksum': serializer.toJson<String?>(checksum),
'isFavorite': serializer.toJson<bool>(isFavorite), 'isFavorite': serializer.toJson<bool>(isFavorite),
'orientation': serializer.toJson<int>(orientation), 'orientation': serializer.toJson<int>(orientation),
'adjustmentTime': serializer.toJson<DateTime?>(adjustmentTime),
'latitude': serializer.toJson<double?>(latitude),
'longitude': serializer.toJson<double?>(longitude),
}; };
} }
@@ -748,6 +897,9 @@ class LocalAssetEntityData extends i0.DataClass
i0.Value<String?> checksum = const i0.Value.absent(), i0.Value<String?> checksum = const i0.Value.absent(),
bool? isFavorite, bool? isFavorite,
int? orientation, int? orientation,
i0.Value<DateTime?> adjustmentTime = const i0.Value.absent(),
i0.Value<double?> latitude = const i0.Value.absent(),
i0.Value<double?> longitude = const i0.Value.absent(),
}) => i1.LocalAssetEntityData( }) => i1.LocalAssetEntityData(
name: name ?? this.name, name: name ?? this.name,
type: type ?? this.type, type: type ?? this.type,
@@ -762,6 +914,11 @@ class LocalAssetEntityData extends i0.DataClass
checksum: checksum.present ? checksum.value : this.checksum, checksum: checksum.present ? checksum.value : this.checksum,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation, orientation: orientation ?? this.orientation,
adjustmentTime: adjustmentTime.present
? adjustmentTime.value
: this.adjustmentTime,
latitude: latitude.present ? latitude.value : this.latitude,
longitude: longitude.present ? longitude.value : this.longitude,
); );
LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) { LocalAssetEntityData copyWithCompanion(i1.LocalAssetEntityCompanion data) {
return LocalAssetEntityData( return LocalAssetEntityData(
@@ -782,6 +939,11 @@ class LocalAssetEntityData extends i0.DataClass
orientation: data.orientation.present orientation: data.orientation.present
? data.orientation.value ? data.orientation.value
: this.orientation, : this.orientation,
adjustmentTime: data.adjustmentTime.present
? data.adjustmentTime.value
: this.adjustmentTime,
latitude: data.latitude.present ? data.latitude.value : this.latitude,
longitude: data.longitude.present ? data.longitude.value : this.longitude,
); );
} }
@@ -798,7 +960,10 @@ class LocalAssetEntityData extends i0.DataClass
..write('id: $id, ') ..write('id: $id, ')
..write('checksum: $checksum, ') ..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ') ..write('isFavorite: $isFavorite, ')
..write('orientation: $orientation') ..write('orientation: $orientation, ')
..write('adjustmentTime: $adjustmentTime, ')
..write('latitude: $latitude, ')
..write('longitude: $longitude')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@@ -816,6 +981,9 @@ class LocalAssetEntityData extends i0.DataClass
checksum, checksum,
isFavorite, isFavorite,
orientation, orientation,
adjustmentTime,
latitude,
longitude,
); );
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@@ -831,7 +999,10 @@ class LocalAssetEntityData extends i0.DataClass
other.id == this.id && other.id == this.id &&
other.checksum == this.checksum && other.checksum == this.checksum &&
other.isFavorite == this.isFavorite && other.isFavorite == this.isFavorite &&
other.orientation == this.orientation); other.orientation == this.orientation &&
other.adjustmentTime == this.adjustmentTime &&
other.latitude == this.latitude &&
other.longitude == this.longitude);
} }
class LocalAssetEntityCompanion class LocalAssetEntityCompanion
@@ -847,6 +1018,9 @@ class LocalAssetEntityCompanion
final i0.Value<String?> checksum; final i0.Value<String?> checksum;
final i0.Value<bool> isFavorite; final i0.Value<bool> isFavorite;
final i0.Value<int> orientation; final i0.Value<int> orientation;
final i0.Value<DateTime?> adjustmentTime;
final i0.Value<double?> latitude;
final i0.Value<double?> longitude;
const LocalAssetEntityCompanion({ const LocalAssetEntityCompanion({
this.name = const i0.Value.absent(), this.name = const i0.Value.absent(),
this.type = const i0.Value.absent(), this.type = const i0.Value.absent(),
@@ -859,6 +1033,9 @@ class LocalAssetEntityCompanion
this.checksum = const i0.Value.absent(), this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(), this.orientation = const i0.Value.absent(),
this.adjustmentTime = const i0.Value.absent(),
this.latitude = const i0.Value.absent(),
this.longitude = const i0.Value.absent(),
}); });
LocalAssetEntityCompanion.insert({ LocalAssetEntityCompanion.insert({
required String name, required String name,
@@ -872,6 +1049,9 @@ class LocalAssetEntityCompanion
this.checksum = const i0.Value.absent(), this.checksum = const i0.Value.absent(),
this.isFavorite = const i0.Value.absent(), this.isFavorite = const i0.Value.absent(),
this.orientation = const i0.Value.absent(), this.orientation = const i0.Value.absent(),
this.adjustmentTime = const i0.Value.absent(),
this.latitude = const i0.Value.absent(),
this.longitude = const i0.Value.absent(),
}) : name = i0.Value(name), }) : name = i0.Value(name),
type = i0.Value(type), type = i0.Value(type),
id = i0.Value(id); id = i0.Value(id);
@@ -887,6 +1067,9 @@ class LocalAssetEntityCompanion
i0.Expression<String>? checksum, i0.Expression<String>? checksum,
i0.Expression<bool>? isFavorite, i0.Expression<bool>? isFavorite,
i0.Expression<int>? orientation, i0.Expression<int>? orientation,
i0.Expression<DateTime>? adjustmentTime,
i0.Expression<double>? latitude,
i0.Expression<double>? longitude,
}) { }) {
return i0.RawValuesInsertable({ return i0.RawValuesInsertable({
if (name != null) 'name': name, if (name != null) 'name': name,
@@ -900,6 +1083,9 @@ class LocalAssetEntityCompanion
if (checksum != null) 'checksum': checksum, if (checksum != null) 'checksum': checksum,
if (isFavorite != null) 'is_favorite': isFavorite, if (isFavorite != null) 'is_favorite': isFavorite,
if (orientation != null) 'orientation': orientation, if (orientation != null) 'orientation': orientation,
if (adjustmentTime != null) 'adjustment_time': adjustmentTime,
if (latitude != null) 'latitude': latitude,
if (longitude != null) 'longitude': longitude,
}); });
} }
@@ -915,6 +1101,9 @@ class LocalAssetEntityCompanion
i0.Value<String?>? checksum, i0.Value<String?>? checksum,
i0.Value<bool>? isFavorite, i0.Value<bool>? isFavorite,
i0.Value<int>? orientation, i0.Value<int>? orientation,
i0.Value<DateTime?>? adjustmentTime,
i0.Value<double?>? latitude,
i0.Value<double?>? longitude,
}) { }) {
return i1.LocalAssetEntityCompanion( return i1.LocalAssetEntityCompanion(
name: name ?? this.name, name: name ?? this.name,
@@ -928,6 +1117,9 @@ class LocalAssetEntityCompanion
checksum: checksum ?? this.checksum, checksum: checksum ?? this.checksum,
isFavorite: isFavorite ?? this.isFavorite, isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation, orientation: orientation ?? this.orientation,
adjustmentTime: adjustmentTime ?? this.adjustmentTime,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
); );
} }
@@ -969,6 +1161,15 @@ class LocalAssetEntityCompanion
if (orientation.present) { if (orientation.present) {
map['orientation'] = i0.Variable<int>(orientation.value); map['orientation'] = i0.Variable<int>(orientation.value);
} }
if (adjustmentTime.present) {
map['adjustment_time'] = i0.Variable<DateTime>(adjustmentTime.value);
}
if (latitude.present) {
map['latitude'] = i0.Variable<double>(latitude.value);
}
if (longitude.present) {
map['longitude'] = i0.Variable<double>(longitude.value);
}
return map; return map;
} }
@@ -985,7 +1186,10 @@ class LocalAssetEntityCompanion
..write('id: $id, ') ..write('id: $id, ')
..write('checksum: $checksum, ') ..write('checksum: $checksum, ')
..write('isFavorite: $isFavorite, ') ..write('isFavorite: $isFavorite, ')
..write('orientation: $orientation') ..write('orientation: $orientation, ')
..write('adjustmentTime: $adjustmentTime, ')
..write('latitude: $latitude, ')
..write('longitude: $longitude')
..write(')')) ..write(')'))
.toString(); .toString();
} }

View File

@@ -10,7 +10,6 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart'; import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
@@ -21,6 +20,7 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.d
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart'; import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart'; import 'package:immich_mobile/infrastructure/entities/stack.entity.dart';
import 'package:immich_mobile/infrastructure/entities/store.entity.dart'; import 'package:immich_mobile/infrastructure/entities/store.entity.dart';
import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart'; import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart'; import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
@@ -95,7 +95,7 @@ class Drift extends $Drift implements IDatabaseRepository {
} }
@override @override
int get schemaVersion => 13; int get schemaVersion => 14;
@override @override
MigrationStrategy get migration => MigrationStrategy( MigrationStrategy get migration => MigrationStrategy(
@@ -185,6 +185,11 @@ class Drift extends $Drift implements IDatabaseRepository {
await m.createIndex(v13.idxTrashedLocalAssetChecksum); await m.createIndex(v13.idxTrashedLocalAssetChecksum);
await m.createIndex(v13.idxTrashedLocalAssetAlbum); await m.createIndex(v13.idxTrashedLocalAssetAlbum);
}, },
from13To14: (m, v14) async {
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.adjustmentTime);
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.latitude);
await m.addColumn(v14.localAssetEntity, v14.localAssetEntity.longitude);
},
), ),
); );

View File

@@ -5485,6 +5485,462 @@ i1.GeneratedColumn<String> _column_95(String aliasedName) =>
false, false,
type: i1.DriftSqlType.string, type: i1.DriftSqlType.string,
); );
final class Schema14 extends i0.VersionedSchema {
Schema14({required super.database}) : super(version: 14);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
userEntity,
remoteAssetEntity,
stackEntity,
localAssetEntity,
remoteAlbumEntity,
localAlbumEntity,
localAlbumAssetEntity,
idxLocalAssetChecksum,
idxRemoteAssetOwnerChecksum,
uQRemoteAssetsOwnerChecksum,
uQRemoteAssetsOwnerLibraryChecksum,
idxRemoteAssetChecksum,
authUserEntity,
userMetadataEntity,
partnerEntity,
remoteExifEntity,
remoteAlbumAssetEntity,
remoteAlbumUserEntity,
memoryEntity,
memoryAssetEntity,
personEntity,
assetFaceEntity,
storeEntity,
trashedLocalAssetEntity,
idxLatLng,
idxTrashedLocalAssetChecksum,
idxTrashedLocalAssetAlbum,
];
late final Shape20 userEntity = Shape20(
source: i0.VersionedTable(
entityName: 'user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_84,
_column_85,
_column_91,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape17 remoteAssetEntity = Shape17(
source: i0.VersionedTable(
entityName: 'remote_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_13,
_column_14,
_column_15,
_column_16,
_column_17,
_column_18,
_column_19,
_column_20,
_column_21,
_column_86,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape3 stackEntity = Shape3(
source: i0.VersionedTable(
entityName: 'stack_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_0, _column_9, _column_5, _column_15, _column_75],
attachedDatabase: database,
),
alias: null,
);
late final Shape24 localAssetEntity = Shape24(
source: i0.VersionedTable(
entityName: 'local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_22,
_column_14,
_column_23,
_column_96,
_column_46,
_column_47,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape9 remoteAlbumEntity = Shape9(
source: i0.VersionedTable(
entityName: 'remote_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_56,
_column_9,
_column_5,
_column_15,
_column_57,
_column_58,
_column_59,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape19 localAlbumEntity = Shape19(
source: i0.VersionedTable(
entityName: 'local_album_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_5,
_column_31,
_column_32,
_column_90,
_column_33,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape22 localAlbumAssetEntity = Shape22(
source: i0.VersionedTable(
entityName: 'local_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_34, _column_35, _column_33],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLocalAssetChecksum = i1.Index(
'idx_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
);
final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
'idx_remote_asset_owner_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
);
final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
'UQ_remote_assets_owner_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
);
final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
'UQ_remote_assets_owner_library_checksum',
'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
);
final i1.Index idxRemoteAssetChecksum = i1.Index(
'idx_remote_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
);
late final Shape21 authUserEntity = Shape21(
source: i0.VersionedTable(
entityName: 'auth_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_1,
_column_3,
_column_2,
_column_84,
_column_85,
_column_92,
_column_93,
_column_7,
_column_94,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape4 userMetadataEntity = Shape4(
source: i0.VersionedTable(
entityName: 'user_metadata_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(user_id, "key")'],
columns: [_column_25, _column_26, _column_27],
attachedDatabase: database,
),
alias: null,
);
late final Shape5 partnerEntity = Shape5(
source: i0.VersionedTable(
entityName: 'partner_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
columns: [_column_28, _column_29, _column_30],
attachedDatabase: database,
),
alias: null,
);
late final Shape8 remoteExifEntity = Shape8(
source: i0.VersionedTable(
entityName: 'remote_exif_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id)'],
columns: [
_column_36,
_column_37,
_column_38,
_column_39,
_column_40,
_column_41,
_column_11,
_column_10,
_column_42,
_column_43,
_column_44,
_column_45,
_column_46,
_column_47,
_column_48,
_column_49,
_column_50,
_column_51,
_column_52,
_column_53,
_column_54,
_column_55,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape7 remoteAlbumAssetEntity = Shape7(
source: i0.VersionedTable(
entityName: 'remote_album_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
columns: [_column_36, _column_60],
attachedDatabase: database,
),
alias: null,
);
late final Shape10 remoteAlbumUserEntity = Shape10(
source: i0.VersionedTable(
entityName: 'remote_album_user_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
columns: [_column_60, _column_25, _column_61],
attachedDatabase: database,
),
alias: null,
);
late final Shape11 memoryEntity = Shape11(
source: i0.VersionedTable(
entityName: 'memory_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_18,
_column_15,
_column_8,
_column_62,
_column_63,
_column_64,
_column_65,
_column_66,
_column_67,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape12 memoryAssetEntity = Shape12(
source: i0.VersionedTable(
entityName: 'memory_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
columns: [_column_36, _column_68],
attachedDatabase: database,
),
alias: null,
);
late final Shape14 personEntity = Shape14(
source: i0.VersionedTable(
entityName: 'person_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_9,
_column_5,
_column_15,
_column_1,
_column_69,
_column_71,
_column_72,
_column_73,
_column_74,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape15 assetFaceEntity = Shape15(
source: i0.VersionedTable(
entityName: 'asset_face_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [
_column_0,
_column_36,
_column_76,
_column_77,
_column_78,
_column_79,
_column_80,
_column_81,
_column_82,
_column_83,
],
attachedDatabase: database,
),
alias: null,
);
late final Shape18 storeEntity = Shape18(
source: i0.VersionedTable(
entityName: 'store_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id)'],
columns: [_column_87, _column_88, _column_89],
attachedDatabase: database,
),
alias: null,
);
late final Shape23 trashedLocalAssetEntity = Shape23(
source: i0.VersionedTable(
entityName: 'trashed_local_asset_entity',
withoutRowId: true,
isStrict: true,
tableConstraints: ['PRIMARY KEY(id, album_id)'],
columns: [
_column_1,
_column_8,
_column_9,
_column_5,
_column_10,
_column_11,
_column_12,
_column_0,
_column_95,
_column_22,
_column_14,
_column_23,
],
attachedDatabase: database,
),
alias: null,
);
final i1.Index idxLatLng = i1.Index(
'idx_lat_lng',
'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
);
final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
'idx_trashed_local_asset_checksum',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
);
final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
'idx_trashed_local_asset_album',
'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
);
}
class Shape24 extends i0.VersionedTable {
Shape24({required super.source, required super.alias}) : super.aliased();
i1.GeneratedColumn<String> get name =>
columnsByName['name']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<int> get type =>
columnsByName['type']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get createdAt =>
columnsByName['created_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<DateTime> get updatedAt =>
columnsByName['updated_at']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<int> get width =>
columnsByName['width']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get height =>
columnsByName['height']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<int> get durationInSeconds =>
columnsByName['duration_in_seconds']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<String> get id =>
columnsByName['id']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<String> get checksum =>
columnsByName['checksum']! as i1.GeneratedColumn<String>;
i1.GeneratedColumn<bool> get isFavorite =>
columnsByName['is_favorite']! as i1.GeneratedColumn<bool>;
i1.GeneratedColumn<int> get orientation =>
columnsByName['orientation']! as i1.GeneratedColumn<int>;
i1.GeneratedColumn<DateTime> get adjustmentTime =>
columnsByName['adjustment_time']! as i1.GeneratedColumn<DateTime>;
i1.GeneratedColumn<double> get latitude =>
columnsByName['latitude']! as i1.GeneratedColumn<double>;
i1.GeneratedColumn<double> get longitude =>
columnsByName['longitude']! as i1.GeneratedColumn<double>;
}
i1.GeneratedColumn<DateTime> _column_96(String aliasedName) =>
i1.GeneratedColumn<DateTime>(
'adjustment_time',
aliasedName,
true,
type: i1.DriftSqlType.dateTime,
);
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -5498,6 +5954,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11, required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12, required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13, required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@@ -5561,6 +6018,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from12To13(migrator, schema); await from12To13(migrator, schema);
return 13; return 13;
case 13:
final schema = Schema14(database: database);
final migrator = i1.Migrator(database, schema);
await from13To14(migrator, schema);
return 14;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@@ -5580,6 +6042,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11, required Future<void> Function(i1.Migrator m, Schema11 schema) from10To11,
required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12, required Future<void> Function(i1.Migrator m, Schema12 schema) from11To12,
required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13, required Future<void> Function(i1.Migrator m, Schema13 schema) from12To13,
required Future<void> Function(i1.Migrator m, Schema14 schema) from13To14,
}) => i0.VersionedSchema.stepByStepHelper( }) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
from1To2: from1To2, from1To2: from1To2,
@@ -5594,5 +6057,6 @@ i1.OnUpgrade stepByStep({
from10To11: from10To11, from10To11: from10To11,
from11To12: from11To12, from11To12: from11To12,
from12To13: from12To13, from12To13: from12To13,
from13To14: from13To14,
), ),
); );

View File

@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart'; import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
@@ -244,7 +246,56 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get(); return query.map((row) => row.readTable(_db.localAssetEntity).toDto()).get();
} }
Future<void> _upsertAssets(Iterable<LocalAsset> localAssets) { Future<void> Function(Iterable<LocalAsset>) get _upsertAssets =>
CurrentPlatform.isIOS ? _upsertAssetsDarwin : _upsertAssetsAndroid;
Future<void> _upsertAssetsDarwin(Iterable<LocalAsset> localAssets) async {
if (localAssets.isEmpty) {
return Future.value();
}
// Reset checksum if asset changed
await _db.batch((batch) async {
for (final asset in localAssets) {
final companion = LocalAssetEntityCompanion(
checksum: const Value(null),
adjustmentTime: Value(asset.adjustmentTime),
);
batch.update(
_db.localAssetEntity,
companion,
where: (row) => row.id.equals(asset.id) & row.adjustmentTime.isNotExp(Variable(asset.adjustmentTime)),
);
}
});
return _db.batch((batch) async {
for (final asset in localAssets) {
final companion = LocalAssetEntityCompanion.insert(
name: asset.name,
type: asset.type,
createdAt: Value(asset.createdAt),
updatedAt: Value(asset.updatedAt),
width: Value(asset.width),
height: Value(asset.height),
durationInSeconds: Value(asset.durationInSeconds),
id: asset.id,
orientation: Value(asset.orientation),
isFavorite: Value(asset.isFavorite),
latitude: Value(asset.latitude),
longitude: Value(asset.longitude),
adjustmentTime: Value(asset.adjustmentTime),
);
batch.insert<$LocalAssetEntityTable, LocalAssetEntityData>(
_db.localAssetEntity,
companion.copyWith(checksum: const Value(null)),
onConflict: DoUpdate((old) => companion),
);
}
});
}
Future<void> _upsertAssetsAndroid(Iterable<LocalAsset> localAssets) async {
if (localAssets.isEmpty) { if (localAssets.isEmpty) {
return Future.value(); return Future.value();
} }
@@ -260,6 +311,7 @@ class DriftLocalAlbumRepository extends DriftDatabaseRepository {
height: Value(asset.height), height: Value(asset.height),
durationInSeconds: Value(asset.durationInSeconds), durationInSeconds: Value(asset.durationInSeconds),
id: asset.id, id: asset.id,
checksum: const Value(null),
orientation: Value(asset.orientation), orientation: Value(asset.orientation),
isFavorite: Value(asset.isFavorite), isFavorite: Value(asset.isFavorite),
); );

View File

@@ -98,7 +98,7 @@ class DriftUploadDetailPage extends ConsumerWidget {
), ),
), ),
Text( Text(
'Tap for more details', "backup_upload_details_page_more_details".t(context: context),
style: context.textTheme.bodySmall?.copyWith( style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.onSurface.withValues(alpha: 0.6), color: context.colorScheme.onSurface.withValues(alpha: 0.6),
), ),
@@ -239,14 +239,20 @@ class FileDetailDialog extends ConsumerWidget {
const SizedBox(height: 24), const SizedBox(height: 24),
if (asset != null) ...[ if (asset != null) ...[
_buildInfoSection(context, [ _buildInfoSection(context, [
_buildInfoRow(context, "Filename", path.basename(uploadStatus.filename)), _buildInfoRow(context, "filename".t(context: context), path.basename(uploadStatus.filename)),
_buildInfoRow(context, "Local ID", asset.id), _buildInfoRow(context, "local_id".t(context: context), asset.id),
_buildInfoRow(context, "File Size", formatHumanReadableBytes(uploadStatus.fileSize, 2)), _buildInfoRow(
if (asset.width != null) _buildInfoRow(context, "Width", "${asset.width}px"), context,
if (asset.height != null) _buildInfoRow(context, "Height", "${asset.height}px"), "file_size".t(context: context),
_buildInfoRow(context, "Created At", asset.createdAt.toString()), formatHumanReadableBytes(uploadStatus.fileSize, 2),
_buildInfoRow(context, "Updated At", asset.updatedAt.toString()), ),
if (asset.checksum != null) _buildInfoRow(context, "Checksum", asset.checksum!), if (asset.width != null) _buildInfoRow(context, "width".t(context: context), "${asset.width}px"),
if (asset.height != null)
_buildInfoRow(context, "height".t(context: context), "${asset.height}px"),
_buildInfoRow(context, "created_at".t(context: context), asset.createdAt.toString()),
_buildInfoRow(context, "updated_at".t(context: context), asset.updatedAt.toString()),
if (asset.checksum != null)
_buildInfoRow(context, "checksum".t(context: context), asset.checksum!),
]), ]),
], ],
], ],

View File

@@ -41,6 +41,9 @@ class PlatformAsset {
required this.durationInSeconds, required this.durationInSeconds,
required this.orientation, required this.orientation,
required this.isFavorite, required this.isFavorite,
this.adjustmentTime,
this.latitude,
this.longitude,
}); });
String id; String id;
@@ -63,8 +66,28 @@ class PlatformAsset {
bool isFavorite; bool isFavorite;
int? adjustmentTime;
double? latitude;
double? longitude;
List<Object?> _toList() { List<Object?> _toList() {
return <Object?>[id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite]; return <Object?>[
id,
name,
type,
createdAt,
updatedAt,
width,
height,
durationInSeconds,
orientation,
isFavorite,
adjustmentTime,
latitude,
longitude,
];
} }
Object encode() { Object encode() {
@@ -84,6 +107,9 @@ class PlatformAsset {
durationInSeconds: result[7]! as int, durationInSeconds: result[7]! as int,
orientation: result[8]! as int, orientation: result[8]! as int,
isFavorite: result[9]! as bool, isFavorite: result[9]! as bool,
adjustmentTime: result[10] as int?,
latitude: result[11] as double?,
longitude: result[12] as double?,
); );
} }

View File

@@ -37,7 +37,7 @@ class DriftActivitiesPage extends HookConsumerWidget {
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text(album.name), title: Text(album.name),
actions: [const LikeActivityActionButton(menuItem: true)], actions: [const LikeActivityActionButton(iconOnly: true)],
actionsPadding: const EdgeInsets.only(right: 8), actionsPadding: const EdgeInsets.only(right: 8),
), ),
body: activities.widgetWhen( body: activities.widgetWhen(

View File

@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
@RoutePage() @RoutePage()
@@ -129,6 +130,15 @@ class _AssetPropertiesSectionState extends ConsumerState<_AssetPropertiesSection
properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString())); properties.insert(4, _PropertyItem(label: 'Orientation', value: asset.orientation.toString()));
final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id); final albums = await ref.read(assetServiceProvider).getSourceAlbums(asset.id);
properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', '))); properties.add(_PropertyItem(label: 'Album', value: albums.map((a) => a.name).join(', ')));
if (CurrentPlatform.isIOS) {
properties.add(_PropertyItem(label: 'Adjustment Time', value: asset.adjustmentTime?.toString()));
}
properties.add(
_PropertyItem(
label: 'GPS Coordinates',
value: asset.hasCoordinates ? '${asset.latitude}, ${asset.longitude}' : null,
),
);
} }
Future<void> _addRemoteAssetProperties(RemoteAsset asset) async { Future<void> _addRemoteAssetProperties(RemoteAsset asset) async {

View File

@@ -27,8 +27,19 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
bool isAlbumTitleTextFieldFocus = false; bool isAlbumTitleTextFieldFocus = false;
Set<BaseAsset> selectedAssets = {}; Set<BaseAsset> selectedAssets = {};
@override
void initState() {
super.initState();
albumTitleController.addListener(_onTitleChanged);
}
void _onTitleChanged() {
setState(() {});
}
@override @override
void dispose() { void dispose() {
albumTitleController.removeListener(_onTitleChanged);
albumTitleController.dispose(); albumTitleController.dispose();
albumDescriptionController.dispose(); albumDescriptionController.dispose();
albumTitleTextFieldFocusNode.dispose(); albumTitleTextFieldFocusNode.dispose();

View File

@@ -21,12 +21,36 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
enum AddToMenuItem { album, archive, unarchive, lockedFolder } enum AddToMenuItem { album, archive, unarchive, lockedFolder }
class AddActionButton extends ConsumerWidget { class AddActionButton extends ConsumerStatefulWidget {
const AddActionButton({super.key}); const AddActionButton({super.key, this.originalTheme});
Future<void> _showAddOptions(BuildContext context, WidgetRef ref) async { final ThemeData? originalTheme;
@override
ConsumerState<AddActionButton> createState() => _AddActionButtonState();
}
class _AddActionButtonState extends ConsumerState<AddActionButton> {
void _handleMenuSelection(AddToMenuItem selected) {
switch (selected) {
case AddToMenuItem.album:
_openAlbumSelector();
break;
case AddToMenuItem.archive:
performArchiveAction(context, ref, source: ActionSource.viewer);
break;
case AddToMenuItem.unarchive:
performUnArchiveAction(context, ref, source: ActionSource.viewer);
break;
case AddToMenuItem.lockedFolder:
performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
break;
}
}
List<Widget> _buildMenuChildren() {
final asset = ref.read(currentAssetNotifier); final asset = ref.read(currentAssetNotifier);
if (asset == null) return; if (asset == null) return [];
final user = ref.read(currentUserProvider); final user = ref.read(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id; final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
@@ -35,93 +59,57 @@ class AddActionButton extends ConsumerWidget {
final hasRemote = asset is RemoteAsset; final hasRemote = asset is RemoteAsset;
final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived; final showArchive = isOwner && !isInLockedView && hasRemote && !isArchived;
final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived; final showUnarchive = isOwner && !isInLockedView && hasRemote && isArchived;
final menuItemHeight = 30.0;
final List<PopupMenuEntry<AddToMenuItem>> items = [ return [
PopupMenuItem( Padding(
enabled: false, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
textStyle: context.textTheme.labelMedium, child: Text("add_to_bottom_bar".tr(), style: context.textTheme.labelMedium),
height: 40,
child: Text("add_to_bottom_bar".tr()),
), ),
PopupMenuItem( BaseActionButton(
height: menuItemHeight, iconData: Icons.photo_album_outlined,
value: AddToMenuItem.album, label: "album".tr(),
child: ListTile(leading: const Icon(Icons.photo_album_outlined), title: Text("album".tr())), menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.album),
), ),
const PopupMenuDivider(),
PopupMenuItem(enabled: false, textStyle: context.textTheme.labelMedium, height: 40, child: Text("move_to".tr())),
if (isOwner) ...[ if (isOwner) ...[
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
),
if (showArchive) if (showArchive)
PopupMenuItem( BaseActionButton(
height: menuItemHeight, iconData: Icons.archive_outlined,
value: AddToMenuItem.archive, label: "archive".tr(),
child: ListTile(leading: const Icon(Icons.archive_outlined), title: Text("archive".tr())), menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.archive),
), ),
if (showUnarchive) if (showUnarchive)
PopupMenuItem( BaseActionButton(
height: menuItemHeight, iconData: Icons.unarchive_outlined,
value: AddToMenuItem.unarchive, label: "unarchive".tr(),
child: ListTile(leading: const Icon(Icons.unarchive_outlined), title: Text("unarchive".tr())), menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.unarchive),
), ),
PopupMenuItem( BaseActionButton(
height: menuItemHeight, iconData: Icons.lock_outline,
value: AddToMenuItem.lockedFolder, label: "locked_folder".tr(),
child: ListTile(leading: const Icon(Icons.lock_outline), title: Text("locked_folder".tr())), menuItem: true,
onPressed: () => _handleMenuSelection(AddToMenuItem.lockedFolder),
), ),
], ],
]; ];
final AddToMenuItem? selected = await showMenu<AddToMenuItem>(
context: context,
color: context.themeData.scaffoldBackgroundColor,
position: _menuPosition(context),
items: items,
popUpAnimationStyle: AnimationStyle.noAnimation,
);
if (selected == null) {
return;
}
switch (selected) {
case AddToMenuItem.album:
_openAlbumSelector(context, ref);
break;
case AddToMenuItem.archive:
await performArchiveAction(context, ref, source: ActionSource.viewer);
break;
case AddToMenuItem.unarchive:
await performUnArchiveAction(context, ref, source: ActionSource.viewer);
break;
case AddToMenuItem.lockedFolder:
await performMoveToLockFolderAction(context, ref, source: ActionSource.viewer);
break;
}
} }
RelativeRect _menuPosition(BuildContext context) { void _openAlbumSelector() {
final renderObject = context.findRenderObject();
if (renderObject is! RenderBox) {
return RelativeRect.fill;
}
final size = renderObject.size;
final position = renderObject.localToGlobal(Offset.zero);
return RelativeRect.fromLTRB(position.dx, position.dy - size.height - 200, position.dx + size.width, position.dy);
}
void _openAlbumSelector(BuildContext context, WidgetRef ref) {
final currentAsset = ref.read(currentAssetNotifier); final currentAsset = ref.read(currentAssetNotifier);
if (currentAsset == null) { if (currentAsset == null) {
ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error); ImmichToast.show(context: context, msg: "Cannot load asset information.", toastType: ToastType.error);
return; return;
} }
final List<Widget> slivers = [ final List<Widget> slivers = [AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(album))];
AlbumSelector(onAlbumSelected: (album) => _addCurrentAssetToAlbum(context, ref, album)),
];
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@@ -141,7 +129,7 @@ class AddActionButton extends ConsumerWidget {
); );
} }
Future<void> _addCurrentAssetToAlbum(BuildContext context, WidgetRef ref, RemoteAlbum album) async { Future<void> _addCurrentAssetToAlbum(RemoteAlbum album) async {
final latest = ref.read(currentAssetNotifier); final latest = ref.read(currentAssetNotifier);
if (latest == null) { if (latest == null) {
@@ -174,17 +162,38 @@ class AddActionButton extends ConsumerWidget {
} }
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context) {
final asset = ref.watch(currentAssetNotifier); final asset = ref.watch(currentAssetNotifier);
if (asset == null) { if (asset == null) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
return Builder(
builder: (buttonContext) { final themeData = widget.originalTheme ?? context.themeData;
return MenuAnchor(
consumeOutsideTap: true,
style: MenuStyle(
backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor),
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
elevation: const WidgetStatePropertyAll(4),
shape: const WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
),
menuChildren: widget.originalTheme != null
? [
Theme(
data: widget.originalTheme!,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: _buildMenuChildren()),
),
]
: _buildMenuChildren(),
builder: (context, controller, child) {
return BaseActionButton( return BaseActionButton(
iconData: Icons.add, iconData: Icons.add,
label: "add_to_bottom_bar".tr(), label: "add_to_bottom_bar".tr(),
onPressed: () => _showAddOptions(buttonContext, ref), onPressed: () => controller.isOpen ? controller.close() : controller.open(),
); );
}, },
); );

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
class BaseActionButton extends StatelessWidget { class BaseActionButton extends ConsumerWidget {
const BaseActionButton({ const BaseActionButton({
super.key, super.key,
required this.label, required this.label,
@@ -11,6 +12,7 @@ class BaseActionButton extends StatelessWidget {
this.onLongPressed, this.onLongPressed,
this.maxWidth = 90.0, this.maxWidth = 90.0,
this.minWidth, this.minWidth,
this.iconOnly = false,
this.menuItem = false, this.menuItem = false,
}); });
@@ -19,25 +21,42 @@ class BaseActionButton extends StatelessWidget {
final Color? iconColor; final Color? iconColor;
final double maxWidth; final double maxWidth;
final double? minWidth; final double? minWidth;
/// When true, renders only an IconButton without text label
final bool iconOnly;
/// When true, renders as a MenuItemButton for use in MenuAnchor menus
final bool menuItem; final bool menuItem;
final void Function()? onPressed; final void Function()? onPressed;
final void Function()? onLongPressed; final void Function()? onLongPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context, WidgetRef ref) {
final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0); final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
final iconTheme = IconTheme.of(context); final iconTheme = IconTheme.of(context);
final iconSize = iconTheme.size ?? 24.0; final iconSize = iconTheme.size ?? 24.0;
final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color; final iconColor = this.iconColor ?? iconTheme.color ?? context.themeData.iconTheme.color;
final textColor = context.themeData.textTheme.labelLarge?.color; final textColor = context.themeData.textTheme.labelLarge?.color;
if (menuItem) { if (iconOnly) {
return IconButton( return IconButton(
onPressed: onPressed, onPressed: onPressed,
icon: Icon(iconData, size: iconSize, color: iconColor), icon: Icon(iconData, size: iconSize, color: iconColor),
); );
} }
if (menuItem) {
final theme = context.themeData;
final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant;
return MenuItemButton(
style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)),
leadingIcon: Icon(iconData, color: effectiveIconColor),
onPressed: onPressed,
child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16)),
);
}
return ConstrainedBox( return ConstrainedBox(
constraints: BoxConstraints(maxWidth: maxWidth), constraints: BoxConstraints(maxWidth: maxWidth),
child: MaterialButton( child: MaterialButton(

View File

@@ -7,8 +7,9 @@ import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart'; import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
class CastActionButton extends ConsumerWidget { class CastActionButton extends ConsumerWidget {
const CastActionButton({super.key, this.menuItem = true}); const CastActionButton({super.key, this.iconOnly = false, this.menuItem = false});
final bool iconOnly;
final bool menuItem; final bool menuItem;
@override @override
@@ -22,6 +23,7 @@ class CastActionButton extends ConsumerWidget {
onPressed: () { onPressed: () {
showDialog(context: context, builder: (context) => const CastDialog()); showDialog(context: context, builder: (context) => const CastDialog());
}, },
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
); );
} }

View File

@@ -10,8 +10,9 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
class DownloadActionButton extends ConsumerWidget { class DownloadActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;
final bool iconOnly;
final bool menuItem; final bool menuItem;
const DownloadActionButton({super.key, required this.source, this.menuItem = false}); const DownloadActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async { void _onTap(BuildContext context, WidgetRef ref, BackgroundSyncManager backgroundSyncManager) async {
if (!context.mounted) { if (!context.mounted) {
@@ -38,6 +39,7 @@ class DownloadActionButton extends ConsumerWidget {
iconData: Icons.download, iconData: Icons.download,
maxWidth: 95, maxWidth: 95,
label: "download".t(context: context), label: "download".t(context: context),
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
onPressed: () => _onTap(context, ref, backgroundManager), onPressed: () => _onTap(context, ref, backgroundManager),
); );

View File

@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
class FavoriteActionButton extends ConsumerWidget { class FavoriteActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;
final bool iconOnly;
final bool menuItem; final bool menuItem;
const FavoriteActionButton({super.key, required this.source, this.menuItem = false}); const FavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref) async { void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) { if (!context.mounted) {
@@ -44,6 +45,7 @@ class FavoriteActionButton extends ConsumerWidget {
return BaseActionButton( return BaseActionButton(
iconData: Icons.favorite_border_rounded, iconData: Icons.favorite_border_rounded,
label: "favorite".t(context: context), label: "favorite".t(context: context),
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
onPressed: () => _onTap(context, ref), onPressed: () => _onTap(context, ref),
); );

View File

@@ -12,8 +12,9 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
class LikeActivityActionButton extends ConsumerWidget { class LikeActivityActionButton extends ConsumerWidget {
const LikeActivityActionButton({super.key, this.menuItem = false}); const LikeActivityActionButton({super.key, this.iconOnly = false, this.menuItem = false});
final bool iconOnly;
final bool menuItem; final bool menuItem;
@override @override
@@ -49,6 +50,7 @@ class LikeActivityActionButton extends ConsumerWidget {
iconData: liked != null ? Icons.favorite : Icons.favorite_border, iconData: liked != null ? Icons.favorite : Icons.favorite_border,
label: "like".t(context: context), label: "like".t(context: context),
onPressed: () => onTap(liked), onPressed: () => onTap(liked),
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
); );
}, },
@@ -57,6 +59,7 @@ class LikeActivityActionButton extends ConsumerWidget {
loading: () => BaseActionButton( loading: () => BaseActionButton(
iconData: Icons.favorite_border, iconData: Icons.favorite_border,
label: "like".t(context: context), label: "like".t(context: context),
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
), ),
error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])), error: (error, stack) => Text('error_saving_image'.tr(args: [error.toString()])),

View File

@@ -5,8 +5,9 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
class MotionPhotoActionButton extends ConsumerWidget { class MotionPhotoActionButton extends ConsumerWidget {
const MotionPhotoActionButton({super.key, this.menuItem = true}); const MotionPhotoActionButton({super.key, this.iconOnly = false, this.menuItem = false});
final bool iconOnly;
final bool menuItem; final bool menuItem;
@override @override
@@ -17,6 +18,7 @@ class MotionPhotoActionButton extends ConsumerWidget {
iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded, iconData: isPlaying ? Icons.motion_photos_pause_outlined : Icons.play_circle_outline_rounded,
label: "play_motion_photo".t(context: context), label: "play_motion_photo".t(context: context),
onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle, onPressed: ref.read(isPlayingMotionVideoProvider.notifier).toggle,
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
); );
} }

View File

@@ -10,9 +10,10 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart';
class UnFavoriteActionButton extends ConsumerWidget { class UnFavoriteActionButton extends ConsumerWidget {
final ActionSource source; final ActionSource source;
final bool iconOnly;
final bool menuItem; final bool menuItem;
const UnFavoriteActionButton({super.key, required this.source, this.menuItem = false}); const UnFavoriteActionButton({super.key, required this.source, this.iconOnly = false, this.menuItem = false});
void _onTap(BuildContext context, WidgetRef ref) async { void _onTap(BuildContext context, WidgetRef ref) async {
if (!context.mounted) { if (!context.mounted) {
@@ -45,6 +46,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
iconData: Icons.favorite_rounded, iconData: Icons.favorite_rounded,
label: "unfavorite".t(context: context), label: "unfavorite".t(context: context),
onPressed: () => _onTap(context, ref), onPressed: () => _onTap(context, ref),
iconOnly: iconOnly,
menuItem: menuItem, menuItem: menuItem,
); );
} }

View File

@@ -38,11 +38,13 @@ class ViewerBottomBar extends ConsumerWidget {
opacity = 0; opacity = 0;
} }
final originalTheme = context.themeData;
final actions = <Widget>[ final actions = <Widget>[
const ShareActionButton(source: ActionSource.viewer), const ShareActionButton(source: ActionSource.viewer),
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer), if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
if (asset.type == AssetType.image) const EditImageActionButton(), if (asset.type == AssetType.image) const EditImageActionButton(),
if (asset.hasRemote) const AddActionButton(), if (asset.hasRemote) AddActionButton(originalTheme: originalTheme),
if (isOwner) ...[ if (isOwner) ...[
asset.isLocalOnly asset.isLocalOnly

View File

@@ -4,25 +4,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
import 'package:immich_mobile/providers/activity.provider.dart'; import 'package:immich_mobile/providers/activity.provider.dart';
import 'package:immich_mobile/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart'; import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/routes.provider.dart'; import 'package:immich_mobile/providers/routes.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/routing/router.dart';
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget { class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
const ViewerTopAppBar({super.key}); const ViewerTopAppBar({super.key});
@@ -41,15 +35,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
final isInLockedView = ref.watch(inLockedViewProvider); final isInLockedView = ref.watch(inLockedViewProvider);
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider); final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
final timelineOrigin = ref.read(timelineServiceProvider).origin;
final showViewInTimelineButton =
timelineOrigin != TimelineOrigin.main &&
timelineOrigin != TimelineOrigin.deepLink &&
timelineOrigin != TimelineOrigin.trash &&
timelineOrigin != TimelineOrigin.archive &&
timelineOrigin != TimelineOrigin.localAlbum &&
isOwner;
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet)); final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity)); int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls)); final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
@@ -62,11 +47,10 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
opacity = 0; opacity = 0;
} }
final isCasting = ref.watch(castProvider.select((c) => c.isCasting)); final originalTheme = context.themeData;
final actions = <Widget>[ final actions = <Widget>[
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, menuItem: true), if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
if (album != null && album.isActivityEnabled && album.isShared) if (album != null && album.isActivityEnabled && album.isShared)
IconButton( IconButton(
icon: const Icon(Icons.chat_outlined), icon: const Icon(Icons.chat_outlined),
@@ -74,28 +58,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true)); EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true));
}, },
), ),
if (showViewInTimelineButton)
IconButton(
onPressed: () async {
await context.maybePop();
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
EventStream.shared.emit(ScrollToDateEvent(asset.createdAt));
},
icon: const Icon(Icons.image_search),
tooltip: 'view_in_timeline'.t(context: context),
),
if (asset.hasRemote && isOwner && !asset.isFavorite) if (asset.hasRemote && isOwner && !asset.isFavorite)
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true), const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
if (asset.hasRemote && isOwner && asset.isFavorite) if (asset.hasRemote && isOwner && asset.isFavorite)
const UnFavoriteActionButton(source: ActionSource.viewer, menuItem: true), const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
if (asset.isMotionPhoto) const MotionPhotoActionButton(menuItem: true),
const _KebabMenu(), ViewerKebabMenu(originalTheme: originalTheme),
]; ];
final lockedViewActions = <Widget>[ final lockedViewActions = <Widget>[ViewerKebabMenu(originalTheme: originalTheme)];
if (isCasting || (asset.hasRemote)) const CastActionButton(menuItem: true),
const _KebabMenu(),
];
return IgnorePointer( return IgnorePointer(
ignoring: opacity < 255, ignoring: opacity < 255,
@@ -122,20 +94,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
Size get preferredSize => const Size.fromHeight(60.0); Size get preferredSize => const Size.fromHeight(60.0);
} }
class _KebabMenu extends ConsumerWidget {
const _KebabMenu();
@override
Widget build(BuildContext context, WidgetRef ref) {
return IconButton(
onPressed: () {
EventStream.shared.emit(const ViewerOpenBottomSheetEvent());
},
icon: const Icon(Icons.more_vert_rounded),
);
}
}
class _AppBarBackButton extends ConsumerWidget { class _AppBarBackButton extends ConsumerWidget {
const _AppBarBackButton(); const _AppBarBackButton();

View File

@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/providers/cast.provider.dart';
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/utils/action_button.utils.dart';
class ViewerKebabMenu extends ConsumerWidget {
const ViewerKebabMenu({super.key, this.originalTheme});
final ThemeData? originalTheme;
@override
Widget build(BuildContext context, WidgetRef ref) {
final asset = ref.watch(currentAssetNotifier);
if (asset == null) {
return const SizedBox.shrink();
}
final user = ref.watch(currentUserProvider);
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
final timelineOrigin = ref.read(timelineServiceProvider).origin;
final kebabContext = ViewerKebabMenuButtonContext(
asset: asset,
isOwner: isOwner,
isCasting: isCasting,
timelineOrigin: timelineOrigin,
originalTheme: originalTheme,
);
final menuChildren = ViewerKebabMenuButtonBuilder.build(kebabContext, context, ref);
return MenuAnchor(
consumeOutsideTap: true,
style: MenuStyle(
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
elevation: const WidgetStatePropertyAll(4),
shape: const WidgetStatePropertyAll(
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
),
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
),
menuChildren: [
ConstrainedBox(
constraints: const BoxConstraints(minWidth: 150),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: menuChildren,
),
),
],
builder: (context, controller, child) {
return IconButton(
icon: const Icon(Icons.more_vert_rounded),
onPressed: () => controller.isOpen ? controller.close() : controller.open(),
);
},
);
}
}

View File

@@ -14,7 +14,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart'; import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
@@ -47,10 +46,7 @@ class ArchiveBottomSheet extends ConsumerWidget {
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
const DeleteLocalActionButton(source: ActionSource.timeline),
const UploadActionButton(source: ActionSource.timeline),
],
], ],
); );
} }

View File

@@ -17,7 +17,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
@@ -86,10 +85,7 @@ class FavoriteBottomSheet extends ConsumerWidget {
if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline), if (multiselect.selectedAssets.length > 1) const StackActionButton(source: ActionSource.timeline),
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
const DeleteLocalActionButton(source: ActionSource.timeline),
const UploadActionButton(source: ActionSource.timeline),
],
], ],
slivers: multiselect.hasRemote slivers: multiselect.hasRemote
? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addAssetsToAlbum)] ? [const AddToAlbumHeader(), AlbumSelector(onAlbumSelected: addAssetsToAlbum)]

View File

@@ -18,7 +18,6 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_act
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart'; import 'package:immich_mobile/presentation/widgets/album/album_selector.widget.dart';
import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
@@ -112,10 +111,7 @@ class _RemoteAlbumBottomSheetState extends ConsumerState<RemoteAlbumBottomSheet>
if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline), if (multiselect.hasStacked) const UnStackActionButton(source: ActionSource.timeline),
], ],
], ],
if (multiselect.hasLocal) ...[ if (multiselect.hasMerged) const DeleteLocalActionButton(source: ActionSource.timeline),
const DeleteLocalActionButton(source: ActionSource.timeline),
const UploadActionButton(source: ActionSource.timeline),
],
if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id), if (ownsAlbum) RemoveFromAlbumActionButton(source: ActionSource.timeline, albumId: widget.album.id),
], ],
slivers: ownsAlbum slivers: ownsAlbum

View File

@@ -324,7 +324,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10; final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10;
const scrubberBottomPadding = 100.0; const scrubberBottomPadding = 100.0;
final bottomPadding = context.padding.bottom + (widget.appBar == null ? 0 : scrubberBottomPadding); const bottomSheetOpenModifier = 120.0;
final bottomPadding =
context.padding.bottom +
(widget.appBar == null ? 0 : scrubberBottomPadding) +
(isMultiSelectEnabled ? bottomSheetOpenModifier : 0);
final grid = CustomScrollView( final grid = CustomScrollView(
primary: true, primary: true,
@@ -347,7 +351,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
addRepaintBoundaries: false, addRepaintBoundaries: false,
), ),
), ),
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)), SliverPadding(padding: EdgeInsets.only(bottom: bottomPadding)),
], ],
); );

View File

@@ -167,7 +167,7 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]), AutoRoute(page: LoginRoute.page, guards: [_duplicateGuard]),
AutoRoute(page: ChangePasswordRoute.page), AutoRoute(page: ChangePasswordRoute.page),
AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false), AutoRoute(page: SearchRoute.page, guards: [_authGuard, _duplicateGuard], maintainState: false),
CustomRoute( AutoRoute(
page: TabControllerRoute.page, page: TabControllerRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
children: [ children: [
@@ -176,9 +176,8 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
], ],
transitionsBuilder: TransitionsBuilders.fadeIn,
), ),
CustomRoute( AutoRoute(
page: TabShellRoute.page, page: TabShellRoute.page,
guards: [_authGuard, _duplicateGuard], guards: [_authGuard, _duplicateGuard],
children: [ children: [
@@ -187,7 +186,6 @@ class AppRouter extends RootStackRouter {
AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftLibraryRoute.page, guards: [_authGuard, _duplicateGuard]),
AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: DriftAlbumsRoute.page, guards: [_authGuard, _duplicateGuard]),
], ],
transitionsBuilder: TransitionsBuilders.fadeIn,
), ),
CustomRoute( CustomRoute(
page: GalleryViewerRoute.page, page: GalleryViewerRoute.page,

View File

@@ -1,9 +1,18 @@
import 'package:flutter/widgets.dart'; import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/album/album.model.dart'; import 'package:immich_mobile/domain/models/album/album.model.dart';
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/events.model.dart';
import 'package:immich_mobile/domain/services/timeline.service.dart';
import 'package:immich_mobile/domain/utils/event_stream.dart';
import 'package:immich_mobile/extensions/translate_extensions.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
@@ -19,6 +28,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
import 'package:immich_mobile/routing/router.dart';
class ActionButtonContext { class ActionButtonContext {
final BaseAsset asset; final BaseAsset asset;
@@ -164,3 +174,98 @@ class ActionButtonBuilder {
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList(); return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
} }
} }
class ViewerKebabMenuButtonContext {
final BaseAsset asset;
final bool isOwner;
final bool isCasting;
final TimelineOrigin timelineOrigin;
final ThemeData? originalTheme;
const ViewerKebabMenuButtonContext({
required this.asset,
required this.isOwner,
required this.isCasting,
required this.timelineOrigin,
this.originalTheme,
});
}
enum ViewerKebabMenuButtonType {
openInfo,
viewInTimeline,
cast,
download;
/// Defines which group each button belongs to.
/// Buttons in the same group will be displayed together,
/// with dividers separating different groups.
int get group => switch (this) {
ViewerKebabMenuButtonType.openInfo => 0,
ViewerKebabMenuButtonType.viewInTimeline => 1,
ViewerKebabMenuButtonType.cast => 1,
ViewerKebabMenuButtonType.download => 1,
};
bool shouldShow(ViewerKebabMenuButtonContext context) {
return switch (this) {
ViewerKebabMenuButtonType.openInfo => true,
ViewerKebabMenuButtonType.viewInTimeline =>
context.timelineOrigin != TimelineOrigin.main &&
context.timelineOrigin != TimelineOrigin.deepLink &&
context.timelineOrigin != TimelineOrigin.trash &&
context.timelineOrigin != TimelineOrigin.archive &&
context.timelineOrigin != TimelineOrigin.localAlbum &&
context.isOwner,
ViewerKebabMenuButtonType.cast => context.isCasting || context.asset.hasRemote,
ViewerKebabMenuButtonType.download => context.asset.isRemoteOnly,
};
}
ConsumerWidget buildButton(ViewerKebabMenuButtonContext context, BuildContext buildContext) {
return switch (this) {
ViewerKebabMenuButtonType.openInfo => BaseActionButton(
label: 'info'.tr(),
iconData: Icons.info_outline,
iconColor: context.originalTheme?.iconTheme.color,
menuItem: true,
onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()),
),
ViewerKebabMenuButtonType.viewInTimeline => BaseActionButton(
label: 'view_in_timeline'.t(context: buildContext),
iconData: Icons.image_search,
iconColor: context.originalTheme?.iconTheme.color,
menuItem: true,
onPressed: () async {
await buildContext.maybePop();
await buildContext.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt));
},
),
ViewerKebabMenuButtonType.cast => const CastActionButton(menuItem: true),
ViewerKebabMenuButtonType.download => const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
};
}
}
class ViewerKebabMenuButtonBuilder {
static List<Widget> build(ViewerKebabMenuButtonContext context, BuildContext buildContext, WidgetRef ref) {
final visibleButtons = ViewerKebabMenuButtonType.values.where((type) => type.shouldShow(context)).toList();
if (visibleButtons.isEmpty) return [];
final List<Widget> result = [];
int? lastGroup;
for (final type in visibleButtons) {
if (lastGroup != null && type.group != lastGroup) {
result.add(const Divider(height: 1));
}
result.add(type.buildButton(context, buildContext).build(buildContext, ref));
lastGroup = type.group;
}
return result;
}
}

View File

@@ -81,7 +81,7 @@ Future<void> migrateDatabaseIfNeeded(Isar db, Drift drift) async {
} }
if (version < 19 && Store.isBetaTimelineEnabled) { if (version < 19 && Store.isBetaTimelineEnabled) {
if (!await _populateUpdatedAtTime(drift)) { if (!await _populateLocalAssetTime(drift)) {
return; return;
} }
} }
@@ -229,7 +229,7 @@ Future<void> _migrateDeviceAsset(Isar db) async {
}); });
} }
Future<bool> _populateUpdatedAtTime(Drift db) async { Future<bool> _populateLocalAssetTime(Drift db) async {
try { try {
final nativeApi = NativeSyncApi(); final nativeApi = NativeSyncApi();
final albums = await nativeApi.getAlbums(); final albums = await nativeApi.getAlbums();
@@ -240,6 +240,9 @@ Future<bool> _populateUpdatedAtTime(Drift db) async {
batch.update( batch.update(
db.localAssetEntity, db.localAssetEntity,
LocalAssetEntityCompanion( LocalAssetEntityCompanion(
longitude: Value(asset.longitude),
latitude: Value(asset.latitude),
adjustmentTime: Value(tryFromSecondsSinceEpoch(asset.adjustmentTime, isUtc: true)),
updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()), updatedAt: Value(tryFromSecondsSinceEpoch(asset.updatedAt, isUtc: true) ?? DateTime.timestamp()),
), ),
where: (t) => t.id.equals(asset.id), where: (t) => t.id.equals(asset.id),
@@ -250,7 +253,7 @@ Future<bool> _populateUpdatedAtTime(Drift db) async {
return true; return true;
} catch (error) { } catch (error) {
dPrint(() => "[MIGRATION] Error while populating updatedAt time: $error"); dPrint(() => "[MIGRATION] Error while populating asset time: $error");
return false; return false;
} }
} }

View File

@@ -2,26 +2,27 @@ import 'package:flutter/material.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart';
class EntitiyCountTile extends StatelessWidget { class EntityCountTile extends StatelessWidget {
final int count; final int count;
final String label; final String label;
final IconData icon; final IconData icon;
const EntitiyCountTile({super.key, required this.count, required this.label, required this.icon}); const EntityCountTile({super.key, required this.count, required this.label, required this.icon});
String zeroPadding(int number, int targetWidth) { String zeroPadding(int number, int targetWidth) {
final numStr = number.toString(); final numStr = number.toString();
return numStr.length < targetWidth ? "0" * (targetWidth - numStr.length) : ""; return numStr.length < targetWidth ? "0" * (targetWidth - numStr.length) : "";
} }
int calculateMaxDigits(double availableWidth) {
const double charWidth = 11.0;
return (availableWidth / charWidth).floor().clamp(1, 8);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final screenWidth = MediaQuery.of(context).size.width;
final availableWidth = (screenWidth - 32 - 8) / 2;
const double charWidth = 11.0;
final maxDigits = ((availableWidth - 32) / charWidth).floor().clamp(1, 8);
return Container( return Container(
height: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: context.colorScheme.surfaceContainerLow, color: context.colorScheme.surfaceContainerLow,
@@ -29,7 +30,6 @@ class EntitiyCountTile extends StatelessWidget {
border: Border.all(width: 0.5, color: context.colorScheme.outline.withAlpha(25)), border: Border.all(width: 0.5, color: context.colorScheme.outline.withAlpha(25)),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
// Icon and Label // Icon and Label
@@ -38,33 +38,30 @@ class EntitiyCountTile extends StatelessWidget {
children: [ children: [
Icon(icon, color: context.primaryColor), Icon(icon, color: context.primaryColor),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Flexible(
label, child: Text(
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16), label,
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
),
), ),
], ],
), ),
const SizedBox(height: 12),
// Number // Number
LayoutBuilder( const Spacer(),
builder: (context, constraints) { RichText(
final maxDigits = calculateMaxDigits(constraints.maxWidth); text: TextSpan(
return RichText( style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
text: TextSpan( children: [
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600), TextSpan(
children: [ text: zeroPadding(count, maxDigits),
TextSpan( style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
text: zeroPadding(count, maxDigits),
style: TextStyle(color: context.colorScheme.onSurfaceSecondary.withAlpha(75)),
),
TextSpan(
text: count.toString(),
style: TextStyle(color: context.primaryColor),
),
],
), ),
); TextSpan(
}, text: count.toString(),
style: TextStyle(color: context.primaryColor),
),
],
),
), ),
], ],
), ),

View File

@@ -282,76 +282,87 @@ class _SyncStatsCounts extends ConsumerWidget {
_SectionHeaderText(text: "assets".t(context: context)), _SectionHeaderText(text: "assets".t(context: context)),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex( // 1. Wrap in IntrinsicHeight
direction: Axis.horizontal, child: IntrinsicHeight(
mainAxisAlignment: MainAxisAlignment.spaceBetween, child: Flex(
spacing: 8.0, direction: Axis.horizontal,
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Expanded( // 2. Stretch children vertically to fill the IntrinsicHeight
child: EntitiyCountTile( crossAxisAlignment: CrossAxisAlignment.stretch,
label: "local".t(context: context), spacing: 8.0,
count: localAssetCount, children: [
icon: Icons.smartphone, Expanded(
child: EntityCountTile(
label: "local".t(context: context),
count: localAssetCount,
icon: Icons.smartphone,
),
), ),
), Expanded(
Expanded( child: EntityCountTile(
child: EntitiyCountTile( label: "remote".t(context: context),
label: "remote".t(context: context), count: remoteAssetCount,
count: remoteAssetCount, icon: Icons.cloud,
icon: Icons.cloud, ),
), ),
), ],
], ),
), ),
), ),
_SectionHeaderText(text: "albums".t(context: context)), _SectionHeaderText(text: "albums".t(context: context)),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex( child: IntrinsicHeight(
direction: Axis.horizontal, child: Flex(
mainAxisAlignment: MainAxisAlignment.spaceBetween, direction: Axis.horizontal,
spacing: 8.0, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ crossAxisAlignment: CrossAxisAlignment.stretch, // Added
Expanded( spacing: 8.0,
child: EntitiyCountTile( children: [
label: "local".t(context: context), Expanded(
count: localAlbumCount, child: EntityCountTile(
icon: Icons.smartphone, label: "local".t(context: context),
count: localAlbumCount,
icon: Icons.smartphone,
),
), ),
), Expanded(
Expanded( child: EntityCountTile(
child: EntitiyCountTile( label: "remote".t(context: context),
label: "remote".t(context: context), count: remoteAlbumCount,
count: remoteAlbumCount, icon: Icons.cloud,
icon: Icons.cloud, ),
), ),
), ],
], ),
), ),
), ),
_SectionHeaderText(text: "other".t(context: context)), _SectionHeaderText(text: "other".t(context: context)),
Padding( Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex( child: IntrinsicHeight(
direction: Axis.horizontal, child: Flex(
mainAxisAlignment: MainAxisAlignment.spaceBetween, direction: Axis.horizontal,
spacing: 8.0, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ crossAxisAlignment: CrossAxisAlignment.stretch, // Added
Expanded( spacing: 8.0,
child: EntitiyCountTile( children: [
label: "memories".t(context: context), Expanded(
count: memoryCount, child: EntityCountTile(
icon: Icons.calendar_today, label: "memories".t(context: context),
count: memoryCount,
icon: Icons.calendar_today,
),
), ),
), Expanded(
Expanded( child: EntityCountTile(
child: EntitiyCountTile( label: "hashed_assets".t(context: context),
label: "hashed_assets".t(context: context), count: localHashedCount,
count: localHashedCount, icon: Icons.tag,
icon: Icons.tag, ),
), ),
), ],
], ),
), ),
), ),
// To be removed once the experimental feature is stable // To be removed once the experimental feature is stable
@@ -364,26 +375,29 @@ class _SyncStatsCounts extends ConsumerWidget {
return counts.when( return counts.when(
data: (c) => Padding( data: (c) => Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Flex( child: IntrinsicHeight(
direction: Axis.horizontal, child: Flex(
mainAxisAlignment: MainAxisAlignment.spaceBetween, direction: Axis.horizontal,
spacing: 8.0, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ crossAxisAlignment: CrossAxisAlignment.stretch, // Added
Expanded( spacing: 8.0,
child: EntitiyCountTile( children: [
label: "local".t(context: context), Expanded(
count: c.total, child: EntityCountTile(
icon: Icons.delete_outline, label: "local".t(context: context),
count: c.total,
icon: Icons.delete_outline,
),
), ),
), Expanded(
Expanded( child: EntityCountTile(
child: EntitiyCountTile( label: "hashed_assets".t(context: context),
label: "hashed_assets".t(context: context), count: c.hashed,
count: c.hashed, icon: Icons.tag,
icon: Icons.tag, ),
), ),
), ],
], ),
), ),
), ),
loading: () => const CircularProgressIndicator(), loading: () => const CircularProgressIndicator(),

View File

@@ -27,6 +27,10 @@ class PlatformAsset {
final int orientation; final int orientation;
final bool isFavorite; final bool isFavorite;
final int? adjustmentTime;
final double? latitude;
final double? longitude;
const PlatformAsset({ const PlatformAsset({
required this.id, required this.id,
required this.name, required this.name,
@@ -38,6 +42,9 @@ class PlatformAsset {
this.durationInSeconds = 0, this.durationInSeconds = 0,
this.orientation = 0, this.orientation = 0,
this.isFavorite = false, this.isFavorite = false,
this.adjustmentTime,
this.latitude,
this.longitude,
}); });
} }

View File

@@ -16,6 +16,7 @@ import 'schema_v10.dart' as v10;
import 'schema_v11.dart' as v11; import 'schema_v11.dart' as v11;
import 'schema_v12.dart' as v12; import 'schema_v12.dart' as v12;
import 'schema_v13.dart' as v13; import 'schema_v13.dart' as v13;
import 'schema_v14.dart' as v14;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@@ -47,10 +48,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v12.DatabaseAtV12(db); return v12.DatabaseAtV12(db);
case 13: case 13:
return v13.DatabaseAtV13(db); return v13.DatabaseAtV13(db);
case 14:
return v14.DatabaseAtV14(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]; static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14];
} }

File diff suppressed because it is too large Load Diff

117
pnpm-lock.yaml generated
View File

@@ -20,8 +20,8 @@ importers:
.github: .github:
devDependencies: devDependencies:
prettier: prettier:
specifier: ^3.5.3 specifier: ^3.7.4
version: 3.7.1 version: 3.7.4
cli: cli:
dependencies: dependencies:
@@ -85,7 +85,7 @@ importers:
version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) version: 10.1.8(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-prettier: eslint-plugin-prettier:
specifier: ^5.1.3 specifier: ^5.1.3
version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1) version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4)
eslint-plugin-unicorn: eslint-plugin-unicorn:
specifier: ^62.0.0 specifier: ^62.0.0
version: 62.0.0(eslint@9.39.1(jiti@2.6.1)) version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
@@ -96,11 +96,11 @@ importers:
specifier: ^5.2.0 specifier: ^5.2.0
version: 5.5.0 version: 5.5.0
prettier: prettier:
specifier: ^3.2.5 specifier: ^3.7.4
version: 3.7.1 version: 3.7.4
prettier-plugin-organize-imports: prettier-plugin-organize-imports:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) version: 4.3.0(prettier@3.7.4)(typescript@5.9.3)
typescript: typescript:
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.9.3 version: 5.9.3
@@ -184,8 +184,8 @@ importers:
specifier: ^3.7.0 specifier: ^3.7.0
version: 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 3.9.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
prettier: prettier:
specifier: ^3.2.4 specifier: ^3.7.4
version: 3.7.1 version: 3.7.4
typescript: typescript:
specifier: ^5.1.6 specifier: ^5.1.6
version: 5.9.3 version: 5.9.3
@@ -239,13 +239,13 @@ importers:
version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) version: 10.1.8(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-prettier: eslint-plugin-prettier:
specifier: ^5.1.3 specifier: ^5.1.3
version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1) version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4)
eslint-plugin-unicorn: eslint-plugin-unicorn:
specifier: ^62.0.0 specifier: ^62.0.0
version: 62.0.0(eslint@9.39.1(jiti@2.6.1)) version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
exiftool-vendored: exiftool-vendored:
specifier: ^33.0.0 specifier: ^34.0.0
version: 33.5.0 version: 34.0.0
globals: globals:
specifier: ^16.0.0 specifier: ^16.0.0
version: 16.5.0 version: 16.5.0
@@ -265,11 +265,11 @@ importers:
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.0.0 version: 7.0.0
prettier: prettier:
specifier: ^3.2.5 specifier: ^3.7.4
version: 3.7.1 version: 3.7.4
prettier-plugin-organize-imports: prettier-plugin-organize-imports:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) version: 4.3.0(prettier@3.7.4)(typescript@5.9.3)
sharp: sharp:
specifier: ^0.34.5 specifier: ^0.34.5
version: 0.34.5 version: 0.34.5
@@ -428,8 +428,8 @@ importers:
specifier: 4.3.3 specifier: 4.3.3
version: 4.3.3 version: 4.3.3
exiftool-vendored: exiftool-vendored:
specifier: ^33.0.0 specifier: ^34.0.0
version: 33.5.0 version: 34.0.0
express: express:
specifier: ^5.1.0 specifier: ^5.1.0
version: 5.2.0 version: 5.2.0
@@ -655,7 +655,7 @@ importers:
version: 10.1.8(eslint@9.39.1(jiti@2.6.1)) version: 10.1.8(eslint@9.39.1(jiti@2.6.1))
eslint-plugin-prettier: eslint-plugin-prettier:
specifier: ^5.1.3 specifier: ^5.1.3
version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1) version: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4)
eslint-plugin-unicorn: eslint-plugin-unicorn:
specifier: ^62.0.0 specifier: ^62.0.0
version: 62.0.0(eslint@9.39.1(jiti@2.6.1)) version: 62.0.0(eslint@9.39.1(jiti@2.6.1))
@@ -672,11 +672,11 @@ importers:
specifier: ^7.0.0 specifier: ^7.0.0
version: 7.0.0 version: 7.0.0
prettier: prettier:
specifier: ^3.0.2 specifier: ^3.7.4
version: 3.7.1 version: 3.7.4
prettier-plugin-organize-imports: prettier-plugin-organize-imports:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) version: 4.3.0(prettier@3.7.4)(typescript@5.9.3)
sql-formatter: sql-formatter:
specifier: ^15.0.0 specifier: ^15.0.0
version: 15.6.10 version: 15.6.10
@@ -717,8 +717,8 @@ importers:
specifier: file:../open-api/typescript-sdk specifier: file:../open-api/typescript-sdk
version: link:../open-api/typescript-sdk version: link:../open-api/typescript-sdk
'@immich/ui': '@immich/ui':
specifier: ^0.50.0 specifier: ^0.50.1
version: 0.50.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2) version: 0.50.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)
'@mapbox/mapbox-gl-rtl-text': '@mapbox/mapbox-gl-rtl-text':
specifier: 0.2.3 specifier: 0.2.3
version: 0.2.3(mapbox-gl@1.13.3) version: 0.2.3(mapbox-gl@1.13.3)
@@ -904,17 +904,17 @@ importers:
specifier: ^16.0.0 specifier: ^16.0.0
version: 16.5.0 version: 16.5.0
prettier: prettier:
specifier: ^3.4.2 specifier: ^3.7.4
version: 3.7.1 version: 3.7.4
prettier-plugin-organize-imports: prettier-plugin-organize-imports:
specifier: ^4.0.0 specifier: ^4.0.0
version: 4.3.0(prettier@3.7.1)(typescript@5.9.3) version: 4.3.0(prettier@3.7.4)(typescript@5.9.3)
prettier-plugin-sort-json: prettier-plugin-sort-json:
specifier: ^4.1.1 specifier: ^4.1.1
version: 4.1.1(prettier@3.7.1) version: 4.1.1(prettier@3.7.4)
prettier-plugin-svelte: prettier-plugin-svelte:
specifier: ^3.3.3 specifier: ^3.3.3
version: 3.4.0(prettier@3.7.1)(svelte@5.45.2) version: 3.4.0(prettier@3.7.4)(svelte@5.45.2)
rollup-plugin-visualizer: rollup-plugin-visualizer:
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.5(rollup@4.53.3) version: 6.0.5(rollup@4.53.3)
@@ -2989,8 +2989,8 @@ packages:
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
'@immich/ui@0.50.0': '@immich/ui@0.50.1':
resolution: {integrity: sha512-7AW9SRZTAgal8xlkUAxm7o4+pSG7HcKb+Bh9JpWLaDRRdGyPCZMmsNa9CjZglOQ7wkAD07tQ9u4+zezBLe0dlQ==} resolution: {integrity: sha512-fNlQGh75ZFa/UZAgJaYk9/ItHOXHNNzN4CunjCmE7WocVVkUZbUxopN9Ku3F5GULSqD/zJ5gNO6PQAZ1ZoSaaQ==}
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
@@ -3236,6 +3236,7 @@ packages:
'@koa/router@14.0.0': '@koa/router@14.0.0':
resolution: {integrity: sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==} resolution: {integrity: sha512-LBSu5K0qAaaQcXX/0WIB9PGDevyCxxpnc1uq13vV/CgObaVxuis5hKl3Eboq/8gcb6ebnkAStW9NB/Em2eYyFA==}
engines: {node: '>= 20'} engines: {node: '>= 20'}
deprecated: Please upgrade to v15 or higher. All reported bugs in this version are fixed in newer releases, dependencies have been updated, and security has been improved.
'@koddsson/eslint-plugin-tscompat@0.2.0': '@koddsson/eslint-plugin-tscompat@0.2.0':
resolution: {integrity: sha512-Oqd4kWSX0LiO9wWHjcmDfXZNC7TotFV/tLRhwCFU3XUeb//KYvJ75c9OmeSJ+vBv5lkCeB+xYsqyNrBc5j18XA==} resolution: {integrity: sha512-Oqd4kWSX0LiO9wWHjcmDfXZNC7TotFV/tLRhwCFU3XUeb//KYvJ75c9OmeSJ+vBv5lkCeB+xYsqyNrBc5j18XA==}
@@ -5503,8 +5504,8 @@ packages:
resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==} resolution: {integrity: sha512-a28v2eWrrRWPpJSzxc+mKwm0ZtVx/G8SepdQZDArnXYU/XS+IF6mp8aB/4E+hH1tyGCoDo3KlUCdlSxGDsRkAw==}
hasBin: true hasBin: true
batch-cluster@15.0.1: batch-cluster@16.0.0:
resolution: {integrity: sha512-eUmh0ld1AUPKTEmdzwGF9QTSexXAyt9rA1F5zDfW1wUi3okA3Tal4NLdCeFI6aiKpBenQhR6NmK9bW9tBHTGPQ==} resolution: {integrity: sha512-+T7Ho09ikx/kP4P8M+GEnpuePzRQa4gTUhtPIu6ApFC8+0GY0sri1y1PuB+yfXlQWl5DkHC/e58z3U6g0qCz/A==}
engines: {node: '>=20'} engines: {node: '>=20'}
batch@0.6.1: batch@0.6.1:
@@ -6848,17 +6849,17 @@ packages:
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
engines: {node: '>=10'} engines: {node: '>=10'}
exiftool-vendored.exe@13.42.0: exiftool-vendored.exe@13.43.0:
resolution: {integrity: sha512-6AFybe5IakduMWleuQBfep9OWGSVZSedt2uKL+LzufRsATp+beOF7tZyKtMztjb6VRH1GF/4F9EvBVam6zm70w==} resolution: {integrity: sha512-EENHNz86tYY5yHGPtGB2mto3FIGstQvEhrcU34f7fm4RMxBKNfTWYOGkhU1jzvjOi+V4575LQX/FUES1TwgUbQ==}
os: [win32] os: [win32]
exiftool-vendored.pl@13.42.0: exiftool-vendored.pl@13.43.0:
resolution: {integrity: sha512-EF5IdxQNIJIvZjHf4bG4jnwAHVVSLkYZToo2q+Mm89kSuppKfRvHz/lngIxN0JALE8rFdC4zt6NWY/PKqRdCcg==} resolution: {integrity: sha512-0ApWaQ/pxaliPK7HzTxVA0sg/wZ8vl7UtFVhCyWhGQg01WfZkFrKwKmELB0Bnn01WTfgIuMadba8ccmFvpmJag==}
os: ['!win32'] os: ['!win32']
hasBin: true hasBin: true
exiftool-vendored@33.5.0: exiftool-vendored@34.0.0:
resolution: {integrity: sha512-7cCh6izwdmC5ZaCxpHFehnExIr2Yp7CJuxHg4WFiGcm81yyxXLtvSE+85ep9VsNwhlOtSpk+XxiqrlddjY5lAw==} resolution: {integrity: sha512-rhIe4XGE7kh76nwytwHtq6qK/pc1mpOBHRV++gudFeG2PfAp3XIVQbFWCLK3S4l9I4AWYOe4mxk8mW8l1oHRTw==}
engines: {node: '>=20.0.0'} engines: {node: '>=20.0.0'}
expect-type@1.2.1: expect-type@1.2.1:
@@ -9765,8 +9766,8 @@ packages:
prettier: ^3.0.0 prettier: ^3.0.0
svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0
prettier@3.7.1: prettier@3.7.4:
resolution: {integrity: sha512-RWKXE4qB3u5Z6yz7omJkjWwmTfLdcbv44jUVHC5NpfXwFGzvpQM798FGv/6WNK879tc+Cn0AAyherCl1KjbyZQ==} resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
engines: {node: '>=14'} engines: {node: '>=14'}
hasBin: true hasBin: true
@@ -14517,7 +14518,7 @@ snapshots:
'@fig/complete-commander@3.2.0(commander@11.1.0)': '@fig/complete-commander@3.2.0(commander@11.1.0)':
dependencies: dependencies:
commander: 11.1.0 commander: 11.1.0
prettier: 3.7.1 prettier: 3.7.4
'@floating-ui/core@1.7.3': '@floating-ui/core@1.7.3':
dependencies: dependencies:
@@ -14700,7 +14701,7 @@ snapshots:
dependencies: dependencies:
svelte: 5.45.2 svelte: 5.45.2
'@immich/ui@0.50.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)': '@immich/ui@0.50.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)':
dependencies: dependencies:
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2) '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2)
'@internationalized/date': 3.10.0 '@internationalized/date': 3.10.0
@@ -15788,7 +15789,7 @@ snapshots:
'@react-email/render@1.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': '@react-email/render@1.4.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
dependencies: dependencies:
html-to-text: 9.0.5 html-to-text: 9.0.5
prettier: 3.7.1 prettier: 3.7.4
react: 19.2.0 react: 19.2.0
react-dom: 19.2.0(react@19.2.0) react-dom: 19.2.0(react@19.2.0)
react-promise-suspense: 0.3.4 react-promise-suspense: 0.3.4
@@ -17580,7 +17581,7 @@ snapshots:
baseline-browser-mapping@2.8.31: {} baseline-browser-mapping@2.8.31: {}
batch-cluster@15.0.1: {} batch-cluster@16.0.0: {}
batch@0.6.1: {} batch@0.6.1: {}
@@ -18907,10 +18908,10 @@ snapshots:
lodash.memoize: 4.1.2 lodash.memoize: 4.1.2
semver: 7.7.3 semver: 7.7.3
eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.1): eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.7.4):
dependencies: dependencies:
eslint: 9.39.1(jiti@2.6.1) eslint: 9.39.1(jiti@2.6.1)
prettier: 3.7.1 prettier: 3.7.4
prettier-linter-helpers: 1.0.0 prettier-linter-helpers: 1.0.0
synckit: 0.11.11 synckit: 0.11.11
optionalDependencies: optionalDependencies:
@@ -19128,21 +19129,21 @@ snapshots:
signal-exit: 3.0.7 signal-exit: 3.0.7
strip-final-newline: 2.0.0 strip-final-newline: 2.0.0
exiftool-vendored.exe@13.42.0: exiftool-vendored.exe@13.43.0:
optional: true optional: true
exiftool-vendored.pl@13.42.0: {} exiftool-vendored.pl@13.43.0: {}
exiftool-vendored@33.5.0: exiftool-vendored@34.0.0:
dependencies: dependencies:
'@photostructure/tz-lookup': 11.3.0 '@photostructure/tz-lookup': 11.3.0
'@types/luxon': 3.7.1 '@types/luxon': 3.7.1
batch-cluster: 15.0.1 batch-cluster: 16.0.0
exiftool-vendored.pl: 13.42.0 exiftool-vendored.pl: 13.43.0
he: 1.2.0 he: 1.2.0
luxon: 3.7.2 luxon: 3.7.2
optionalDependencies: optionalDependencies:
exiftool-vendored.exe: 13.42.0 exiftool-vendored.exe: 13.43.0
expect-type@1.2.1: {} expect-type@1.2.1: {}
@@ -22636,21 +22637,21 @@ snapshots:
dependencies: dependencies:
fast-diff: 1.3.0 fast-diff: 1.3.0
prettier-plugin-organize-imports@4.3.0(prettier@3.7.1)(typescript@5.9.3): prettier-plugin-organize-imports@4.3.0(prettier@3.7.4)(typescript@5.9.3):
dependencies: dependencies:
prettier: 3.7.1 prettier: 3.7.4
typescript: 5.9.3 typescript: 5.9.3
prettier-plugin-sort-json@4.1.1(prettier@3.7.1): prettier-plugin-sort-json@4.1.1(prettier@3.7.4):
dependencies: dependencies:
prettier: 3.7.1 prettier: 3.7.4
prettier-plugin-svelte@3.4.0(prettier@3.7.1)(svelte@5.45.2): prettier-plugin-svelte@3.4.0(prettier@3.7.4)(svelte@5.45.2):
dependencies: dependencies:
prettier: 3.7.1 prettier: 3.7.4
svelte: 5.45.2 svelte: 5.45.2
prettier@3.7.1: {} prettier@3.7.4: {}
pretty-error@4.0.0: pretty-error@4.0.0:
dependencies: dependencies:

View File

@@ -50,13 +50,15 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
FROM builder AS plugins FROM builder AS plugins
ARG TARGETPLATFORM
COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY ./plugins/mise.toml ./plugins/ COPY ./plugins/mise.toml ./plugins/
ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml
ENV MISE_DATA_DIR=/buildcache/mise ENV MISE_DATA_DIR=/buildcache/mise
RUN --mount=type=cache,id=mise-tools,target=/buildcache/mise \ RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
mise install --cd plugins mise install --cd plugins
COPY ./plugins ./plugins/ COPY ./plugins ./plugins/
@@ -66,7 +68,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \ --mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \ --mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
--mount=type=cache,id=mise-tools,target=/buildcache/mise \ --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
cd plugins && mise run build cd plugins && mise run build
FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29 FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29

View File

@@ -70,7 +70,7 @@
"cookie": "^1.0.2", "cookie": "^1.0.2",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cron": "4.3.3", "cron": "4.3.3",
"exiftool-vendored": "^33.0.0", "exiftool-vendored": "^34.0.0",
"express": "^5.1.0", "express": "^5.1.0",
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fluent-ffmpeg": "^2.1.2", "fluent-ffmpeg": "^2.1.2",
@@ -153,7 +153,7 @@
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"node-gyp": "^12.0.0", "node-gyp": "^12.0.0",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"prettier": "^3.0.2", "prettier": "^3.7.4",
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"sql-formatter": "^15.0.0", "sql-formatter": "^15.0.0",
"supertest": "^7.1.0", "supertest": "^7.1.0",

View File

@@ -2,7 +2,7 @@ import { asOptions } from 'src/sql-tools/helpers';
import { register } from 'src/sql-tools/register'; import { register } from 'src/sql-tools/register';
import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types'; import { ColumnStorage, ColumnType, DatabaseEnum } from 'src/sql-tools/types';
export type ColumnValue = null | boolean | string | number | object | Date | (() => string); export type ColumnValue = null | boolean | string | number | Array<unknown> | object | Date | (() => string);
export type ColumnBaseOptions = { export type ColumnBaseOptions = {
name?: string; name?: string;

View File

@@ -39,6 +39,10 @@ export const fromColumnValue = (columnValue?: ColumnValue) => {
return `'${value.toISOString()}'`; return `'${value.toISOString()}'`;
} }
if (Array.isArray(value)) {
return "'{}'";
}
return `'${String(value)}'`; return `'${String(value)}'`;
}; };

View File

@@ -394,6 +394,20 @@ describe(schemaDiff.name, () => {
expect(diff.items).toEqual([]); expect(diff.items).toEqual([]);
}); });
it('should support arrays, ignoring types', () => {
const diff = schemaDiff(
fromColumn({ name: 'column1', type: 'character varying', isArray: true, default: "'{}'" }),
fromColumn({
name: 'column1',
type: 'character varying',
isArray: true,
default: "'{}'::character varying[]",
}),
);
expect(diff.items).toEqual([]);
});
}); });
}); });

View File

@@ -0,0 +1,40 @@
import { Column, DatabaseSchema, Table } from 'src/sql-tools';
@Table()
export class Table1 {
@Column({ type: 'character varying', array: true, default: [] })
column1!: string[];
}
export const description = 'should register a table with a column with a default value (array)';
export const schema: DatabaseSchema = {
databaseName: 'postgres',
schemaName: 'public',
functions: [],
enums: [],
extensions: [],
parameters: [],
overrides: [],
tables: [
{
name: 'table1',
columns: [
{
name: 'column1',
tableName: 'table1',
type: 'character varying',
nullable: false,
isArray: true,
primary: false,
synchronize: true,
default: "'{}'",
},
],
indexes: [],
triggers: [],
constraints: [],
synchronize: true,
},
],
warnings: [],
};

View File

@@ -28,7 +28,7 @@
"@formatjs/icu-messageformat-parser": "^2.9.8", "@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/justified-layout-wasm": "^0.4.3", "@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.50.0", "@immich/ui": "^0.50.1",
"@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.14.0", "@photo-sphere-viewer/core": "^5.14.0",
@@ -93,7 +93,7 @@
"factory.ts": "^1.4.1", "factory.ts": "^1.4.1",
"globals": "^16.0.0", "globals": "^16.0.0",
"happy-dom": "^20.0.0", "happy-dom": "^20.0.0",
"prettier": "^3.4.2", "prettier": "^3.7.4",
"prettier-plugin-organize-imports": "^4.0.0", "prettier-plugin-organize-imports": "^4.0.0",
"prettier-plugin-sort-json": "^4.1.1", "prettier-plugin-sort-json": "^4.1.1",
"prettier-plugin-svelte": "^3.3.3", "prettier-plugin-svelte": "^3.3.3",

View File

@@ -98,7 +98,7 @@
<ControlAppBar showBackButton={false}> <ControlAppBar showBackButton={false}>
{#snippet leading()} {#snippet leading()}
<a data-sveltekit-preload-data="hover" class="ms-4" href="/"> <a data-sveltekit-preload-data="hover" class="ms-4" href="/">
<Logo variant="inline" /> <Logo variant="inline" class="min-w-min" />
</a> </a>
{/snippet} {/snippet}

View File

@@ -32,7 +32,7 @@
</script> </script>
<tr <tr
class="flex h-12 w-full place-items-center border-3 border-transparent p-2 text-center even:bg-subtle/20 odd:bg-subtle/80 hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5" class="flex w-full place-items-center border-3 border-transparent p-2 text-center even:bg-subtle/20 odd:bg-subtle/80 hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:px-5 md:py-2"
onclick={() => goto(resolve(`${AppRoute.ALBUMS}/${album.id}`))} onclick={() => goto(resolve(`${AppRoute.ALBUMS}/${album.id}`))}
{oncontextmenu} {oncontextmenu}
> >

View File

@@ -126,6 +126,7 @@
const onMouseLeave = () => { const onMouseLeave = () => {
mouseOver = false; mouseOver = false;
onMouseEvent?.({ isMouseOver: false, selectedGroupIndex: groupIndex });
}; };
let timer: ReturnType<typeof setTimeout> | null = null; let timer: ReturnType<typeof setTimeout> | null = null;

View File

@@ -9,9 +9,9 @@
import { generateId } from '$lib/utils/generate-id'; import { generateId } from '$lib/utils/generate-id';
import { getMetadataSearchQuery } from '$lib/utils/metadata-search'; import { getMetadataSearchQuery } from '$lib/utils/metadata-search';
import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk'; import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk';
import { IconButton, modalManager } from '@immich/ui'; import { Button, IconButton, modalManager } from '@immich/ui';
import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js'; import { mdiClose, mdiMagnify, mdiTune } from '@mdi/js';
import { onDestroy, tick } from 'svelte'; import { onDestroy, onMount, tick } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import SearchHistoryBox from './search-history-box.svelte'; import SearchHistoryBox from './search-history-box.svelte';
@@ -31,6 +31,8 @@
let isSearchSuggestions = $state(false); let isSearchSuggestions = $state(false);
let selectedId: string | undefined = $state(); let selectedId: string | undefined = $state();
let close: (() => Promise<void>) | undefined; let close: (() => Promise<void>) | undefined;
let showSearchTypeDropdown = $state(false);
let currentSearchType = $state('smart');
const listboxId = generateId(); const listboxId = generateId();
const searchTypeId = generateId(); const searchTypeId = generateId();
@@ -70,6 +72,7 @@
const onFocusIn = () => { const onFocusIn = () => {
searchStore.isSearchEnabled = true; searchStore.isSearchEnabled = true;
getSearchType();
}; };
const onFocusOut = () => { const onFocusOut = () => {
@@ -98,6 +101,9 @@
const searchResult = await result.onClose; const searchResult = await result.onClose;
close = undefined; close = undefined;
// Refresh search type after modal closes
getSearchType();
if (!searchResult) { if (!searchResult) {
return; return;
} }
@@ -139,6 +145,7 @@
const onEscape = () => { const onEscape = () => {
closeDropdown(); closeDropdown();
closeSearchTypeDropdown();
}; };
const onArrow = async (direction: 1 | -1) => { const onArrow = async (direction: 1 | -1) => {
@@ -168,6 +175,20 @@
searchHistoryBox?.clearSelection(); searchHistoryBox?.clearSelection();
}; };
const toggleSearchTypeDropdown = () => {
showSearchTypeDropdown = !showSearchTypeDropdown;
};
const closeSearchTypeDropdown = () => {
showSearchTypeDropdown = false;
};
const selectSearchType = (type: string) => {
localStorage.setItem('searchQueryType', type);
currentSearchType = type;
showSearchTypeDropdown = false;
};
const onsubmit = (event: Event) => { const onsubmit = (event: Event) => {
event.preventDefault(); event.preventDefault();
onSubmit(); onSubmit();
@@ -180,17 +201,18 @@
case 'metadata': case 'metadata':
case 'description': case 'description':
case 'ocr': { case 'ocr': {
currentSearchType = searchType;
return searchType; return searchType;
} }
default: { default: {
currentSearchType = 'smart';
return 'smart'; return 'smart';
} }
} }
} }
function getSearchTypeText(): string { function getSearchTypeText(): string {
const searchType = getSearchType(); switch (currentSearchType) {
switch (searchType) {
case 'smart': { case 'smart': {
return $t('context'); return $t('context');
} }
@@ -203,8 +225,22 @@
case 'ocr': { case 'ocr': {
return $t('ocr'); return $t('ocr');
} }
default: {
return $t('context');
}
} }
} }
onMount(() => {
getSearchType();
});
const searchTypes = [
{ value: 'smart', label: () => $t('context') },
{ value: 'metadata', label: () => $t('filename') },
{ value: 'description', label: () => $t('description') },
{ value: 'ocr', label: () => $t('ocr') },
] as const;
</script> </script>
<svelte:document <svelte:document
@@ -293,11 +329,34 @@
class:max-md:hidden={value} class:max-md:hidden={value}
class:end-28={value.length > 0} class:end-28={value.length > 0}
> >
<p <div class="relative">
class="bg-immich-primary text-white dark:bg-immich-dark-primary/90 dark:text-black/75 rounded-full px-3 py-1 text-xs" <Button
> class="bg-immich-primary text-white dark:bg-immich-dark-primary/90 dark:text-black/75 rounded-full px-3 py-1 text-xs hover:opacity-80 transition-opacity cursor-pointer"
{getSearchTypeText()} onclick={toggleSearchTypeDropdown}
</p> aria-expanded={showSearchTypeDropdown}
aria-haspopup="listbox"
>
{getSearchTypeText()}
</Button>
{#if showSearchTypeDropdown}
<div
class="absolute top-full right-0 mt-1 bg-white dark:bg-immich-dark-gray border border-gray-200 dark:border-gray-600 rounded-lg shadow-lg py-1 min-w-32 z-9999"
use:focusOutside={{ onFocusOut: closeSearchTypeDropdown }}
>
{#each searchTypes as searchType (searchType.value)}
<button
type="button"
class="w-full text-left px-3 py-2 text-xs hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors
{currentSearchType === searchType.value ? 'bg-gray-100 dark:bg-gray-700' : ''}"
onclick={() => selectSearchType(searchType.value)}
>
{searchType.label()}
</button>
{/each}
</div>
{/if}
</div>
</div> </div>
{/if} {/if}

View File

@@ -3,7 +3,6 @@ import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import { queueManager } from '$lib/managers/queue-manager.svelte'; import { queueManager } from '$lib/managers/queue-manager.svelte';
import JobCreateModal from '$lib/modals/JobCreateModal.svelte'; import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
import { user } from '$lib/stores/user.store';
import type { HeaderButtonActionItem } from '$lib/types'; import type { HeaderButtonActionItem } from '$lib/types';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
@@ -33,7 +32,6 @@ import {
mdiPlay, mdiPlay,
mdiPlus, mdiPlus,
mdiStateMachine, mdiStateMachine,
mdiSync,
mdiTable, mdiTable,
mdiTagFaces, mdiTagFaces,
mdiTrashCanOutline, mdiTrashCanOutline,
@@ -41,7 +39,6 @@ import {
mdiVideo, mdiVideo,
} from '@mdi/js'; } from '@mdi/js';
import type { MessageFormatter } from 'svelte-i18n'; import type { MessageFormatter } from 'svelte-i18n';
import { get } from 'svelte/store';
type QueueItem = { type QueueItem = {
icon: IconLike; icon: IconLike;