Merge branch 'main' into feat/mobile-ocr

This commit is contained in:
Yaros
2026-04-22 18:08:10 +02:00
1565 changed files with 43096 additions and 79581 deletions
Submodule mobile/.isar deleted from 6643d064ab
-859
View File
@@ -1,859 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "autocfg"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
[[package]]
name = "bindgen"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36d860121800b2a9a94f9b5604b332d5cffb234ce17609ea479d723dbc9d3885"
dependencies = [
"bitflags 1.3.2",
"cexpr",
"clang-sys",
"lazy_static",
"lazycell",
"peeking_take_while",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 1.0.109",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "block"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
[[package]]
name = "cc"
version = "1.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d74707dde2ba56f86ae90effb3b43ddd369504387e718014de010cec7959800"
dependencies = [
"shlex",
]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading",
]
[[package]]
name = "cmake"
version = "0.1.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a"
dependencies = [
"cc",
]
[[package]]
name = "combine"
version = "4.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
dependencies = [
"bytes",
"memchr",
]
[[package]]
name = "crossbeam-channel"
version = "0.5.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
[[package]]
name = "dirs"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
dependencies = [
"libc",
"redox_users",
"winapi",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "either"
version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
[[package]]
name = "enum_dispatch"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "float_next_after"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fc612c5837986b7104a87a0df74a5460931f1c5274be12f8d0f40aa2f30d632"
dependencies = [
"num-traits",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "glob"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "hermit-abi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "intmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee87fd093563344074bacf24faa0bb0227fb6969fb223e922db798516de924d6"
[[package]]
name = "isar"
version = "0.0.0"
dependencies = [
"dirs",
"intmap",
"isar-core",
"itertools",
"jni",
"ndk-context",
"objc",
"objc-foundation",
"once_cell",
"paste",
"serde_json",
"threadpool",
"unicode-segmentation",
]
[[package]]
name = "isar-core"
version = "0.0.0"
dependencies = [
"byteorder",
"cfg-if",
"crossbeam-channel",
"enum_dispatch",
"float_next_after",
"intmap",
"itertools",
"libc",
"mdbx-sys",
"once_cell",
"paste",
"rand",
"serde",
"serde_json",
"snafu",
"widestring",
"xxhash-rust",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "jni"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "039022cdf4d7b1cf548d31f60ae783138e5fd42013f6271049d7df7afadef96c"
dependencies = [
"cesu8",
"combine",
"jni-sys",
"log",
"thiserror",
"walkdir",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "libc"
version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]]
name = "libloading"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4"
dependencies = [
"cfg-if",
"windows-targets",
]
[[package]]
name = "libredox"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
dependencies = [
"bitflags 2.6.0",
"libc",
]
[[package]]
name = "log"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "malloc_buf"
version = "0.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
dependencies = [
"libc",
]
[[package]]
name = "mdbx-sys"
version = "0.0.0"
dependencies = [
"bindgen",
"cc",
"cmake",
"libc",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "ndk-context"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
dependencies = [
"hermit-abi",
"libc",
]
[[package]]
name = "objc"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
dependencies = [
"malloc_buf",
]
[[package]]
name = "objc-foundation"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9"
dependencies = [
"block",
"objc",
"objc_id",
]
[[package]]
name = "objc_id"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b"
dependencies = [
"objc",
]
[[package]]
name = "once_cell"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ea5043e58958ee56f3e15a90aee535795cd7dfd319846288d93c5b57d85cbe"
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "peeking_take_while"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
[[package]]
name = "ppv-lite86"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]]
name = "regex"
version = "1.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.210"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "serde_json"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "snafu"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6"
dependencies = [
"doc-comment",
"snafu-derive",
]
[[package]]
name = "snafu-derive"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "syn"
version = "1.0.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "syn"
version = "2.0.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
[[package]]
name = "threadpool"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa"
dependencies = [
"num_cpus",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "unicode-segmentation"
version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "widestring"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311"
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "xxhash-rust"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a5cbf750400958819fb6178eaa83bee5cd9c29a26a40cc241df8c70fdd46984"
[[package]]
name = "zerocopy"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
dependencies = [
"byteorder",
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.7.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.77",
]
+1 -1
View File
@@ -1,5 +1,5 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.35.7",
"dart.flutterSdkPath": ".fvm/versions/3.41.7",
"dart.lineLength": 120,
"[dart]": {
"editor.rulers": [
-79
View File
@@ -52,90 +52,11 @@ analyzer:
unawaited_futures: warning
custom_lint:
debug: true
rules:
- avoid_build_context_in_providers: false
- avoid_public_notifier_properties: false
- avoid_manual_providers_as_generated_provider_dependency: false
- unsupported_provider_value: false
- import_rule_photo_manager:
message: photo_manager must only be used in MediaRepositories
restrict: package:photo_manager
allowed:
# required / wanted
- 'lib/infrastructure/repositories/album_media.repository.dart'
- 'lib/infrastructure/repositories/{storage,asset_media}.repository.dart'
- 'lib/repositories/{album,asset,file}_media.repository.dart'
# acceptable exceptions for the time being
- lib/entities/asset.entity.dart # to provide local AssetEntity for now
- lib/providers/image/immich_local_{image,thumbnail}_provider.dart # accesses thumbnails via PhotoManager
# refactor to make the providers and services testable
- lib/providers/backup/{backup,manual_upload}.provider.dart # uses only PMProgressHandler
- lib/services/{background,backup}.service.dart # uses only PMProgressHandler
- test/**.dart
- import_rule_isar:
message: isar must only be used in entities and repositories
restrict: package:isar
allowed:
# required / wanted
- lib/entities/*.entity.dart
- lib/repositories/{album,asset,backup,database,etag,exif_info,user,timeline,partner}.repository.dart
- lib/infrastructure/entities/*.entity.dart
- lib/infrastructure/repositories/*.repository.dart
- lib/providers/infrastructure/db.provider.dart
# acceptable exceptions for the time being (until Isar is fully replaced)
- lib/providers/app_life_cycle.provider.dart
- integration_test/test_utils/general_helper.dart
- lib/domain/services/background_worker.service.dart
- lib/main.dart
- lib/pages/album/album_asset_selection.page.dart
- lib/routing/router.dart
- lib/services/immich_logger.service.dart # not really a service... more a util
- lib/utils/{db,migration}.dart
- lib/utils/bootstrap.dart
- lib/widgets/asset_grid/asset_grid_data_structure.dart
- test/**.dart
# refactor the remaining providers
- lib/providers/db.provider.dart
- import_rule_openapi:
message: openapi must only be used through ApiRepositories
restrict: package:openapi
allowed:
# required / wanted
- lib/repositories/*_api.repository.dart
- lib/domain/models/sync_event.model.dart
- lib/{domain,infrastructure}/**/sync_stream.*
- lib/{domain,infrastructure}/**/sync_api.*
- lib/infrastructure/repositories/*_api.repository.dart
- lib/infrastructure/utils/*.converter.dart
# acceptable exceptions for the time being
- lib/entities/{album,asset,exif_info,user}.entity.dart # to convert DTOs to entities
- lib/infrastructure/utils/*.converter.dart
- lib/utils/{image_url_builder,openapi_patching}.dart # utils are fine
- test/modules/utils/openapi_patching_test.dart # filename is self-explanatory...
- lib/domain/services/sync_stream.service.dart # Making sure to comply with the type from database
- lib/domain/services/search.service.dart
# refactor
- lib/models/map/map_marker.model.dart
- lib/models/server_info/server_{config,disk_info,features,version}.model.dart
- lib/models/shared_link/shared_link.model.dart
- lib/providers/asset_viewer/asset_people.provider.dart
- lib/providers/auth.provider.dart
- lib/providers/image/immich_remote_{image,thumbnail}_provider.dart
- lib/providers/map/map_state.provider.dart
- lib/providers/search/{search,search_filter}.provider.dart
- lib/providers/websocket.provider.dart
- lib/routing/auth_guard.dart
- lib/services/{api,asset,backup,memory,oauth,search,shared_link,stack,trash}.service.dart
- lib/widgets/album/album_thumbnail_listtile.dart
- lib/widgets/forms/login/login_form.dart
- lib/widgets/search/search_filter/{camera_picker,location_picker,people_picker}.dart
- lib/services/auth.service.dart # on ApiException
- test/services/auth.service_test.dart # on ApiException
# allow import from test
- test/**.dart
dart_code_metrics:
rules:
+35 -64
View File
@@ -1,27 +1,10 @@
plugins {
id "com.android.application"
id "kotlin-android"
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id "dev.flutter.flutter-gradle-plugin"
id 'com.google.devtools.ksp'
id 'org.jetbrains.kotlin.plugin.serialization'
id 'org.jetbrains.kotlin.plugin.compose' version '2.0.20' // this version matches your Kotlin version
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withInputStream { localProperties.load(it) }
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
alias(libs.plugins.ksp)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlin.compose)
}
def keystoreProperties = new Properties()
@@ -31,8 +14,8 @@ if (keystorePropertiesFile.exists()) {
}
android {
compileSdkVersion 35
ndkVersion = "28.2.13676358"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
@@ -55,10 +38,10 @@ android {
defaultConfig {
applicationId "app.alextran.immich"
minSdkVersion 26
targetSdkVersion 35
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
minSdk = 26
targetSdk = flutter.targetSdkVersion
versionCode flutter.versionCode
versionName flutter.versionName
}
signingConfigs {
@@ -67,10 +50,10 @@ android {
def keyPasswordVal = System.getenv("ANDROID_KEY_PASSWORD")
def storePasswordVal = System.getenv("ANDROID_STORE_PASSWORD")
keyAlias keyAliasVal ? keyAliasVal : keystoreProperties['keyAlias']
keyPassword keyPasswordVal ? keyPasswordVal : keystoreProperties['keyPassword']
storeFile file("../key.jks") ? file("../key.jks") : file(keystoreProperties['storeFile'])
storePassword storePasswordVal ? storePasswordVal : keystoreProperties['storePassword']
keyAlias keyAliasVal ?: keystoreProperties['keyAlias']
keyPassword keyPasswordVal ?: keystoreProperties['keyPassword']
storeFile file("../key.jks").exists() ? file("../key.jks") : file(keystoreProperties['storeFile'] ?: '../key.jks')
storePassword storePasswordVal ?: keystoreProperties['storePassword']
}
}
@@ -99,43 +82,31 @@ flutter {
}
dependencies {
def kotlin_version = '2.0.20'
def kotlin_coroutines_version = '1.9.0'
def work_version = '2.9.1'
def concurrent_version = '1.2.0'
def guava_version = '33.3.1-android'
def glide_version = '4.16.0'
def serialization_version = '1.8.1'
def compose_version = '1.1.1'
def gson_version = '2.10.1'
def okhttp_version = '4.12.0'
implementation libs.okhttp
implementation libs.cronet.embedded
implementation libs.media3.datasource.okhttp
implementation libs.media3.datasource.cronet
implementation libs.kotlinx.coroutines.android
implementation libs.work.runtime.ktx
implementation libs.concurrent.futures
implementation libs.guava
implementation libs.glide
implementation libs.kotlinx.serialization.json
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation 'org.chromium.net:cronet-embedded:143.7445.0'
implementation("androidx.media3:media3-datasource-okhttp:1.10.0")
implementation("androidx.media3:media3-datasource-cronet:1.10.0")
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "androidx.concurrent:concurrent-futures:$concurrent_version"
implementation "com.google.guava:guava:$guava_version"
implementation "com.github.bumptech.glide:glide:$glide_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version"
ksp "com.github.bumptech.glide:ksp:$glide_version"
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.2'
ksp libs.glide.ksp
coreLibraryDesugaring libs.desugar.jdk.libs
//Glance Widget
implementation "androidx.glance:glance-appwidget:$compose_version"
implementation "com.google.code.gson:gson:$gson_version"
implementation libs.glance.appwidget
implementation libs.gson
// Glance Configure
implementation "androidx.activity:activity-compose:1.8.2"
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.ui:ui-tooling:$compose_version"
implementation "androidx.compose.material3:material3:1.2.1"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.6.2"
implementation "com.google.android.material:material:1.12.0"
implementation libs.activity.compose
implementation libs.compose.ui
implementation libs.compose.ui.tooling
implementation libs.compose.material3
implementation libs.lifecycle.runtime.ktx
implementation libs.material
}
// This is uncommented in F-Droid build script
@@ -39,10 +39,6 @@
android:exported="false"
android:foregroundServiceType="dataSync|shortService" />
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/notification_icon" />
@@ -1,389 +0,0 @@
package app.alextran.immich
import android.app.Activity
import android.content.ContentResolver
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.provider.MediaStore
import android.provider.Settings
import android.util.Log
import androidx.annotation.RequiresApi
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.PluginRegistry
import java.security.MessageDigest
import java.io.FileInputStream
import kotlinx.coroutines.*
import androidx.core.net.toUri
/**
* Android plugin for Dart `BackgroundService` and file trash operations
*/
class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware, PluginRegistry.ActivityResultListener {
private var methodChannel: MethodChannel? = null
private var fileTrashChannel: MethodChannel? = null
private var context: Context? = null
private var pendingResult: Result? = null
private val permissionRequestCode = 1001
private val trashRequestCode = 1002
private var activityBinding: ActivityPluginBinding? = null
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
onAttachedToEngine(binding.applicationContext, binding.binaryMessenger)
}
private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) {
context = ctx
methodChannel = MethodChannel(messenger, "immich/foregroundChannel")
methodChannel?.setMethodCallHandler(this)
// Add file trash channel
fileTrashChannel = MethodChannel(messenger, "file_trash")
fileTrashChannel?.setMethodCallHandler(this)
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
onDetachedFromEngine()
}
private fun onDetachedFromEngine() {
methodChannel?.setMethodCallHandler(null)
methodChannel = null
fileTrashChannel?.setMethodCallHandler(null)
fileTrashChannel = null
}
override fun onMethodCall(call: MethodCall, result: Result) {
val ctx = context!!
when (call.method) {
// Existing BackgroundService methods
"enable" -> {
val args = call.arguments<ArrayList<*>>()!!
ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true)
.putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args[0] as Long)
.putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args[1] as String)
.apply()
ContentObserverWorker.enable(ctx, immediate = args[2] as Boolean)
result.success(true)
}
"configure" -> {
val args = call.arguments<ArrayList<*>>()!!
val requireUnmeteredNetwork = args[0] as Boolean
val requireCharging = args[1] as Boolean
val triggerUpdateDelay = (args[2] as Number).toLong()
val triggerMaxDelay = (args[3] as Number).toLong()
ContentObserverWorker.configureWork(
ctx,
requireUnmeteredNetwork,
requireCharging,
triggerUpdateDelay,
triggerMaxDelay
)
result.success(true)
}
"disable" -> {
ContentObserverWorker.disable(ctx)
BackupWorker.stopWork(ctx)
result.success(true)
}
"isEnabled" -> {
result.success(ContentObserverWorker.isEnabled(ctx))
}
"isIgnoringBatteryOptimizations" -> {
result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx))
}
"digestFiles" -> {
val args = call.arguments<ArrayList<String>>()!!
GlobalScope.launch(Dispatchers.IO) {
val buf = ByteArray(BUFFER_SIZE)
val digest: MessageDigest = MessageDigest.getInstance("SHA-1")
val hashes = arrayOfNulls<ByteArray>(args.size)
for (i in args.indices) {
val path = args[i]
var len = 0
try {
val file = FileInputStream(path)
file.use { assetFile ->
while (true) {
len = assetFile.read(buf)
if (len != BUFFER_SIZE) break
digest.update(buf)
}
}
digest.update(buf, 0, len)
hashes[i] = digest.digest()
} catch (e: Exception) {
// skip this file
Log.w(TAG, "Failed to hash file ${args[i]}: $e")
}
}
result.success(hashes.asList())
}
}
// File Trash methods moved from MainActivity
"moveToTrash" -> {
val mediaUrls = call.argument<List<String>>("mediaUrls")
if (mediaUrls != null) {
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
moveToTrash(mediaUrls, result)
} else {
result.error("PERMISSION_DENIED", "Media permission required", null)
}
} else {
result.error("INVALID_NAME", "The mediaUrls is not specified.", null)
}
}
"restoreFromTrash" -> {
val fileName = call.argument<String>("fileName")
val type = call.argument<Int>("type")
val mediaId = call.argument<String>("mediaId")
if (fileName != null && type != null) {
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
restoreFromTrash(fileName, type, result)
} else {
result.error("PERMISSION_DENIED", "Media permission required", null)
}
} else
if (mediaId != null && type != null) {
if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) && hasManageMediaPermission()) {
restoreFromTrashById(mediaId, type, result)
} else {
result.error("PERMISSION_DENIED", "Media permission required", null)
}
} else {
result.error("INVALID_PARAMS", "Required params are not specified.", null)
}
}
"requestManageMediaPermission" -> {
if (!hasManageMediaPermission()) {
requestManageMediaPermission(result)
} else {
Log.e("Manage storage permission", "Permission already granted")
result.success(true)
}
}
"hasManageMediaPermission" -> {
if (hasManageMediaPermission()) {
Log.i("Manage storage permission", "Permission already granted")
result.success(true)
} else {
result.success(false)
}
}
"manageMediaPermission" -> requestManageMediaPermission(result)
else -> result.notImplemented()
}
}
private fun hasManageMediaPermission(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
MediaStore.canManageMedia(context!!);
} else {
false
}
}
private fun requestManageMediaPermission(result: Result) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
pendingResult = result // Store the result callback
val activity = activityBinding?.activity ?: return
val intent = Intent(Settings.ACTION_REQUEST_MANAGE_MEDIA)
intent.data = "package:${activity.packageName}".toUri()
activity.startActivityForResult(intent, permissionRequestCode)
} else {
result.success(false)
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun moveToTrash(mediaUrls: List<String>, result: Result) {
val urisToTrash = mediaUrls.map { it.toUri() }
if (urisToTrash.isEmpty()) {
result.error("INVALID_ARGS", "No valid URIs provided", null)
return
}
toggleTrash(urisToTrash, true, result);
}
@RequiresApi(Build.VERSION_CODES.R)
private fun restoreFromTrash(name: String, type: Int, result: Result) {
val uri = getTrashedFileUri(name, type)
if (uri == null) {
Log.e("TrashError", "Asset Uri cannot be found obtained")
result.error("TrashError", "Asset Uri cannot be found obtained", null)
return
}
Log.e("FILE_URI", uri.toString())
uri.let { toggleTrash(listOf(it), false, result) }
}
@RequiresApi(Build.VERSION_CODES.R)
private fun restoreFromTrashById(mediaId: String, type: Int, result: Result) {
val id = mediaId.toLongOrNull()
if (id == null) {
result.error("INVALID_ID", "The file id is not a valid number: $mediaId", null)
return
}
if (!isInTrash(id)) {
result.error("TrashNotFound", "Item with id=$id not found in trash", null)
return
}
val uri = ContentUris.withAppendedId(contentUriForType(type), id)
try {
Log.i(TAG, "restoreFromTrashById: uri=$uri (type=$type,id=$id)")
restoreUris(listOf(uri), result)
} catch (e: Exception) {
Log.w(TAG, "restoreFromTrashById failed", e)
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun toggleTrash(contentUris: List<Uri>, isTrashed: Boolean, result: Result) {
val activity = activityBinding?.activity
val contentResolver = context?.contentResolver
if (activity == null || contentResolver == null) {
result.error("TrashError", "Activity or ContentResolver not available", null)
return
}
try {
val pendingIntent = MediaStore.createTrashRequest(contentResolver, contentUris, isTrashed)
pendingResult = result // Store for onActivityResult
activity.startIntentSenderForResult(
pendingIntent.intentSender,
trashRequestCode,
null, 0, 0, 0
)
} catch (e: Exception) {
Log.e("TrashError", "Error creating or starting trash request", e)
result.error("TrashError", "Error creating or starting trash request", null)
}
}
@RequiresApi(Build.VERSION_CODES.R)
private fun getTrashedFileUri(fileName: String, type: Int): Uri? {
val contentResolver = context?.contentResolver ?: return null
val queryUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val projection = arrayOf(MediaStore.Files.FileColumns._ID)
val queryArgs = Bundle().apply {
putString(
ContentResolver.QUERY_ARG_SQL_SELECTION,
"${MediaStore.Files.FileColumns.DISPLAY_NAME} = ?"
)
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(fileName))
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
}
contentResolver.query(queryUri, projection, queryArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
return ContentUris.withAppendedId(contentUriForType(type), id)
}
}
return null
}
// ActivityAware implementation
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activityBinding = binding
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivityForConfigChanges() {
activityBinding?.removeActivityResultListener(this)
activityBinding = null
}
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activityBinding = binding
binding.addActivityResultListener(this)
}
override fun onDetachedFromActivity() {
activityBinding?.removeActivityResultListener(this)
activityBinding = null
}
// ActivityResultListener implementation
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
if (requestCode == permissionRequestCode) {
val granted = hasManageMediaPermission()
pendingResult?.success(granted)
pendingResult = null
return true
}
if (requestCode == trashRequestCode) {
val approved = resultCode == Activity.RESULT_OK
pendingResult?.success(approved)
pendingResult = null
return true
}
return false
}
@RequiresApi(Build.VERSION_CODES.R)
private fun isInTrash(id: Long): Boolean {
val contentResolver = context?.contentResolver ?: return false
val filesUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
val args = Bundle().apply {
putString(ContentResolver.QUERY_ARG_SQL_SELECTION, "${MediaStore.Files.FileColumns._ID}=?")
putStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS, arrayOf(id.toString()))
putInt(MediaStore.QUERY_ARG_MATCH_TRASHED, MediaStore.MATCH_ONLY)
putInt(ContentResolver.QUERY_ARG_LIMIT, 1)
}
return contentResolver.query(filesUri, arrayOf(MediaStore.Files.FileColumns._ID), args, null)
?.use { it.moveToFirst() } == true
}
@RequiresApi(Build.VERSION_CODES.R)
private fun restoreUris(uris: List<Uri>, result: Result) {
if (uris.isEmpty()) {
result.error("TrashError", "No URIs to restore", null)
return
}
Log.i(TAG, "restoreUris: count=${uris.size}, first=${uris.first()}")
toggleTrash(uris, false, result)
}
@RequiresApi(Build.VERSION_CODES.Q)
private fun contentUriForType(type: Int): Uri =
when (type) {
// same order as AssetType from dart
1 -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
2 -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
3 -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
else -> MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
}
}
private const val TAG = "BackgroundServicePlugin"
private const val BUFFER_SIZE = 2 * 1024 * 1024
@@ -1,394 +0,0 @@
package app.alextran.immich
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.PowerManager
import android.os.SystemClock
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.concurrent.futures.ResolvableFuture
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ForegroundInfo
import androidx.work.ListenableWorker
import androidx.work.NetworkType
import androidx.work.WorkerParameters
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkInfo
import com.google.common.util.concurrent.ListenableFuture
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.embedding.engine.loader.FlutterLoader
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.view.FlutterCallbackInformation
import java.util.concurrent.TimeUnit
/**
* Worker executed by Android WorkManager to perform backup in background
*
* Starts the Dart runtime/engine and calls `_nativeEntry` function in
* `background.service.dart` to run the actual backup logic.
* Called by Android WorkManager when all constraints for the work are met,
* i.e. battery is not low and optionally Wifi and charging are active.
*/
class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params),
MethodChannel.MethodCallHandler {
private val resolvableFuture = ResolvableFuture.create<Result>()
private var engine: FlutterEngine? = null
private lateinit var backgroundChannel: MethodChannel
private val notificationManager =
ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext)
private var timeBackupStarted: Long = 0L
private var notificationBuilder: NotificationCompat.Builder? = null
private var notificationDetailBuilder: NotificationCompat.Builder? = null
private var fgFuture: ListenableFuture<Void>? = null
override fun startWork(): ListenableFuture<ListenableWorker.Result> {
Log.d(TAG, "startWork")
val ctx = applicationContext
if (!flutterLoader.initialized()) {
flutterLoader.startInitialization(ctx)
}
// Create a Notification channel
createChannel()
Log.d(TAG, "isIgnoringBatteryOptimizations $isIgnoringBatteryOptimizations")
if (isIgnoringBatteryOptimizations) {
// normal background services can only up to 10 minutes
// foreground services are allowed to run indefinitely
// requires battery optimizations to be disabled (either manually by the user
// or by the system learning that immich is important to the user)
val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
.getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!!
showInfo(getInfoBuilder(title, indeterminate = true).build())
}
engine = FlutterEngine(ctx)
flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) {
runDart()
}
return resolvableFuture
}
/**
* Starts the Dart runtime/engine and calls `_nativeEntry` function in
* `background.service.dart` to run the actual backup logic.
*/
private fun runDart() {
val callbackDispatcherHandle = applicationContext.getSharedPreferences(
SHARED_PREF_NAME, Context.MODE_PRIVATE
).getLong(SHARED_PREF_CALLBACK_KEY, 0L)
val callbackInformation =
FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle)
val appBundlePath = flutterLoader.findAppBundlePath()
engine?.let { engine ->
backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel")
backgroundChannel.setMethodCallHandler(this@BackupWorker)
engine.dartExecutor.executeDartCallback(
DartExecutor.DartCallback(
applicationContext.assets,
appBundlePath,
callbackInformation
)
)
}
}
override fun onStopped() {
Log.d(TAG, "onStopped")
// called when the system has to stop this worker because constraints are
// no longer met or the system needs resources for more important tasks
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
if (::backgroundChannel.isInitialized) {
backgroundChannel.invokeMethod("systemStop", null)
}
}
waitOnSetForegroundAsync()
// cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException)
// instead, wait for 5 seconds until forcefully stopping backup work
Handler(Looper.getMainLooper()).postDelayed({
stopEngine(null)
}, 5000)
}
private fun waitOnSetForegroundAsync() {
val fgFuture = this.fgFuture
if (fgFuture != null && !fgFuture.isCancelled && !fgFuture.isDone) {
try {
fgFuture.get(500, TimeUnit.MILLISECONDS)
} catch (e: Exception) {
// ignored, there is nothing to be done
}
}
}
private fun stopEngine(result: Result?) {
clearBackgroundNotification()
engine?.destroy()
engine = null
if (result != null) {
Log.d(TAG, "stopEngine result=${result}")
resolvableFuture.set(result)
}
waitOnSetForegroundAsync()
}
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) {
when (call.method) {
"initialized" -> {
timeBackupStarted = SystemClock.uptimeMillis()
backgroundChannel.invokeMethod(
"onAssetsChanged",
null,
object : MethodChannel.Result {
override fun notImplemented() {
stopEngine(Result.failure())
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
stopEngine(Result.failure())
}
override fun success(receivedResult: Any?) {
val success = receivedResult as Boolean
stopEngine(if (success) Result.success() else Result.retry())
}
}
)
}
"updateNotification" -> {
val args = call.arguments<ArrayList<*>>()!!
val title = args[0] as String?
val content = args[1] as String?
val progress = args[2] as Int
val max = args[3] as Int
val indeterminate = args[4] as Boolean
val isDetail = args[5] as Boolean
val onlyIfFG = args[6] as Boolean
if (!onlyIfFG || isIgnoringBatteryOptimizations) {
showInfo(
getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(),
isDetail
)
}
}
"showError" -> {
val args = call.arguments<ArrayList<*>>()!!
val title = args[0] as String
val content = args[1] as String?
val individualTag = args[2] as String?
showError(title, content, individualTag)
}
"clearErrorNotifications" -> clearErrorNotifications()
"hasContentChanged" -> {
val lastChange = applicationContext
.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
.getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted)
val hasContentChanged = lastChange > timeBackupStarted;
timeBackupStarted = SystemClock.uptimeMillis()
r.success(hasContentChanged)
}
else -> r.notImplemented()
}
}
private fun showError(title: String, content: String?, individualTag: String?) {
val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID)
.setContentTitle(title)
.setTicker(title)
.setContentText(content)
.setSmallIcon(R.drawable.notification_icon)
.build()
notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification)
}
private fun clearErrorNotifications() {
notificationManager.cancel(NOTIFICATION_ERROR_ID)
}
private fun clearBackgroundNotification() {
notificationManager.cancel(NOTIFICATION_ID)
notificationManager.cancel(NOTIFICATION_DETAIL_ID)
}
private fun showInfo(notification: Notification, isDetail: Boolean = false) {
val id = if (isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID
if (isIgnoringBatteryOptimizations && !isDetail) {
fgFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE))
} else {
setForegroundAsync(ForegroundInfo(id, notification))
}
} else {
notificationManager.notify(id, notification)
}
}
private fun getInfoBuilder(
title: String? = null,
content: String? = null,
isDetail: Boolean = false,
progress: Int = 0,
max: Int = 0,
indeterminate: Boolean = false,
): NotificationCompat.Builder {
var builder = if (isDetail) notificationDetailBuilder else notificationBuilder
if (builder == null) {
builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setOnlyAlertOnce(true)
.setOngoing(true)
if (isDetail) {
notificationDetailBuilder = builder
} else {
notificationBuilder = builder
}
}
if (title != null) {
builder.setTicker(title).setContentTitle(title)
}
if (content != null) {
builder.setContentText(content)
}
return builder.setProgress(max, progress, indeterminate)
}
private fun createChannel() {
val foreground = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
NOTIFICATION_CHANNEL_ID,
NotificationManager.IMPORTANCE_LOW
)
notificationManager.createNotificationChannel(foreground)
val error = NotificationChannel(
NOTIFICATION_CHANNEL_ERROR_ID,
NOTIFICATION_CHANNEL_ERROR_ID,
NotificationManager.IMPORTANCE_HIGH
)
notificationManager.createNotificationChannel(error)
}
companion object {
const val SHARED_PREF_NAME = "immichBackgroundService"
const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle"
const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle"
const val SHARED_PREF_LAST_CHANGE = "lastChange"
private const val TASK_NAME_BACKUP = "immich/BackupWorker"
private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService"
private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError"
private const val NOTIFICATION_DEFAULT_TITLE = "Immich"
private const val NOTIFICATION_ID = 1
private const val NOTIFICATION_ERROR_ID = 2
private const val NOTIFICATION_DETAIL_ID = 3
private const val ONE_MINUTE = 60000L
/**
* Enqueues the BackupWorker to run once the constraints are met
*/
fun enqueueBackupWorker(
context: Context,
requireWifi: Boolean = false,
requireCharging: Boolean = false,
delayMilliseconds: Long = 0L
) {
val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds)
WorkManager.getInstance(context)
.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest)
Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued")
}
/**
* Updates the constraints of an already enqueued BackupWorker
*/
fun updateBackupWorker(
context: Context,
requireWifi: Boolean = false,
requireCharging: Boolean = false
) {
try {
val wm = WorkManager.getInstance(context)
val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP)
val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS)
if (workInfoList != null) {
for (workInfo in workInfoList) {
if (workInfo.state == WorkInfo.State.ENQUEUED) {
val workRequest = buildWorkRequest(requireWifi, requireCharging)
wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest)
Log.d(TAG, "updateBackupWorker updated BackupWorker constraints")
return
}
}
}
Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued")
} catch (e: Exception) {
Log.d(TAG, "updateBackupWorker failed: $e")
}
}
/**
* Stops the currently running worker (if any) and removes it from the work queue
*/
fun stopWork(context: Context) {
WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP)
Log.d(TAG, "stopWork: BackupWorker cancelled")
}
/**
* Returns `true` if the app is ignoring battery optimizations
*/
fun isIgnoringBatteryOptimizations(ctx: Context): Boolean {
val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager
return powerManager.isIgnoringBatteryOptimizations(ctx.packageName)
}
private fun buildWorkRequest(
requireWifi: Boolean = false,
requireCharging: Boolean = false,
delayMilliseconds: Long = 0L
): OneTimeWorkRequest {
val constraints = Constraints.Builder()
.setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.setRequiresCharging(requireCharging)
.build();
val work = OneTimeWorkRequest.Builder(BackupWorker::class.java)
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS)
.setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS)
.build()
return work
}
private val flutterLoader = FlutterLoader()
}
}
private const val TAG = "BackupWorker"
@@ -1,144 +0,0 @@
package app.alextran.immich
import android.content.Context
import android.os.SystemClock
import android.provider.MediaStore
import android.util.Log
import androidx.work.Constraints
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.Operation
import java.util.concurrent.TimeUnit
/**
* Worker executed by Android WorkManager observing content changes (new photos/videos)
*
* Immediately enqueues the BackupWorker when running.
* As this work is not triggered periodically, but on content change, the
* worker enqueues itself again after each run.
*/
class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
if (!isEnabled(applicationContext)) {
return Result.failure()
}
if (triggeredContentUris.size > 0) {
startBackupWorker(applicationContext, delayMilliseconds = 0)
}
enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE)
return Result.success()
}
companion object {
const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled"
private const val SHARED_PREF_REQUIRE_WIFI = "requireWifi"
private const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging"
private const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay"
private const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay"
private const val TASK_NAME_OBSERVER = "immich/ContentObserver"
/**
* Enqueues the `ContentObserverWorker`.
*
* @param context Android Context
*/
fun enable(context: Context, immediate: Boolean = false) {
enqueueObserverWorker(context, ExistingWorkPolicy.KEEP)
Log.d(TAG, "enabled ContentObserverWorker")
if (immediate) {
startBackupWorker(context, delayMilliseconds = 5000)
}
}
/**
* Configures the `BackupWorker` to run when all constraints are met.
*
* @param context Android Context
* @param requireWifi if true, task only runs if connected to wifi
* @param requireCharging if true, task only runs if device is charging
*/
fun configureWork(context: Context,
requireWifi: Boolean = false,
requireCharging: Boolean = false,
triggerUpdateDelay: Long = 5000,
triggerMaxDelay: Long = 50000) {
context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
.edit()
.putBoolean(SHARED_PREF_SERVICE_ENABLED, true)
.putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi)
.putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging)
.putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay)
.putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay)
.apply()
BackupWorker.updateBackupWorker(context, requireWifi, requireCharging)
}
/**
* Stops the currently running worker (if any) and removes it from the work queue
*/
fun disable(context: Context) {
context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
.edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply()
WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER)
Log.d(TAG, "disabled ContentObserverWorker")
}
/**
* Return true if the user has enabled the background backup service
*/
fun isEnabled(ctx: Context): Boolean {
return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)
}
/**
* Enqueue and replace the worker without the content trigger but with a short delay
*/
fun workManagerAppClearedWorkaround(context: Context) {
val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)
.setInitialDelay(500, TimeUnit.MILLISECONDS)
.build()
WorkManager
.getInstance(context)
.enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work)
.result
.get()
Log.d(TAG, "workManagerAppClearedWorkaround")
}
private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) {
val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
val constraints = Constraints.Builder()
.addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true)
.addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true)
.setTriggerContentUpdateDelay(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS)
.setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS)
.build()
val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java)
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work)
}
fun startBackupWorker(context: Context, delayMilliseconds: Long) {
val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE)
if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false))
return
val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true)
val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false)
BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds)
sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply()
}
}
}
private const val TAG = "ContentObserverWorker"
@@ -18,8 +18,6 @@ class ImmichApp : Application() {
// Thus, the BackupWorker is not started. If the system kills the process after each initialization
// (because of low memory etc.), the backup is never performed.
// As a workaround, we also run a backup check when initializing the application
ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0)
Handler(Looper.getMainLooper()).postDelayed({
// We can only check the engine count and not the status of the lock here,
// as the previous start might have been killed without unlocking.
@@ -51,7 +51,6 @@ class MainActivity : FlutterFragmentActivity() {
BackgroundWorkerFgHostApi.setUp(messenger, BackgroundWorkerApiImpl(ctx))
ConnectivityApi.setUp(messenger, ConnectivityApiImpl(ctx))
flutterEngine.plugins.add(BackgroundServicePlugin())
flutterEngine.plugins.add(backgroundEngineLockImpl)
flutterEngine.plugins.add(nativeSyncApiImpl)
}
@@ -43,8 +43,8 @@ class BackgroundEngineLock(context: Context) : BackgroundWorkerLockApi, ImmichPl
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
super.onAttachedToEngine(binding)
checkAndEnforceBackgroundLock(binding.applicationContext)
engineCount.incrementAndGet()
checkAndEnforceBackgroundLock(binding.applicationContext)
Log.i(TAG, "Flutter engine attached. Attached Engines count: $engineCount")
}
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -37,36 +37,150 @@ private object BackgroundWorkerPigeonUtils {
)
}
}
fun doubleEquals(a: Double, b: Double): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
}
fun floatEquals(a: Float, b: Float): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
}
fun doubleHash(d: Double): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (d == 0.0) 0.0 else d
val bits = java.lang.Double.doubleToLongBits(normalized)
return (bits xor (bits ushr 32)).toInt()
}
fun floatHash(f: Float): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (f == 0.0f) 0.0f else f
return java.lang.Float.floatToIntBits(normalized)
}
fun deepEquals(a: Any?, b: Any?): Boolean {
if (a === b) {
return true
}
if (a == null || b == null) {
return false
}
if (a is ByteArray && b is ByteArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is IntArray && b is IntArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is LongArray && b is LongArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is DoubleArray && b is DoubleArray) {
return a.contentEquals(b)
if (a.size != b.size) return false
for (i in a.indices) {
if (!doubleEquals(a[i], b[i])) return false
}
return true
}
if (a is FloatArray && b is FloatArray) {
if (a.size != b.size) return false
for (i in a.indices) {
if (!floatEquals(a[i], b[i])) return false
}
return true
}
if (a is Array<*> && b is Array<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
for (i in a.indices) {
if (!deepEquals(a[i], b[i])) return false
}
return true
}
if (a is List<*> && b is List<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
val iterA = a.iterator()
val iterB = b.iterator()
while (iterA.hasNext() && iterB.hasNext()) {
if (!deepEquals(iterA.next(), iterB.next())) return false
}
return true
}
if (a is Map<*, *> && b is Map<*, *>) {
return a.size == b.size && a.all {
(b as Map<Any?, Any?>).containsKey(it.key) &&
deepEquals(it.value, b[it.key])
if (a.size != b.size) return false
for (entry in a) {
val key = entry.key
var found = false
for (bEntry in b) {
if (deepEquals(key, bEntry.key)) {
if (deepEquals(entry.value, bEntry.value)) {
found = true
break
} else {
return false
}
}
}
if (!found) return false
}
return true
}
if (a is Double && b is Double) {
return doubleEquals(a, b)
}
if (a is Float && b is Float) {
return floatEquals(a, b)
}
return a == b
}
fun deepHash(value: Any?): Int {
return when (value) {
null -> 0
is ByteArray -> value.contentHashCode()
is IntArray -> value.contentHashCode()
is LongArray -> value.contentHashCode()
is DoubleArray -> {
var result = 1
for (item in value) {
result = 31 * result + doubleHash(item)
}
result
}
is FloatArray -> {
var result = 1
for (item in value) {
result = 31 * result + floatHash(item)
}
result
}
is Array<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is List<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is Map<*, *> -> {
var result = 0
for (entry in value) {
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
}
result
}
is Double -> doubleHash(value)
is Float -> floatHash(value)
else -> value.hashCode()
}
}
}
/**
@@ -79,7 +193,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
) : RuntimeException()
/** Generated class from Pigeon that represents data sent in messages. */
data class BackgroundWorkerSettings (
@@ -101,15 +215,22 @@ data class BackgroundWorkerSettings (
)
}
override fun equals(other: Any?): Boolean {
if (other !is BackgroundWorkerSettings) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return BackgroundWorkerPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as BackgroundWorkerSettings
return BackgroundWorkerPigeonUtils.deepEquals(this.requiresCharging, other.requiresCharging) && BackgroundWorkerPigeonUtils.deepEquals(this.minimumDelaySeconds, other.minimumDelaySeconds)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.requiresCharging)
result = 31 * result + BackgroundWorkerPigeonUtils.deepHash(this.minimumDelaySeconds)
return result
}
}
private open class BackgroundWorkerPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
@@ -5,8 +5,10 @@ import android.provider.MediaStore
import android.util.Log
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import io.flutter.embedding.engine.FlutterEngineCache
import java.util.concurrent.TimeUnit
@@ -18,6 +20,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
override fun enable() {
enqueueMediaObserver(ctx)
enqueuePeriodicWorker(ctx)
}
override fun saveNotificationMessage(title: String, body: String) {
@@ -27,12 +30,14 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
override fun configure(settings: BackgroundWorkerSettings) {
BackgroundWorkerPreferences(ctx).updateSettings(settings)
enqueueMediaObserver(ctx)
enqueuePeriodicWorker(ctx)
}
override fun disable() {
WorkManager.getInstance(ctx).apply {
cancelUniqueWork(OBSERVER_WORKER_NAME)
cancelUniqueWork(BACKGROUND_WORKER_NAME)
cancelUniqueWork(PERIODIC_WORKER_NAME)
}
Log.i(TAG, "Cancelled background upload tasks")
}
@@ -40,6 +45,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
companion object {
private const val BACKGROUND_WORKER_NAME = "immich/BackgroundWorkerV1"
private const val OBSERVER_WORKER_NAME = "immich/MediaObserverV1"
private const val PERIODIC_WORKER_NAME = "immich/PeriodicBackgroundWorkerV1"
const val ENGINE_CACHE_KEY = "immich::background_worker::engine"
@@ -55,7 +61,7 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
setRequiresCharging(settings.requiresCharging)
}.build()
val work = OneTimeWorkRequest.Builder(MediaObserver::class.java)
val work = OneTimeWorkRequestBuilder<MediaObserver>()
.setConstraints(constraints)
.build()
WorkManager.getInstance(ctx)
@@ -67,10 +73,30 @@ class BackgroundWorkerApiImpl(context: Context) : BackgroundWorkerFgHostApi {
)
}
fun enqueuePeriodicWorker(ctx: Context) {
val settings = BackgroundWorkerPreferences(ctx).getSettings()
val constraints = Constraints.Builder().apply {
setRequiresCharging(settings.requiresCharging)
}.build()
val work =
PeriodicWorkRequestBuilder<PeriodicWorker>(
1,
TimeUnit.HOURS,
15,
TimeUnit.MINUTES
).setConstraints(constraints)
.build()
WorkManager.getInstance(ctx)
.enqueueUniquePeriodicWork(PERIODIC_WORKER_NAME, ExistingPeriodicWorkPolicy.UPDATE, work)
Log.i(TAG, "Enqueued periodic background worker with name: $PERIODIC_WORKER_NAME")
}
fun enqueueBackgroundWorker(ctx: Context) {
val constraints = Constraints.Builder().setRequiresBatteryNotLow(true).build()
val work = OneTimeWorkRequest.Builder(BackgroundWorker::class.java)
val work = OneTimeWorkRequestBuilder<BackgroundWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 1, TimeUnit.MINUTES)
.build()
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -0,0 +1,16 @@
package app.alextran.immich.background
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
class PeriodicWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
private val ctx: Context = context.applicationContext
override fun doWork(): Result {
Log.i("PeriodicWorker", "Periodic worker triggered, starting background worker")
BackgroundWorkerApiImpl.enqueueBackgroundWorker(ctx)
return Result.success()
}
}
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -46,7 +46,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
) : RuntimeException()
enum class NetworkCapability(val raw: Int) {
CELLULAR(0),
@@ -75,7 +75,7 @@ private open class ConnectivityPigeonCodec : StandardMessageCodec() {
when (value) {
is NetworkCapability -> {
stream.write(129)
writeValue(stream, value.raw)
writeValue(stream, value.raw.toLong())
}
else -> super.writeValue(stream, value)
}
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -34,36 +34,150 @@ private object NetworkPigeonUtils {
)
}
}
fun doubleEquals(a: Double, b: Double): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
}
fun floatEquals(a: Float, b: Float): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
}
fun doubleHash(d: Double): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (d == 0.0) 0.0 else d
val bits = java.lang.Double.doubleToLongBits(normalized)
return (bits xor (bits ushr 32)).toInt()
}
fun floatHash(f: Float): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (f == 0.0f) 0.0f else f
return java.lang.Float.floatToIntBits(normalized)
}
fun deepEquals(a: Any?, b: Any?): Boolean {
if (a === b) {
return true
}
if (a == null || b == null) {
return false
}
if (a is ByteArray && b is ByteArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is IntArray && b is IntArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is LongArray && b is LongArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is DoubleArray && b is DoubleArray) {
return a.contentEquals(b)
if (a.size != b.size) return false
for (i in a.indices) {
if (!doubleEquals(a[i], b[i])) return false
}
return true
}
if (a is FloatArray && b is FloatArray) {
if (a.size != b.size) return false
for (i in a.indices) {
if (!floatEquals(a[i], b[i])) return false
}
return true
}
if (a is Array<*> && b is Array<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
for (i in a.indices) {
if (!deepEquals(a[i], b[i])) return false
}
return true
}
if (a is List<*> && b is List<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
val iterA = a.iterator()
val iterB = b.iterator()
while (iterA.hasNext() && iterB.hasNext()) {
if (!deepEquals(iterA.next(), iterB.next())) return false
}
return true
}
if (a is Map<*, *> && b is Map<*, *>) {
return a.size == b.size && a.all {
(b as Map<Any?, Any?>).containsKey(it.key) &&
deepEquals(it.value, b[it.key])
if (a.size != b.size) return false
for (entry in a) {
val key = entry.key
var found = false
for (bEntry in b) {
if (deepEquals(key, bEntry.key)) {
if (deepEquals(entry.value, bEntry.value)) {
found = true
break
} else {
return false
}
}
}
if (!found) return false
}
return true
}
if (a is Double && b is Double) {
return doubleEquals(a, b)
}
if (a is Float && b is Float) {
return floatEquals(a, b)
}
return a == b
}
fun deepHash(value: Any?): Int {
return when (value) {
null -> 0
is ByteArray -> value.contentHashCode()
is IntArray -> value.contentHashCode()
is LongArray -> value.contentHashCode()
is DoubleArray -> {
var result = 1
for (item in value) {
result = 31 * result + doubleHash(item)
}
result
}
is FloatArray -> {
var result = 1
for (item in value) {
result = 31 * result + floatHash(item)
}
result
}
is Array<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is List<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is Map<*, *> -> {
var result = 0
for (entry in value) {
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
}
result
}
is Double -> doubleHash(value)
is Float -> floatHash(value)
else -> value.hashCode()
}
}
}
/**
@@ -76,7 +190,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
) : RuntimeException()
/** Generated class from Pigeon that represents data sent in messages. */
data class ClientCertData (
@@ -98,15 +212,22 @@ data class ClientCertData (
)
}
override fun equals(other: Any?): Boolean {
if (other !is ClientCertData) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return NetworkPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as ClientCertData
return NetworkPigeonUtils.deepEquals(this.data, other.data) && NetworkPigeonUtils.deepEquals(this.password, other.password)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + NetworkPigeonUtils.deepHash(this.data)
result = 31 * result + NetworkPigeonUtils.deepHash(this.password)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -135,15 +256,24 @@ data class ClientCertPrompt (
)
}
override fun equals(other: Any?): Boolean {
if (other !is ClientCertPrompt) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return NetworkPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as ClientCertPrompt
return NetworkPigeonUtils.deepEquals(this.title, other.title) && NetworkPigeonUtils.deepEquals(this.message, other.message) && NetworkPigeonUtils.deepEquals(this.cancel, other.cancel) && NetworkPigeonUtils.deepEquals(this.confirm, other.confirm)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + NetworkPigeonUtils.deepHash(this.title)
result = 31 * result + NetworkPigeonUtils.deepHash(this.message)
result = 31 * result + NetworkPigeonUtils.deepHash(this.cancel)
result = 31 * result + NetworkPigeonUtils.deepHash(this.confirm)
return result
}
}
private open class NetworkPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -46,7 +46,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
) : RuntimeException()
private open class LocalImagesPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
return super.readValueOfType(type, buffer)
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
@@ -34,36 +34,150 @@ private object MessagesPigeonUtils {
)
}
}
fun doubleEquals(a: Double, b: Double): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0) 0.0 else a) == (if (b == 0.0) 0.0 else b) || (a.isNaN() && b.isNaN())
}
fun floatEquals(a: Float, b: Float): Boolean {
// Normalize -0.0 to 0.0 and handle NaN equality.
return (if (a == 0.0f) 0.0f else a) == (if (b == 0.0f) 0.0f else b) || (a.isNaN() && b.isNaN())
}
fun doubleHash(d: Double): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (d == 0.0) 0.0 else d
val bits = java.lang.Double.doubleToLongBits(normalized)
return (bits xor (bits ushr 32)).toInt()
}
fun floatHash(f: Float): Int {
// Normalize -0.0 to 0.0 and handle NaN to ensure consistent hash codes.
val normalized = if (f == 0.0f) 0.0f else f
return java.lang.Float.floatToIntBits(normalized)
}
fun deepEquals(a: Any?, b: Any?): Boolean {
if (a === b) {
return true
}
if (a == null || b == null) {
return false
}
if (a is ByteArray && b is ByteArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is IntArray && b is IntArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is LongArray && b is LongArray) {
return a.contentEquals(b)
return a.contentEquals(b)
}
if (a is DoubleArray && b is DoubleArray) {
return a.contentEquals(b)
if (a.size != b.size) return false
for (i in a.indices) {
if (!doubleEquals(a[i], b[i])) return false
}
return true
}
if (a is FloatArray && b is FloatArray) {
if (a.size != b.size) return false
for (i in a.indices) {
if (!floatEquals(a[i], b[i])) return false
}
return true
}
if (a is Array<*> && b is Array<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
for (i in a.indices) {
if (!deepEquals(a[i], b[i])) return false
}
return true
}
if (a is List<*> && b is List<*>) {
return a.size == b.size &&
a.indices.all{ deepEquals(a[it], b[it]) }
if (a.size != b.size) return false
val iterA = a.iterator()
val iterB = b.iterator()
while (iterA.hasNext() && iterB.hasNext()) {
if (!deepEquals(iterA.next(), iterB.next())) return false
}
return true
}
if (a is Map<*, *> && b is Map<*, *>) {
return a.size == b.size && a.all {
(b as Map<Any?, Any?>).containsKey(it.key) &&
deepEquals(it.value, b[it.key])
if (a.size != b.size) return false
for (entry in a) {
val key = entry.key
var found = false
for (bEntry in b) {
if (deepEquals(key, bEntry.key)) {
if (deepEquals(entry.value, bEntry.value)) {
found = true
break
} else {
return false
}
}
}
if (!found) return false
}
return true
}
if (a is Double && b is Double) {
return doubleEquals(a, b)
}
if (a is Float && b is Float) {
return floatEquals(a, b)
}
return a == b
}
fun deepHash(value: Any?): Int {
return when (value) {
null -> 0
is ByteArray -> value.contentHashCode()
is IntArray -> value.contentHashCode()
is LongArray -> value.contentHashCode()
is DoubleArray -> {
var result = 1
for (item in value) {
result = 31 * result + doubleHash(item)
}
result
}
is FloatArray -> {
var result = 1
for (item in value) {
result = 31 * result + floatHash(item)
}
result
}
is Array<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is List<*> -> {
var result = 1
for (item in value) {
result = 31 * result + deepHash(item)
}
result
}
is Map<*, *> -> {
var result = 0
for (entry in value) {
result += ((deepHash(entry.key) * 31) xor deepHash(entry.value))
}
result
}
is Double -> doubleHash(value)
is Float -> floatHash(value)
else -> value.hashCode()
}
}
}
/**
@@ -76,7 +190,7 @@ class FlutterError (
val code: String,
override val message: String? = null,
val details: Any? = null
) : Throwable()
) : RuntimeException()
enum class PlatformAssetPlaybackStyle(val raw: Int) {
UNKNOWN(0),
@@ -102,7 +216,7 @@ data class PlatformAsset (
val updatedAt: Long? = null,
val width: Long? = null,
val height: Long? = null,
val durationInSeconds: Long,
val durationMs: Long,
val orientation: Long,
val isFavorite: Boolean,
val adjustmentTime: Long? = null,
@@ -120,14 +234,14 @@ data class PlatformAsset (
val updatedAt = pigeonVar_list[4] as Long?
val width = pigeonVar_list[5] as Long?
val height = pigeonVar_list[6] as Long?
val durationInSeconds = pigeonVar_list[7] as Long
val durationMs = pigeonVar_list[7] as Long
val orientation = pigeonVar_list[8] as Long
val isFavorite = pigeonVar_list[9] as Boolean
val adjustmentTime = pigeonVar_list[10] as Long?
val latitude = pigeonVar_list[11] as Double?
val longitude = pigeonVar_list[12] as Double?
val playbackStyle = pigeonVar_list[13] as PlatformAssetPlaybackStyle
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationInSeconds, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle)
return PlatformAsset(id, name, type, createdAt, updatedAt, width, height, durationMs, orientation, isFavorite, adjustmentTime, latitude, longitude, playbackStyle)
}
}
fun toList(): List<Any?> {
@@ -139,7 +253,7 @@ data class PlatformAsset (
updatedAt,
width,
height,
durationInSeconds,
durationMs,
orientation,
isFavorite,
adjustmentTime,
@@ -149,15 +263,34 @@ data class PlatformAsset (
)
}
override fun equals(other: Any?): Boolean {
if (other !is PlatformAsset) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as PlatformAsset
return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.type, other.type) && MessagesPigeonUtils.deepEquals(this.createdAt, other.createdAt) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.width, other.width) && MessagesPigeonUtils.deepEquals(this.height, other.height) && MessagesPigeonUtils.deepEquals(this.durationMs, other.durationMs) && MessagesPigeonUtils.deepEquals(this.orientation, other.orientation) && MessagesPigeonUtils.deepEquals(this.isFavorite, other.isFavorite) && MessagesPigeonUtils.deepEquals(this.adjustmentTime, other.adjustmentTime) && MessagesPigeonUtils.deepEquals(this.latitude, other.latitude) && MessagesPigeonUtils.deepEquals(this.longitude, other.longitude) && MessagesPigeonUtils.deepEquals(this.playbackStyle, other.playbackStyle)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.id)
result = 31 * result + MessagesPigeonUtils.deepHash(this.name)
result = 31 * result + MessagesPigeonUtils.deepHash(this.type)
result = 31 * result + MessagesPigeonUtils.deepHash(this.createdAt)
result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt)
result = 31 * result + MessagesPigeonUtils.deepHash(this.width)
result = 31 * result + MessagesPigeonUtils.deepHash(this.height)
result = 31 * result + MessagesPigeonUtils.deepHash(this.durationMs)
result = 31 * result + MessagesPigeonUtils.deepHash(this.orientation)
result = 31 * result + MessagesPigeonUtils.deepHash(this.isFavorite)
result = 31 * result + MessagesPigeonUtils.deepHash(this.adjustmentTime)
result = 31 * result + MessagesPigeonUtils.deepHash(this.latitude)
result = 31 * result + MessagesPigeonUtils.deepHash(this.longitude)
result = 31 * result + MessagesPigeonUtils.deepHash(this.playbackStyle)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -189,15 +322,25 @@ data class PlatformAlbum (
)
}
override fun equals(other: Any?): Boolean {
if (other !is PlatformAlbum) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as PlatformAlbum
return MessagesPigeonUtils.deepEquals(this.id, other.id) && MessagesPigeonUtils.deepEquals(this.name, other.name) && MessagesPigeonUtils.deepEquals(this.updatedAt, other.updatedAt) && MessagesPigeonUtils.deepEquals(this.isCloud, other.isCloud) && MessagesPigeonUtils.deepEquals(this.assetCount, other.assetCount)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.id)
result = 31 * result + MessagesPigeonUtils.deepHash(this.name)
result = 31 * result + MessagesPigeonUtils.deepHash(this.updatedAt)
result = 31 * result + MessagesPigeonUtils.deepHash(this.isCloud)
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetCount)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -226,15 +369,24 @@ data class SyncDelta (
)
}
override fun equals(other: Any?): Boolean {
if (other !is SyncDelta) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as SyncDelta
return MessagesPigeonUtils.deepEquals(this.hasChanges, other.hasChanges) && MessagesPigeonUtils.deepEquals(this.updates, other.updates) && MessagesPigeonUtils.deepEquals(this.deletes, other.deletes) && MessagesPigeonUtils.deepEquals(this.assetAlbums, other.assetAlbums)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.hasChanges)
result = 31 * result + MessagesPigeonUtils.deepHash(this.updates)
result = 31 * result + MessagesPigeonUtils.deepHash(this.deletes)
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetAlbums)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -260,15 +412,23 @@ data class HashResult (
)
}
override fun equals(other: Any?): Boolean {
if (other !is HashResult) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as HashResult
return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.hash, other.hash)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId)
result = 31 * result + MessagesPigeonUtils.deepHash(this.error)
result = 31 * result + MessagesPigeonUtils.deepHash(this.hash)
return result
}
}
/** Generated class from Pigeon that represents data sent in messages. */
@@ -294,15 +454,23 @@ data class CloudIdResult (
)
}
override fun equals(other: Any?): Boolean {
if (other !is CloudIdResult) {
if (other == null || other.javaClass != javaClass) {
return false
}
if (this === other) {
return true
}
return MessagesPigeonUtils.deepEquals(toList(), other.toList()) }
val other = other as CloudIdResult
return MessagesPigeonUtils.deepEquals(this.assetId, other.assetId) && MessagesPigeonUtils.deepEquals(this.error, other.error) && MessagesPigeonUtils.deepEquals(this.cloudId, other.cloudId)
}
override fun hashCode(): Int = toList().hashCode()
override fun hashCode(): Int {
var result = javaClass.hashCode()
result = 31 * result + MessagesPigeonUtils.deepHash(this.assetId)
result = 31 * result + MessagesPigeonUtils.deepHash(this.error)
result = 31 * result + MessagesPigeonUtils.deepHash(this.cloudId)
return result
}
}
private open class MessagesPigeonCodec : StandardMessageCodec() {
override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
@@ -344,7 +512,7 @@ private open class MessagesPigeonCodec : StandardMessageCodec() {
when (value) {
is PlatformAssetPlaybackStyle -> {
stream.write(129)
writeValue(stream, value.raw)
writeValue(stream, value.raw.toLong())
}
is PlatformAsset -> {
stream.write(130)
@@ -94,11 +94,12 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
// _special_format requires S Extensions 21+
// _special_format: added in API level 37, also in S Extensions 21+
// https://developer.android.com/reference/android/provider/MediaStore.Files.FileColumns#SPECIAL_FORMAT
private fun hasSpecialFormatColumn(): Boolean =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 21
Build.VERSION.SDK_INT >= 37 ||
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 21)
}
protected fun getCursor(
@@ -177,7 +178,7 @@ open class NativeSyncApiImplBase(context: Context) : ImmichPlugin() {
val height = c.getInt(heightColumn).toLong()
// Duration is milliseconds
val duration = if (rawMediaType == MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE) 0L
else c.getLong(durationColumn) / 1000
else c.getLong(durationColumn)
val orientation = c.getInt(orientationColumn)
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
-18
View File
@@ -1,6 +1,4 @@
allprojects {
ext.kotlin_version = '2.2.20'
repositories {
google()
mavenCentral()
@@ -10,22 +8,7 @@ allprojects {
rootProject.buildDir = '../build'
subprojects {
// fix for verifyReleaseResources
// ============
afterEvaluate { project ->
if (project.plugins.hasPlugin("com.android.application") ||
project.plugins.hasPlugin("com.android.library")) {
project.android {
compileSdkVersion 36
buildToolsVersion "36.0.0"
}
}
}
// ============
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
@@ -36,4 +19,3 @@ tasks.register("clean", Delete) {
tasks.named('wrapper') {
distributionType = Wrapper.DistributionType.ALL
}
+51
View File
@@ -0,0 +1,51 @@
[versions]
agp = "8.11.2"
kotlin = "2.2.20"
ksp = "2.2.20-2.0.3"
coroutines = "1.9.0"
work = "2.9.1"
concurrent = "1.2.0"
guava = "33.3.1-android"
glide = "4.16.0"
serialization-json = "1.8.1"
glance = "1.1.1"
gson = "2.10.1"
okhttp = "4.12.0"
cronet = "143.7445.0"
media3 = "1.10.0"
desugar = "2.1.2"
activity-compose = "1.8.2"
compose-ui = "1.1.1"
material3 = "1.2.1"
lifecycle = "2.6.2"
material = "1.12.0"
[libraries]
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
cronet-embedded = { module = "org.chromium.net:cronet-embedded", version.ref = "cronet" }
media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3" }
media3-datasource-cronet = { module = "androidx.media3:media3-datasource-cronet", version.ref = "media3" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }
concurrent-futures = { module = "androidx.concurrent:concurrent-futures", version.ref = "concurrent" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
glide-ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization-json" }
desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar" }
glance-appwidget = { module = "androidx.glance:glance-appwidget", version.ref = "glance" }
gson = { module = "com.google.code.gson:gson", version.ref = "gson" }
activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" }
compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose-ui" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-ui" }
compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle" }
material = { module = "com.google.android.material:material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
# TODO: update to version.ref = "kotlin" when background_downloader is removed
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.1.0" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+4 -3
View File
@@ -18,10 +18,11 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version '8.11.2' apply false
id "com.android.application" version "8.11.2" apply false
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.22' apply false
id 'com.google.devtools.ksp' version '2.2.20-2.0.3' apply false
// TODO: update to match kotlin version when background_downloader is removed
id "org.jetbrains.kotlin.plugin.serialization" version "2.1.0" apply false
id "com.google.devtools.ksp" version "2.2.20-2.0.3" apply false
}
include ":app"
-3
View File
@@ -1,3 +0,0 @@
# Used to filter out tags from test runs
tags:
widget:
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
include: package:lints/recommended.yaml
@@ -1,89 +0,0 @@
import 'package:analyzer/error/error.dart' show ErrorSeverity;
import 'package:analyzer/error/listener.dart';
import 'package:custom_lint_builder/custom_lint_builder.dart';
// ignore: depend_on_referenced_packages
import 'package:glob/glob.dart';
PluginBase createPlugin() => ImmichLinter();
class ImmichLinter extends PluginBase {
@override
List<LintRule> getLintRules(CustomLintConfigs configs) {
final List<LintRule> rules = [];
for (final entry in configs.rules.entries) {
if (entry.value.enabled && entry.key.startsWith("import_rule_")) {
final code = makeCode(entry.key, entry.value);
final allowedPaths = getStrings(entry.value, "allowed");
final forbiddenPaths = getStrings(entry.value, "forbidden");
final restrict = getStrings(entry.value, "restrict");
rules.add(ImportRule(code, buildGlob(allowedPaths),
buildGlob(forbiddenPaths), restrict));
}
}
return rules;
}
static LintCode makeCode(String name, LintOptions options) => LintCode(
name: name,
problemMessage: options.json["message"] as String,
errorSeverity: ErrorSeverity.WARNING,
);
static List<String> getStrings(LintOptions options, String field) {
final List<String> result = [];
final excludeOption = options.json[field];
if (excludeOption is String) {
result.add(excludeOption);
} else if (excludeOption is List) {
result.addAll(excludeOption.map((option) => option));
}
return result;
}
Glob? buildGlob(List<String> globs) {
if (globs.isEmpty) return null;
if (globs.length == 1) return Glob(globs[0], caseSensitive: true);
return Glob("{${globs.join(",")}}", caseSensitive: true);
}
}
// ignore: must_be_immutable
class ImportRule extends DartLintRule {
ImportRule(LintCode code, this._allowed, this._forbidden, this._restrict)
: super(code: code);
final Glob? _allowed;
final Glob? _forbidden;
final List<String> _restrict;
int _rootOffset = -1;
@override
void run(
CustomLintResolver resolver,
ErrorReporter reporter,
CustomLintContext context,
) {
if (_rootOffset == -1) {
const project = "/immich/mobile/";
_rootOffset =
resolver.path.toLowerCase().indexOf(project) + project.length;
}
final path = resolver.path.substring(_rootOffset);
if ((_allowed != null && _allowed!.matches(path)) &&
(_forbidden == null || !_forbidden!.matches(path))) {
return;
}
context.registry.addImportDirective((node) {
final uri = node.uri.stringValue;
if (uri == null) return;
for (final restricted in _restrict) {
if (uri.startsWith(restricted) == true) {
reporter.atNode(node, code);
return;
}
}
});
}
}
-365
View File
@@ -1,365 +0,0 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: e55636ed79578b9abca5fecf9437947798f5ef7456308b5cb85720b793eac92f
url: "https://pub.dev"
source: hosted
version: "82.0.0"
analyzer:
dependency: "direct main"
description:
name: analyzer
sha256: "904ae5bb474d32c38fb9482e2d925d5454cda04ddd0e55d2e6826bc72f6ba8c0"
url: "https://pub.dev"
source: hosted
version: "7.4.5"
analyzer_plugin:
dependency: "direct main"
description:
name: analyzer_plugin
sha256: ee188b6df6c85f1441497c7171c84f1392affadc0384f71089cb10a3bc508cef
url: "https://pub.dev"
source: hosted
version: "0.13.1"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://pub.dev"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
checked_yaml:
dependency: transitive
description:
name: checked_yaml
sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f"
url: "https://pub.dev"
source: hosted
version: "2.0.4"
ci:
dependency: transitive
description:
name: ci
sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c
url: "https://pub.dev"
source: hosted
version: "0.4.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
crypto:
dependency: transitive
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev"
source: hosted
version: "3.0.6"
custom_lint:
dependency: transitive
description:
name: custom_lint
sha256: "409c485fd14f544af1da965d5a0d160ee57cd58b63eeaa7280a4f28cf5bda7f1"
url: "https://pub.dev"
source: hosted
version: "0.7.5"
custom_lint_builder:
dependency: "direct main"
description:
name: custom_lint_builder
sha256: "107e0a43606138015777590ee8ce32f26ba7415c25b722ff0908a6f5d7a4c228"
url: "https://pub.dev"
source: hosted
version: "0.7.5"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be"
url: "https://pub.dev"
source: hosted
version: "0.7.5"
custom_lint_visitor:
dependency: transitive
description:
name: custom_lint_visitor
sha256: cba5b6d7a6217312472bf4468cdf68c949488aed7ffb0eab792cd0b6c435054d
url: "https://pub.dev"
source: hosted
version: "1.0.0+7.4.5"
dart_style:
dependency: transitive
description:
name: dart_style
sha256: "5b236382b47ee411741447c1f1e111459c941ea1b3f2b540dde54c210a3662af"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
file:
dependency: transitive
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
freezed_annotation:
dependency: transitive
description:
name: freezed_annotation
sha256: "7294967ff0a6d98638e7acb774aac3af2550777accd8149c90af5b014e6d44d8"
url: "https://pub.dev"
source: hosted
version: "3.1.0"
glob:
dependency: "direct main"
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: bc167a1163807b03bada490bfe2df25b0d744df359227880220a5cbd04e5734b
url: "https://pub.dev"
source: hosted
version: "4.3.0"
json_annotation:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
lints:
dependency: "direct dev"
description:
name: lints
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://pub.dev"
source: hosted
version: "6.0.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
pubspec_parse:
dependency: transitive
description:
name: pubspec_parse
sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
rxdart:
dependency: transitive
description:
name: rxdart
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
url: "https://pub.dev"
source: hosted
version: "0.28.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
stream_transform:
dependency: transitive
description:
name: stream_transform
sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871
url: "https://pub.dev"
source: hosted
version: "2.1.1"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: transitive
description:
name: uuid
sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff
url: "https://pub.dev"
source: hosted
version: "4.5.1"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
watcher:
dependency: transitive
description:
name: watcher
sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.8.0 <4.0.0"
-14
View File
@@ -1,14 +0,0 @@
name: immich_mobile_immich_lint
publish_to: none
environment:
sdk: '>=3.0.0 <4.0.0'
dependencies:
analyzer: ^7.0.0
analyzer_plugin: ^0.13.0
custom_lint_builder: ^0.7.5
glob: ^2.1.2
dev_dependencies:
lints: ^6.0.0
@@ -5,7 +5,6 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/main.dart' as app;
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:integration_test/integration_test.dart';
@@ -39,20 +38,11 @@ class ImmichTestHelper {
static Future<void> loadApp(WidgetTester tester) async {
await EasyLocalization.ensureInitialized();
// Clear all data from Isar (reuse existing instance if available)
final (isar, drift, logDb) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDb);
final (drift, _) = await Bootstrap.initDomain();
await Store.clear();
await isar.writeTxn(() => isar.clear());
// Load main Widget
await tester.pumpWidget(
ProviderScope(
overrides: [
dbProvider.overrideWithValue(isar),
isarProvider.overrideWithValue(isar),
driftProvider.overrideWith(driftOverride(drift)),
],
child: const app.MainWidget(),
),
ProviderScope(overrides: [driftProvider.overrideWith(driftOverride(drift))], child: const app.MainWidget()),
);
// Post run tasks
await EasyLocalization.ensureInitialized();
@@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
+18 -68
View File
@@ -20,21 +20,21 @@ PODS:
- Flutter
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
- flutter_web_auth_2 (3.0.0):
- KeychainAccess
- flutter_web_auth_2 (5.0.0):
- Flutter
- fluttertoast (0.0.2):
- Flutter
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
- home_widget (0.0.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
- Flutter
- isar_community_flutter_libs (1.0.0):
- Flutter
- KeychainAccess (4.2.2)
- local_auth_darwin (0.0.1):
- Flutter
- FlutterMacOS
@@ -46,19 +46,13 @@ PODS:
- Flutter
- network_info_plus (0.0.1):
- Flutter
- objective_c (0.0.1):
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- photo_manager (3.7.1):
- photo_manager (3.9.0):
- Flutter
- FlutterMacOS
- SAMKeychain (1.5.3)
- share_handler_ios (0.0.14):
- Flutter
- share_handler_ios/share_handler_ios_models (= 0.0.14)
@@ -72,28 +66,6 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.49.1):
- sqlite3/common (= 3.49.1)
- sqlite3/common (3.49.1)
- sqlite3/dbstatvtab (3.49.1):
- sqlite3/common
- sqlite3/fts5 (3.49.1):
- sqlite3/common
- sqlite3/perf-threadsafe (3.49.1):
- sqlite3/common
- sqlite3/rtree (3.49.1):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.1)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
- sqlite3/rtree
- url_launcher_ios (0.0.1):
- Flutter
- wakelock_plus (0.0.1):
@@ -112,34 +84,28 @@ DEPENDENCIES:
- flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
- flutter_web_auth_2 (from `.symlinks/plugins/flutter_web_auth_2/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- isar_community_flutter_libs (from `.symlinks/plugins/isar_community_flutter_libs/ios`)
- local_auth_darwin (from `.symlinks/plugins/local_auth_darwin/darwin`)
- maplibre_gl (from `.symlinks/plugins/maplibre_gl/ios`)
- native_video_player (from `.symlinks/plugins/native_video_player/ios`)
- network_info_plus (from `.symlinks/plugins/network_info_plus/ios`)
- objective_c (from `.symlinks/plugins/objective_c/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
SPEC REPOS:
trunk:
- KeychainAccess
- MapLibre
- SAMKeychain
- sqlite3
EXTERNAL SOURCES:
background_downloader:
@@ -167,15 +133,13 @@ EXTERNAL SOURCES:
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/ios"
:path: ".symlinks/plugins/geolocator_apple/darwin"
home_widget:
:path: ".symlinks/plugins/home_widget/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
isar_community_flutter_libs:
:path: ".symlinks/plugins/isar_community_flutter_libs/ios"
local_auth_darwin:
:path: ".symlinks/plugins/local_auth_darwin/darwin"
maplibre_gl:
@@ -184,16 +148,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/native_video_player/ios"
network_info_plus:
:path: ".symlinks/plugins/network_info_plus/ios"
objective_c:
:path: ".symlinks/plugins/objective_c/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
:path: ".symlinks/plugins/photo_manager/darwin"
share_handler_ios:
:path: ".symlinks/plugins/share_handler_ios/ios"
share_handler_ios_models:
@@ -202,10 +162,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqlite3_flutter_libs:
:path: ".symlinks/plugins/sqlite3_flutter_libs/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
wakelock_plus:
@@ -221,33 +177,27 @@ SPEC CHECKSUMS:
flutter_local_notifications: ad39620c743ea4c15127860f4b5641649a988100
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
flutter_udid: f7c3884e6ec2951efe4f9de082257fc77c4d15e9
flutter_web_auth_2: 5c8d9dcd7848b5a9efb086d24e7a9adcae979c80
flutter_udid: 92a5d31fe0526b7b6002a2318df702e12e7eb300
flutter_web_auth_2: 646fc9df97a01c59e5eea99b237da2c6360f8439
fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1
geolocator_apple: 1560c3c875af2a412242c7a923e15d0d401966ff
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
isar_community_flutter_libs: bede843185a61a05ff364a05c9b23209523f7e0d
local_auth_darwin: 553ce4f9b16d3fdfeafce9cf042e7c9f77c1c391
KeychainAccess: c0c4f7f38f6fc7bbe58f5702e25f7bd2f65abf51
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb
MapLibre: 69e572367f4ef6287e18246cfafc39c80cdcabcd
maplibre_gl: 3c924e44725147b03dda33430ad216005b40555f
native_video_player: b65c58951ede2f93d103a25366bdebca95081265
network_info_plus: cf61925ab5205dce05a4f0895989afdb6aade5fc
objective_c: 89e720c30d716b036faf9c9684022048eee1eee2
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
photo_manager: 1d80ae07a89a67dfbcae95953a1e5a24af7c3e62
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
share_handler_ios: e2244e990f826b2c8eaa291ac3831569438ba0fb
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: f8fc13346870e73fe35ebf6dbb997fbcd156b241
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
PODFILE CHECKSUM: 938abbae4114b9c2140c550a2a0d8f7c674f5dfe
+9 -27
View File
@@ -3,15 +3,13 @@
archiveVersion = 1;
classes = {
};
objectVersion = 77;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
3B6A31FED0FC846D6BD69BBC /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */; };
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */; };
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
@@ -90,8 +88,6 @@
357FC57E54FD0F51795CF28A /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
571EAA93D77181C7C98C2EA6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; };
65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundServicePlugin.swift; sourceTree = "<group>"; };
65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundSyncWorker.swift; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -151,11 +147,15 @@
/* Begin PBXFileSystemSynchronizedRootGroup section */
B231F52D2E93A44A00BC45D1 /* Core */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Core;
sourceTree = "<group>";
};
B2CF7F8C2DDE4EBB00744BF6 /* Sync */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Sync;
sourceTree = "<group>";
};
@@ -177,6 +177,8 @@
};
FEE084F22EC172080045228E /* Schemas */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
);
path = Schemas;
sourceTree = "<group>";
};
@@ -238,15 +240,6 @@
name = Frameworks;
sourceTree = "<group>";
};
65DD438629917FAD0047FFA8 /* BackgroundSync */ = {
isa = PBXGroup;
children = (
65F32F32299D349D00CE9261 /* BackgroundSyncWorker.swift */,
65F32F30299BD2F800CE9261 /* BackgroundServicePlugin.swift */,
);
path = BackgroundSync;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
@@ -291,7 +284,6 @@
B21E34A62E5AF9760031FDB9 /* Background */,
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
FA9973382CF6DF4B000EF859 /* Runner.entitlements */,
65DD438629917FAD0047FFA8 /* BackgroundSync */,
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
@@ -571,14 +563,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -607,14 +595,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -627,7 +611,6 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
65F32F31299BD2F800CE9261 /* BackgroundServicePlugin.swift in Sources */,
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
A01DD69B2F7F43B40049AB63 /* ImageRequest.swift in Sources */,
B21E34AC2E5B09190031FDB9 /* BackgroundWorker.swift in Sources */,
@@ -642,7 +625,6 @@
B21E34AA2E5AFD2B0031FDB9 /* BackgroundWorkerApiImpl.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
B2BE315F2E5E5229006EEF88 /* BackgroundWorker.g.swift in Sources */,
65F32F33299D349D00CE9261 /* BackgroundSyncWorker.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1261,7 +1243,7 @@
repositoryURL = "https://github.com/pointfreeco/sqlite-data";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.3.0;
minimumVersion = 1.6.1;
};
};
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */ = {
@@ -1269,7 +1251,7 @@
repositoryURL = "https://github.com/apple/swift-http-structured-headers.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.5.0;
minimumVersion = 1.6.0;
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -19,31 +19,13 @@
"version" : "7.8.0"
}
},
{
"identity" : "opencombine",
"kind" : "remoteSourceControl",
"location" : "https://github.com/OpenCombine/OpenCombine.git",
"state" : {
"revision" : "8576f0d579b27020beccbccc3ea6844f3ddfc2c2",
"version" : "0.14.0"
}
},
{
"identity" : "sqlite-data",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/sqlite-data",
"state" : {
"revision" : "b66b894b9a5710f1072c8eb6448a7edfc2d743d9",
"version" : "1.3.0"
}
},
{
"identity" : "swift-case-paths",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-case-paths",
"state" : {
"revision" : "6989976265be3f8d2b5802c722f9ba168e227c71",
"version" : "1.7.2"
"revision" : "da3a94ed49c7a30d82853de551c07a93196e8cab",
"version" : "1.6.1"
}
},
{
@@ -96,8 +78,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-structured-headers.git",
"state" : {
"revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb",
"version" : "1.5.0"
"revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b",
"version" : "1.6.0"
}
},
{
@@ -141,8 +123,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-structured-queries",
"state" : {
"revision" : "9c84335373bae5f5c9f7b5f0adf3ae10f2cab5b9",
"version" : "0.25.2"
"revision" : "8da8818fccd9959bd683934ddc62cf45bb65b3c8",
"version" : "0.31.1"
}
},
{
@@ -154,15 +136,6 @@
"version" : "602.0.0"
}
},
{
"identity" : "swift-tagged",
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-tagged",
"state" : {
"revision" : "3907a9438f5b57d317001dc99f3f11b46882272b",
"version" : "0.10.0"
}
},
{
"identity" : "xctest-dynamic-overlay",
"kind" : "remoteSourceControl",
@@ -24,8 +24,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/sqlite-data",
"state" : {
"revision" : "05704b563ecb7f0bd7e49b6f360a6383a3e53e7d",
"version" : "1.5.1"
"revision" : "da3a94ed49c7a30d82853de551c07a93196e8cab",
"version" : "1.6.1"
}
},
{
@@ -78,8 +78,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-http-structured-headers.git",
"state" : {
"revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb",
"version" : "1.5.0"
"revision" : "76d7627bd88b47bf5a0f8497dd244885960dde0b",
"version" : "1.6.0"
}
},
{
@@ -123,8 +123,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-structured-queries",
"state" : {
"revision" : "d8163b3a98f3c8434c4361e85126db449d84bc66",
"version" : "0.30.0"
"revision" : "8da8818fccd9959bd683934ddc62cf45bb65b3c8",
"version" : "0.31.1"
}
},
{
+16 -46
View File
@@ -1,15 +1,7 @@
import BackgroundTasks
import Flutter
import native_video_player
import network_info_plus
import path_provider_foundation
import permission_handler_apple
import photo_manager
import shared_preferences_foundation
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
@@ -21,48 +13,26 @@ import UIKit
SwiftNativeVideoPlayerPlugin.cookieStorage = URLSessionManager.cookieStorage
URLSessionManager.patchBackgroundDownloader()
GeneratedPluginRegistrant.register(with: self)
let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
AppDelegate.registerPlugins(with: controller.engine, controller: controller)
BackgroundServicePlugin.register(with: self.registrar(forPlugin: "BackgroundServicePlugin")!)
BackgroundServicePlugin.registerBackgroundProcessing()
BackgroundWorkerApiImpl.registerBackgroundWorkers()
BackgroundServicePlugin.setPluginRegistrantCallback { registry in
if !registry.hasPlugin("org.cocoapods.path-provider-foundation") {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.path-provider-foundation")!)
}
if !registry.hasPlugin("org.cocoapods.photo-manager") {
PhotoManagerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.photo-manager")!)
}
if !registry.hasPlugin("org.cocoapods.shared-preferences-foundation") {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.shared-preferences-foundation")!)
}
if !registry.hasPlugin("org.cocoapods.permission-handler-apple") {
PermissionHandlerPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.permission-handler-apple")!)
}
if !registry.hasPlugin("org.cocoapods.network-info-plus") {
FPPNetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "org.cocoapods.network-info-plus")!)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
public static func registerPlugins(with engine: FlutterEngine, controller: FlutterViewController?) {
NativeSyncApiImpl.register(with: engine.registrar(forPlugin: NativeSyncApiImpl.name)!)
LocalImageApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: LocalImageApiImpl())
RemoteImageApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: RemoteImageApiImpl())
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: BackgroundWorkerApiImpl())
ConnectivityApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: ConnectivityApiImpl())
NetworkApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: NetworkApiImpl(viewController: controller))
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
let messenger = engineBridge.applicationRegistrar.messenger()
AppDelegate.registerPlugins(with: engineBridge.pluginRegistry, messenger: messenger)
}
public static func registerPlugins(with registry: FlutterPluginRegistry, messenger: FlutterBinaryMessenger) {
NativeSyncApiImpl.register(with: registry.registrar(forPlugin: NativeSyncApiImpl.name)!)
LocalImageApiSetup.setUp(binaryMessenger: messenger, api: LocalImageApiImpl())
RemoteImageApiSetup.setUp(binaryMessenger: messenger, api: RemoteImageApiImpl())
BackgroundWorkerFgHostApiSetup.setUp(binaryMessenger: messenger, api: BackgroundWorkerApiImpl())
ConnectivityApiSetup.setUp(binaryMessenger: messenger, api: ConnectivityApiImpl())
NetworkApiSetup.setUp(binaryMessenger: messenger, api: NetworkApiImpl())
}
public static func cancelPlugins(with engine: FlutterEngine) {
(engine.valuePublished(byPlugin: NativeSyncApiImpl.name) as? NativeSyncApiImpl)?.detachFromEngine()
}
+87 -34
View File
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
@@ -50,6 +50,19 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
return value as! T?
}
private func doubleEqualsBackgroundWorker(_ lhs: Double, _ rhs: Double) -> Bool {
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
}
private func doubleHashBackgroundWorker(_ value: Double, _ hasher: inout Hasher) {
if value.isNaN {
hasher.combine(0x7FF8000000000000)
} else {
// Normalize -0.0 to 0.0
hasher.combine(value == 0 ? 0 : value)
}
}
func deepEqualsBackgroundWorker(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
@@ -60,59 +73,92 @@ func deepEqualsBackgroundWorker(_ lhs: Any?, _ rhs: Any?) -> Bool {
case (nil, _), (_, nil):
return false
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
return true
case is (Void, Void):
return true
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
return cleanLhsHashable == cleanRhsHashable
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
for (index, element) in cleanLhsArray.enumerated() {
if !deepEqualsBackgroundWorker(element, cleanRhsArray[index]) {
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !deepEqualsBackgroundWorker(element, rhsArray[index]) {
return false
}
}
return true
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
for (key, cleanLhsValue) in cleanLhsDictionary {
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
if !deepEqualsBackgroundWorker(cleanLhsValue, cleanRhsDictionary[key]!) {
case (let lhsArray, let rhsArray) as ([Double], [Double]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !doubleEqualsBackgroundWorker(element, rhsArray[index]) {
return false
}
}
return true
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard lhsDictionary.count == rhsDictionary.count else { return false }
for (lhsKey, lhsValue) in lhsDictionary {
var found = false
for (rhsKey, rhsValue) in rhsDictionary {
if deepEqualsBackgroundWorker(lhsKey, rhsKey) {
if deepEqualsBackgroundWorker(lhsValue, rhsValue) {
found = true
break
} else {
return false
}
}
}
if !found { return false }
}
return true
case (let lhs as Double, let rhs as Double):
return doubleEqualsBackgroundWorker(lhs, rhs)
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
return lhsHashable == rhsHashable
default:
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
return false
}
}
func deepHashBackgroundWorker(value: Any?, hasher: inout Hasher) {
if let valueList = value as? [AnyHashable] {
for item in valueList { deepHashBackgroundWorker(value: item, hasher: &hasher) }
return
}
if let valueDict = value as? [AnyHashable: AnyHashable] {
for key in valueDict.keys {
hasher.combine(key)
deepHashBackgroundWorker(value: valueDict[key]!, hasher: &hasher)
let cleanValue = nilOrValue(value) as Any?
if let cleanValue = cleanValue {
if let doubleValue = cleanValue as? Double {
doubleHashBackgroundWorker(doubleValue, &hasher)
} else if let valueList = cleanValue as? [Any?] {
for item in valueList {
deepHashBackgroundWorker(value: item, hasher: &hasher)
}
} else if let valueList = cleanValue as? [Double] {
for item in valueList {
doubleHashBackgroundWorker(item, &hasher)
}
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
var result = 0
for (key, value) in valueDict {
var entryKeyHasher = Hasher()
deepHashBackgroundWorker(value: key, hasher: &entryKeyHasher)
var entryValueHasher = Hasher()
deepHashBackgroundWorker(value: value, hasher: &entryValueHasher)
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
}
hasher.combine(result)
} else if let hashableValue = cleanValue as? AnyHashable {
hasher.combine(hashableValue)
} else {
hasher.combine(String(describing: cleanValue))
}
return
} else {
hasher.combine(0)
}
if let hashableValue = value as? AnyHashable {
hasher.combine(hashableValue.hashValue)
}
return hasher.combine(String(describing: value))
}
/// Generated class from Pigeon that represents data sent in messages.
struct BackgroundWorkerSettings: Hashable {
@@ -137,9 +183,16 @@ struct BackgroundWorkerSettings: Hashable {
]
}
static func == (lhs: BackgroundWorkerSettings, rhs: BackgroundWorkerSettings) -> Bool {
return deepEqualsBackgroundWorker(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsBackgroundWorker(lhs.requiresCharging, rhs.requiresCharging) && deepEqualsBackgroundWorker(lhs.minimumDelaySeconds, rhs.minimumDelaySeconds)
}
func hash(into hasher: inout Hasher) {
deepHashBackgroundWorker(value: toList(), hasher: &hasher)
hasher.combine("BackgroundWorkerSettings")
deepHashBackgroundWorker(value: requiresCharging, hasher: &hasher)
deepHashBackgroundWorker(value: minimumDelaySeconds, hasher: &hasher)
}
}
@@ -95,7 +95,7 @@ class BackgroundWorker: BackgroundWorkerBgHostApi {
// Register plugins in the new engine
GeneratedPluginRegistrant.register(with: engine)
// Register custom plugins
AppDelegate.registerPlugins(with: engine, controller: nil)
AppDelegate.registerPlugins(with: engine, messenger: engine.binaryMessenger)
flutterApi = BackgroundWorkerFlutterApi(binaryMessenger: engine.binaryMessenger)
BackgroundWorkerBgHostApiSetup.setUp(binaryMessenger: engine.binaryMessenger, api: self)
@@ -1,408 +0,0 @@
//
// BackgroundServicePlugin.swift
// Runner
//
// Created by Marty Fuhry on 2/14/23.
//
import Flutter
import BackgroundTasks
import path_provider_foundation
import CryptoKit
import Network
class BackgroundServicePlugin: NSObject, FlutterPlugin {
public static var flutterPluginRegistrantCallback: FlutterPluginRegistrantCallback?
public static func setPluginRegistrantCallback(_ callback: FlutterPluginRegistrantCallback) {
flutterPluginRegistrantCallback = callback
}
// Pause the application in XCode, then enter
// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundFetch"]
// or
// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"app.alextran.immich.backgroundProcessing"]
// Then resume the application see the background code run
// Tested on a physical device, not a simulator
// This will submit either the Fetch or Processing command to the BGTaskScheduler for immediate processing.
// In my tests, I can only get app.alextran.immich.backgroundProcessing simulated by running the above command
// This is the task ID in Info.plist to register as our background task ID
public static let backgroundFetchTaskID = "app.alextran.immich.backgroundFetch"
public static let backgroundProcessingTaskID = "app.alextran.immich.backgroundProcessing"
// Establish communication with the main isolate and set up the channel call
// to this BackgroundServicePlugion()
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "immich/foregroundChannel",
binaryMessenger: registrar.messenger()
)
let instance = BackgroundServicePlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
registrar.addApplicationDelegate(instance)
}
// Registers the Flutter engine with the plugins, used by the other Background Flutter engine
public static func register(engine: FlutterEngine) {
GeneratedPluginRegistrant.register(with: engine)
}
// Registers the task IDs from the system so that we can process them here in this class
public static func registerBackgroundProcessing() {
let processingRegisterd = BGTaskScheduler.shared.register(
forTaskWithIdentifier: backgroundProcessingTaskID,
using: nil) { task in
if task is BGProcessingTask {
handleBackgroundProcessing(task: task as! BGProcessingTask)
}
}
let fetchRegisterd = BGTaskScheduler.shared.register(
forTaskWithIdentifier: backgroundFetchTaskID,
using: nil) { task in
if task is BGAppRefreshTask {
handleBackgroundFetch(task: task as! BGAppRefreshTask)
}
}
}
// Handles the channel methods from Flutter
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "enable":
handleBackgroundEnable(call: call, result: result)
break
case "configure":
handleConfigure(call: call, result: result)
break
case "disable":
handleDisable(call: call, result: result)
break
case "isEnabled":
handleIsEnabled(call: call, result: result)
break
case "isIgnoringBatteryOptimizations":
result(FlutterMethodNotImplemented)
break
case "lastBackgroundFetchTime":
let defaults = UserDefaults.standard
let lastRunTime = defaults.value(forKey: "last_background_fetch_run_time")
result(lastRunTime)
break
case "lastBackgroundProcessingTime":
let defaults = UserDefaults.standard
let lastRunTime = defaults.value(forKey: "last_background_processing_run_time")
result(lastRunTime)
break
case "numberOfBackgroundProcesses":
handleNumberOfProcesses(call: call, result: result)
break
case "backgroundAppRefreshEnabled":
handleBackgroundRefreshStatus(call: call, result: result)
break
case "digestFiles":
handleDigestFiles(call: call, result: result)
break
default:
result(FlutterMethodNotImplemented)
break
}
}
// Calculates the SHA-1 hash of each file from the list of paths provided
func handleDigestFiles(call: FlutterMethodCall, result: @escaping FlutterResult) {
let bufsize = 2 * 1024 * 1024
// Private error to throw if file cannot be read
enum DigestError: String, LocalizedError {
case NoFileHandle = "Cannot Open File Handle"
public var errorDescription: String? { self.rawValue }
}
// Parse the arguments or else fail
guard let args = call.arguments as? Array<String> else {
print("Cannot parse args as array: \(String(describing: call.arguments))")
result(FlutterError(code: "Malformed",
message: "Received args is not an Array<String>",
details: nil))
return
}
// Compute hash in background thread
DispatchQueue.global(qos: .background).async {
var hashes: [FlutterStandardTypedData?] = Array(repeating: nil, count: args.count)
for i in (0 ..< args.count) {
do {
guard let file = FileHandle(forReadingAtPath: args[i]) else { throw DigestError.NoFileHandle }
var hasher = Insecure.SHA1.init();
while autoreleasepool(invoking: {
let chunk = file.readData(ofLength: bufsize)
guard !chunk.isEmpty else { return false } // EOF
hasher.update(data: chunk)
return true // continue
}) { }
let digest = hasher.finalize()
hashes[i] = FlutterStandardTypedData(bytes: Data(Array(digest.makeIterator())))
} catch {
print("Cannot calculate the digest of the file \(args[i]) due to \(error.localizedDescription)")
}
}
// Return result in main thread
DispatchQueue.main.async {
result(Array(hashes))
}
}
}
// Called by the flutter code when enabled so that we can turn on the background services
// and save the callback information to communicate on this method channel
public func handleBackgroundEnable(call: FlutterMethodCall, result: FlutterResult) {
// Needs to parse the arguments from the method call
guard let args = call.arguments as? Array<Any> else {
print("Cannot parse args as array: \(call.arguments)")
result(FlutterMethodNotImplemented)
return
}
// Requires 3 arguments in the array
guard args.count == 3 else {
print("Requires 3 arguments and received \(args.count)")
result(FlutterMethodNotImplemented)
return
}
// Parses the arguments
let callbackHandle = args[0] as? Int64
let notificationTitle = args[1] as? String
let instant = args[2] as? Bool
// Write enabled to settings
let defaults = UserDefaults.standard
// We are now enabled, so store this
defaults.set(true, forKey: "background_service_enabled")
// The callback handle is an int64 address to communicate with the main isolate's
// entry function
defaults.set(callbackHandle, forKey: "callback_handle")
// This is not used yet and will need to be implemented
defaults.set(notificationTitle, forKey: "notification_title")
// Schedule the background services
BackgroundServicePlugin.scheduleBackgroundSync()
BackgroundServicePlugin.scheduleBackgroundFetch()
result(true)
}
// Called by the flutter code at launch to see if the background service is enabled or not
func handleIsEnabled(call: FlutterMethodCall, result: FlutterResult) {
let defaults = UserDefaults.standard
let enabled = defaults.value(forKey: "background_service_enabled") as? Bool
// False by default
result(enabled ?? false)
}
// Called by the Flutter code whenever a change in configuration is set
func handleConfigure(call: FlutterMethodCall, result: FlutterResult) {
// Needs to be able to parse the arguments or else fail
guard let args = call.arguments as? Array<Any> else {
print("Cannot parse args as array: \(call.arguments)")
result(FlutterError())
return
}
// Needs to have 4 arguments in the call or else fail
guard args.count == 4 else {
print("Not enough arguments, 4 required: \(args.count) given")
result(FlutterError())
return
}
// Parse the arguments from the method call
let requireUnmeteredNetwork = args[0] as? Bool
let requireCharging = args[1] as? Bool
let triggerUpdateDelay = args[2] as? Int
let triggerMaxDelay = args[3] as? Int
// Store the values from the call in the defaults
let defaults = UserDefaults.standard
defaults.set(requireUnmeteredNetwork, forKey: "require_unmetered_network")
defaults.set(requireCharging, forKey: "require_charging")
defaults.set(triggerUpdateDelay, forKey: "trigger_update_delay")
defaults.set(triggerMaxDelay, forKey: "trigger_max_delay")
// Cancel the background services and reschedule them
BGTaskScheduler.shared.cancelAllTaskRequests()
BackgroundServicePlugin.scheduleBackgroundSync()
BackgroundServicePlugin.scheduleBackgroundFetch()
result(true)
}
// Returns the number of currently scheduled background processes to Flutter, strictly
// for debugging
func handleNumberOfProcesses(call: FlutterMethodCall, result: @escaping FlutterResult) {
BGTaskScheduler.shared.getPendingTaskRequests { requests in
result(requests.count)
}
}
// Disables the service, cancels all the task requests
func handleDisable(call: FlutterMethodCall, result: FlutterResult) {
let defaults = UserDefaults.standard
defaults.set(false, forKey: "background_service_enabled")
BGTaskScheduler.shared.cancelAllTaskRequests()
result(true)
}
// Checks the status of the Background App Refresh from the system
// Returns true if the service is enabled for Immich, and false otherwise
func handleBackgroundRefreshStatus(call: FlutterMethodCall, result: FlutterResult) {
switch UIApplication.shared.backgroundRefreshStatus {
case .available:
result(true)
break
case .denied:
result(false)
break
case .restricted:
result(false)
break
default:
result(false)
break
}
}
// Schedules a short-running background sync to sync only a few photos
static func scheduleBackgroundFetch() {
// We will schedule this task to run no matter the charging or wifi requirents from the end user
// 1. They can set Background App Refresh to Off / Wi-Fi / Wi-Fi & Cellular Data from Settings
// 2. We will check the battery connectivity when we begin running the background activity
let backgroundFetch = BGAppRefreshTaskRequest(identifier: BackgroundServicePlugin.backgroundFetchTaskID)
// Use 5 minutes from now as earliest begin date
backgroundFetch.earliestBeginDate = Date(timeIntervalSinceNow: 5 * 60)
do {
try BGTaskScheduler.shared.submit(backgroundFetch)
} catch {
print("Could not schedule the background task \(error.localizedDescription)")
}
}
// Schedules a long-running background sync for syncing all of the photos
static func scheduleBackgroundSync() {
let backgroundProcessing = BGProcessingTaskRequest(identifier: BackgroundServicePlugin.backgroundProcessingTaskID)
// We need the values for requiring charging
let defaults = UserDefaults.standard
let requireCharging = defaults.value(forKey: "require_charging") as? Bool
// Always require network connectivity, and set the require charging from the above
backgroundProcessing.requiresNetworkConnectivity = true
backgroundProcessing.requiresExternalPower = requireCharging ?? true
// Use 15 minutes from now as earliest begin date
backgroundProcessing.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
do {
// Submit the task to the scheduler
try BGTaskScheduler.shared.submit(backgroundProcessing)
} catch {
print("Could not schedule the background task \(error.localizedDescription)")
}
}
// This function runs when the system kicks off the BGAppRefreshTask from the Background Task Scheduler
static func handleBackgroundFetch(task: BGAppRefreshTask) {
// Schedule the next sync task so we can run this again later
scheduleBackgroundFetch()
// Log the time of last background processing to now
let defaults = UserDefaults.standard
defaults.set(Date().timeIntervalSince1970, forKey: "last_background_fetch_run_time")
// If we have required charging, we should check the charging status
let requireCharging = defaults.value(forKey: "require_charging") as? Bool ?? false
if (requireCharging) {
UIDevice.current.isBatteryMonitoringEnabled = true
if (UIDevice.current.batteryState == .unplugged) {
// The device is unplugged and we have required charging
// Therefore, we will simply complete the task without
// running it.
task.setTaskCompleted(success: true)
return
}
}
// If we have required Wi-Fi, we can check the isExpensive property
let requireWifi = defaults.value(forKey: "require_wifi") as? Bool ?? false
if (requireWifi) {
let wifiMonitor = NWPathMonitor(requiredInterfaceType: .wifi)
let isExpensive = wifiMonitor.currentPath.isExpensive
if (isExpensive) {
// The network is expensive and we have required Wi-Fi
// Therefore, we will simply complete the task without
// running it
task.setTaskCompleted(success: true)
return
}
}
// Schedule the next sync task so we can run this again later
scheduleBackgroundFetch()
// The background sync task should only run for 20 seconds at most
BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: 20)
}
// This function runs when the system kicks off the BGProcessingTask from the Background Task Scheduler
static func handleBackgroundProcessing(task: BGProcessingTask) {
// Schedule the next sync task so we run this again later
scheduleBackgroundSync()
// Log the time of last background processing to now
let defaults = UserDefaults.standard
defaults.set(Date().timeIntervalSince1970, forKey: "last_background_processing_run_time")
// We won't specify a max time for the background sync service, so this can run for longer
BackgroundServicePlugin.runBackgroundSync(task, maxSeconds: nil)
}
// This is a synchronous function which uses a semaphore to run the background sync worker's run
// function, which will create a background Isolate and communicate with the Flutter code to back
// up the assets. When it completes, we signal the semaphore and complete the execution allowing the
// control to pass back to the caller synchronously
static func runBackgroundSync(_ task: BGTask, maxSeconds: Int?) {
let semaphore = DispatchSemaphore(value: 0)
DispatchQueue.main.async {
let backgroundWorker = BackgroundSyncWorker { _ in
semaphore.signal()
}
task.expirationHandler = {
backgroundWorker.cancel()
task.setTaskCompleted(success: true)
}
backgroundWorker.run(maxSeconds: maxSeconds)
task.setTaskCompleted(success: true)
}
semaphore.wait()
}
}
@@ -1,271 +0,0 @@
//
// BackgroundSyncProcessing.swift
// Runner
//
// Created by Marty Fuhry on 2/6/23.
//
// Credit to https://github.com/fluttercommunity/flutter_workmanager/blob/main/ios/Classes/BackgroundWorker.swift
import Foundation
import Flutter
import BackgroundTasks
// The background worker which creates a new Flutter VM, communicates with it
// to run the backup job, and then finishes execution and calls back to its callback
// handler
class BackgroundSyncWorker {
// The Flutter engine we create for background execution.
// This is not the main Flutter engine which shows the UI,
// this is a brand new isolate created and managed in this code
// here. It does not share memory with the main
// Flutter engine which shows the UI.
// It needs to be started up, registered, and torn down here
let engine: FlutterEngine? = FlutterEngine(
name: "BackgroundImmich"
)
let notificationId = "com.alextran.immich/backgroundNotifications"
// The background message passing channel
var channel: FlutterMethodChannel?
var completionHandler: (UIBackgroundFetchResult) -> Void
let taskSessionStart = Date()
// We need the completion handler to tell the system when we are done running
init(_ completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
// This is the background message passing channel to be used with the background engine
// created here in this platform code
self.channel = FlutterMethodChannel(
name: "immich/backgroundChannel",
binaryMessenger: engine!.binaryMessenger
)
self.completionHandler = completionHandler
}
// Handles all of the messages from the Flutter VM called into this platform code
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "initialized":
// Initialize tells us that we can now call into the Flutter VM to tell it to begin the update
self.channel?.invokeMethod(
"backgroundProcessing",
arguments: nil,
result: { flutterResult in
// This is the result we send back to the BGTaskScheduler to let it know whether we'll need more time later or
// if this execution failed
let result: UIBackgroundFetchResult = (flutterResult as? Bool ?? false) ? .newData : .failed
// Show the task duration
let taskSessionCompleter = Date()
let taskDuration = taskSessionCompleter.timeIntervalSince(self.taskSessionStart)
print("[\(String(describing: self))] \(#function) -> performBackgroundRequest.\(result) (finished in \(taskDuration) seconds)")
// Complete the execution
self.complete(result)
})
break
case "updateNotification":
let handled = self.handleNotification(call)
result(handled)
break
case "showError":
let handled = self.handleError(call)
result(handled)
break
case "clearErrorNotifications":
self.handleClearErrorNotifications()
result(true)
break
case "hasContentChanged":
// This is only called for Android, but we provide an implementation here
// telling Flutter that we don't have any information about whether the gallery
// contents have changed or not, so we can just say "no, they've not changed"
result(false)
break
default:
result(FlutterError())
self.complete(UIBackgroundFetchResult.failed)
}
}
// Runs the background sync by starting up a new isolate and handling the calls
// until it completes
public func run(maxSeconds: Int?) {
// We need the callback handle to start up the Flutter VM from the entry point
let defaults = UserDefaults.standard
guard let callbackHandle = defaults.value(forKey: "callback_handle") as? Int64 else {
// Can't find the callback handle, this is fatal
complete(UIBackgroundFetchResult.failed)
return
}
// Use the provided callbackHandle to get the callback function
guard let callback = FlutterCallbackCache.lookupCallbackInformation(callbackHandle) else {
// We need this callback or else this is fatal
complete(UIBackgroundFetchResult.failed)
return
}
// Sanity check for the engine existing
if engine == nil {
complete(UIBackgroundFetchResult.failed)
return
}
// Run the engine
let isRunning = engine!.run(
withEntrypoint: callback.callbackName,
libraryURI: callback.callbackLibraryPath
)
// If this engine isn't running, this is fatal
if !isRunning {
complete(UIBackgroundFetchResult.failed)
return
}
// If we have a timer, we need to start the timer to cancel ourselves
// so that we don't run longer than the provided maxSeconds
// After maxSeconds has elapsed, we will invoke "systemStop"
if maxSeconds != nil {
// Schedule a non-repeating timer to run after maxSeconds
let timer = Timer.scheduledTimer(withTimeInterval: TimeInterval(maxSeconds!),
repeats: false) { timer in
// The callback invalidates the timer and stops execution
timer.invalidate()
// If the channel is already deallocated, we don't need to do anything
if self.channel == nil {
return
}
// Tell the Flutter VM to stop backing up now
self.channel?.invokeMethod(
"systemStop",
arguments: nil,
result: nil)
// Complete the execution
self.complete(UIBackgroundFetchResult.newData)
}
}
// Set the handle function to the channel message handler
self.channel?.setMethodCallHandler(handle)
// Register this to get access to the plugins on the platform channel
BackgroundServicePlugin.flutterPluginRegistrantCallback?(engine!)
}
// Cancels execution of this task, used by the system's task expiration handler
// which is called shortly before execution is about to expire
public func cancel() {
// If the channel is already deallocated, we don't need to do anything
if self.channel == nil {
return
}
// Tell the Flutter VM to stop backing up now
self.channel?.invokeMethod(
"systemStop",
arguments: nil,
result: nil)
// Complete the execution
self.complete(UIBackgroundFetchResult.newData)
}
// Completes the execution, destroys the engine, and sends a completion to our callback completionHandler
private func complete(_ fetchResult: UIBackgroundFetchResult) {
engine?.destroyContext()
channel = nil
completionHandler(fetchResult)
}
private func handleNotification(_ call: FlutterMethodCall) -> Bool {
// Parse the arguments as an array list
guard let args = call.arguments as? Array<Any> else {
print("Failed to parse \(call.arguments) as array")
return false;
}
// Requires 7 arguments passed or else fail
guard args.count == 7 else {
print("Needs 7 arguments, but was only passed \(args.count)")
return false
}
// Parse the arguments to send the notification update
let title = args[0] as? String
let content = args[1] as? String
let progress = args[2] as? Int
let maximum = args[3] as? Int
let indeterminate = args[4] as? Bool
let isDetail = args[5] as? Bool
let onlyIfForeground = args[6] as? Bool
// Build the notification
let notificationContent = UNMutableNotificationContent()
notificationContent.body = content ?? "Uploading..."
notificationContent.title = title ?? "Immich"
// Add it to the Notification center
let notification = UNNotificationRequest(
identifier: notificationId,
content: notificationContent,
trigger: nil
)
let center = UNUserNotificationCenter.current()
center.add(notification) { (error: Error?) in
if let theError = error {
print("Error showing notifications: \(theError)")
}
}
return true
}
private func handleError(_ call: FlutterMethodCall) -> Bool {
// Parse the arguments as an array list
guard let args = call.arguments as? Array<Any> else {
return false;
}
// Requires 7 arguments passed or else fail
guard args.count == 3 else {
return false
}
let title = args[0] as? String
let content = args[1] as? String
let individualTag = args[2] as? String
// Build the notification
let notificationContent = UNMutableNotificationContent()
notificationContent.body = content ?? "Error running the backup job."
notificationContent.title = title ?? "Immich"
// Add it to the Notification center
let notification = UNNotificationRequest(
identifier: notificationId,
content: notificationContent,
trigger: nil
)
let center = UNUserNotificationCenter.current()
center.add(notification)
return true
}
private func handleClearErrorNotifications() {
let center = UNUserNotificationCenter.current()
center.removeDeliveredNotifications(withIdentifiers: [notificationId])
center.removePendingNotificationRequests(withIdentifiers: [notificationId])
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
+98 -36
View File
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
@@ -46,6 +46,19 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
return value as! T?
}
private func doubleEqualsNetwork(_ lhs: Double, _ rhs: Double) -> Bool {
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
}
private func doubleHashNetwork(_ value: Double, _ hasher: inout Hasher) {
if value.isNaN {
hasher.combine(0x7FF8000000000000)
} else {
// Normalize -0.0 to 0.0
hasher.combine(value == 0 ? 0 : value)
}
}
func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
@@ -56,59 +69,92 @@ func deepEqualsNetwork(_ lhs: Any?, _ rhs: Any?) -> Bool {
case (nil, _), (_, nil):
return false
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
return true
case is (Void, Void):
return true
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
return cleanLhsHashable == cleanRhsHashable
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
for (index, element) in cleanLhsArray.enumerated() {
if !deepEqualsNetwork(element, cleanRhsArray[index]) {
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !deepEqualsNetwork(element, rhsArray[index]) {
return false
}
}
return true
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
for (key, cleanLhsValue) in cleanLhsDictionary {
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
if !deepEqualsNetwork(cleanLhsValue, cleanRhsDictionary[key]!) {
case (let lhsArray, let rhsArray) as ([Double], [Double]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !doubleEqualsNetwork(element, rhsArray[index]) {
return false
}
}
return true
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard lhsDictionary.count == rhsDictionary.count else { return false }
for (lhsKey, lhsValue) in lhsDictionary {
var found = false
for (rhsKey, rhsValue) in rhsDictionary {
if deepEqualsNetwork(lhsKey, rhsKey) {
if deepEqualsNetwork(lhsValue, rhsValue) {
found = true
break
} else {
return false
}
}
}
if !found { return false }
}
return true
case (let lhs as Double, let rhs as Double):
return doubleEqualsNetwork(lhs, rhs)
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
return lhsHashable == rhsHashable
default:
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
return false
}
}
func deepHashNetwork(value: Any?, hasher: inout Hasher) {
if let valueList = value as? [AnyHashable] {
for item in valueList { deepHashNetwork(value: item, hasher: &hasher) }
return
}
if let valueDict = value as? [AnyHashable: AnyHashable] {
for key in valueDict.keys {
hasher.combine(key)
deepHashNetwork(value: valueDict[key]!, hasher: &hasher)
let cleanValue = nilOrValue(value) as Any?
if let cleanValue = cleanValue {
if let doubleValue = cleanValue as? Double {
doubleHashNetwork(doubleValue, &hasher)
} else if let valueList = cleanValue as? [Any?] {
for item in valueList {
deepHashNetwork(value: item, hasher: &hasher)
}
} else if let valueList = cleanValue as? [Double] {
for item in valueList {
doubleHashNetwork(item, &hasher)
}
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
var result = 0
for (key, value) in valueDict {
var entryKeyHasher = Hasher()
deepHashNetwork(value: key, hasher: &entryKeyHasher)
var entryValueHasher = Hasher()
deepHashNetwork(value: value, hasher: &entryValueHasher)
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
}
hasher.combine(result)
} else if let hashableValue = cleanValue as? AnyHashable {
hasher.combine(hashableValue)
} else {
hasher.combine(String(describing: cleanValue))
}
return
} else {
hasher.combine(0)
}
if let hashableValue = value as? AnyHashable {
hasher.combine(hashableValue.hashValue)
}
return hasher.combine(String(describing: value))
}
/// Generated class from Pigeon that represents data sent in messages.
struct ClientCertData: Hashable {
@@ -133,9 +179,16 @@ struct ClientCertData: Hashable {
]
}
static func == (lhs: ClientCertData, rhs: ClientCertData) -> Bool {
return deepEqualsNetwork(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsNetwork(lhs.data, rhs.data) && deepEqualsNetwork(lhs.password, rhs.password)
}
func hash(into hasher: inout Hasher) {
deepHashNetwork(value: toList(), hasher: &hasher)
hasher.combine("ClientCertData")
deepHashNetwork(value: data, hasher: &hasher)
deepHashNetwork(value: password, hasher: &hasher)
}
}
@@ -170,9 +223,18 @@ struct ClientCertPrompt: Hashable {
]
}
static func == (lhs: ClientCertPrompt, rhs: ClientCertPrompt) -> Bool {
return deepEqualsNetwork(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsNetwork(lhs.title, rhs.title) && deepEqualsNetwork(lhs.message, rhs.message) && deepEqualsNetwork(lhs.cancel, rhs.cancel) && deepEqualsNetwork(lhs.confirm, rhs.confirm)
}
func hash(into hasher: inout Hasher) {
deepHashNetwork(value: toList(), hasher: &hasher)
hasher.combine("ClientCertPrompt")
deepHashNetwork(value: title, hasher: &hasher)
deepHashNetwork(value: message, hasher: &hasher)
deepHashNetwork(value: cancel, hasher: &hasher)
deepHashNetwork(value: confirm, hasher: &hasher)
}
}
+7 -4
View File
@@ -10,11 +10,14 @@ enum ImportError: Error {
}
class NetworkApiImpl: NetworkApi {
weak var viewController: UIViewController?
private var activeImporter: CertImporter?
init(viewController: UIViewController?) {
self.viewController = viewController
private var viewController: UIViewController? {
UIApplication.shared.connectedScenes
.compactMap { $0 as? UIWindowScene }
.flatMap { $0.windows }
.first { $0.isKeyWindow }?
.rootViewController
}
func selectCertificate(promptText: ClientCertPrompt, completion: @escaping (Result<Void, any Error>) -> Void) {
+2 -2
View File
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
+2 -2
View File
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@@ -32,7 +32,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
+21 -4
View File
@@ -8,8 +8,6 @@
<array>
<string>app.alextran.immich.background.refreshUpload</string>
<string>app.alextran.immich.background.processingUpload</string>
<string>app.alextran.immich.backgroundFetch</string>
<string>app.alextran.immich.backgroundProcessing</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
@@ -108,8 +106,6 @@
</array>
<key>CFBundleVersion</key>
<string>240</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
@@ -154,6 +150,27 @@
<array>
<string>INSendMessageIntent</string>
</array>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
+146 -46
View File
@@ -1,4 +1,4 @@
// Autogenerated from Pigeon (v26.0.2), do not edit directly.
// Autogenerated from Pigeon (v26.3.4), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
@@ -50,7 +50,7 @@ private func wrapError(_ error: Any) -> [Any?] {
}
return [
"\(error)",
"\(type(of: error))",
"\(Swift.type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
@@ -64,6 +64,19 @@ private func nilOrValue<T>(_ value: Any?) -> T? {
return value as! T?
}
private func doubleEqualsMessages(_ lhs: Double, _ rhs: Double) -> Bool {
return (lhs.isNaN && rhs.isNaN) || lhs == rhs
}
private func doubleHashMessages(_ value: Double, _ hasher: inout Hasher) {
if value.isNaN {
hasher.combine(0x7FF8000000000000)
} else {
// Normalize -0.0 to 0.0
hasher.combine(value == 0 ? 0 : value)
}
}
func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
@@ -74,59 +87,92 @@ func deepEqualsMessages(_ lhs: Any?, _ rhs: Any?) -> Bool {
case (nil, _), (_, nil):
return false
case (let lhs as AnyObject, let rhs as AnyObject) where lhs === rhs:
return true
case is (Void, Void):
return true
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
return cleanLhsHashable == cleanRhsHashable
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
for (index, element) in cleanLhsArray.enumerated() {
if !deepEqualsMessages(element, cleanRhsArray[index]) {
case (let lhsArray, let rhsArray) as ([Any?], [Any?]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !deepEqualsMessages(element, rhsArray[index]) {
return false
}
}
return true
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
for (key, cleanLhsValue) in cleanLhsDictionary {
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
if !deepEqualsMessages(cleanLhsValue, cleanRhsDictionary[key]!) {
case (let lhsArray, let rhsArray) as ([Double], [Double]):
guard lhsArray.count == rhsArray.count else { return false }
for (index, element) in lhsArray.enumerated() {
if !doubleEqualsMessages(element, rhsArray[index]) {
return false
}
}
return true
case (let lhsDictionary, let rhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard lhsDictionary.count == rhsDictionary.count else { return false }
for (lhsKey, lhsValue) in lhsDictionary {
var found = false
for (rhsKey, rhsValue) in rhsDictionary {
if deepEqualsMessages(lhsKey, rhsKey) {
if deepEqualsMessages(lhsValue, rhsValue) {
found = true
break
} else {
return false
}
}
}
if !found { return false }
}
return true
case (let lhs as Double, let rhs as Double):
return doubleEqualsMessages(lhs, rhs)
case (let lhsHashable, let rhsHashable) as (AnyHashable, AnyHashable):
return lhsHashable == rhsHashable
default:
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
return false
}
}
func deepHashMessages(value: Any?, hasher: inout Hasher) {
if let valueList = value as? [AnyHashable] {
for item in valueList { deepHashMessages(value: item, hasher: &hasher) }
return
}
if let valueDict = value as? [AnyHashable: AnyHashable] {
for key in valueDict.keys {
hasher.combine(key)
deepHashMessages(value: valueDict[key]!, hasher: &hasher)
let cleanValue = nilOrValue(value) as Any?
if let cleanValue = cleanValue {
if let doubleValue = cleanValue as? Double {
doubleHashMessages(doubleValue, &hasher)
} else if let valueList = cleanValue as? [Any?] {
for item in valueList {
deepHashMessages(value: item, hasher: &hasher)
}
} else if let valueList = cleanValue as? [Double] {
for item in valueList {
doubleHashMessages(item, &hasher)
}
} else if let valueDict = cleanValue as? [AnyHashable: Any?] {
var result = 0
for (key, value) in valueDict {
var entryKeyHasher = Hasher()
deepHashMessages(value: key, hasher: &entryKeyHasher)
var entryValueHasher = Hasher()
deepHashMessages(value: value, hasher: &entryValueHasher)
result = result &+ ((entryKeyHasher.finalize() &* 31) ^ entryValueHasher.finalize())
}
hasher.combine(result)
} else if let hashableValue = cleanValue as? AnyHashable {
hasher.combine(hashableValue)
} else {
hasher.combine(String(describing: cleanValue))
}
return
} else {
hasher.combine(0)
}
if let hashableValue = value as? AnyHashable {
hasher.combine(hashableValue.hashValue)
}
return hasher.combine(String(describing: value))
}
enum PlatformAssetPlaybackStyle: Int {
case unknown = 0
@@ -146,7 +192,7 @@ struct PlatformAsset: Hashable {
var updatedAt: Int64? = nil
var width: Int64? = nil
var height: Int64? = nil
var durationInSeconds: Int64
var durationMs: Int64
var orientation: Int64
var isFavorite: Bool
var adjustmentTime: Int64? = nil
@@ -164,7 +210,7 @@ struct PlatformAsset: Hashable {
let updatedAt: Int64? = nilOrValue(pigeonVar_list[4])
let width: Int64? = nilOrValue(pigeonVar_list[5])
let height: Int64? = nilOrValue(pigeonVar_list[6])
let durationInSeconds = pigeonVar_list[7] as! Int64
let durationMs = pigeonVar_list[7] as! Int64
let orientation = pigeonVar_list[8] as! Int64
let isFavorite = pigeonVar_list[9] as! Bool
let adjustmentTime: Int64? = nilOrValue(pigeonVar_list[10])
@@ -180,7 +226,7 @@ struct PlatformAsset: Hashable {
updatedAt: updatedAt,
width: width,
height: height,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
orientation: orientation,
isFavorite: isFavorite,
adjustmentTime: adjustmentTime,
@@ -198,7 +244,7 @@ struct PlatformAsset: Hashable {
updatedAt,
width,
height,
durationInSeconds,
durationMs,
orientation,
isFavorite,
adjustmentTime,
@@ -208,9 +254,28 @@ struct PlatformAsset: Hashable {
]
}
static func == (lhs: PlatformAsset, rhs: PlatformAsset) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.type, rhs.type) && deepEqualsMessages(lhs.createdAt, rhs.createdAt) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.width, rhs.width) && deepEqualsMessages(lhs.height, rhs.height) && deepEqualsMessages(lhs.durationMs, rhs.durationMs) && deepEqualsMessages(lhs.orientation, rhs.orientation) && deepEqualsMessages(lhs.isFavorite, rhs.isFavorite) && deepEqualsMessages(lhs.adjustmentTime, rhs.adjustmentTime) && deepEqualsMessages(lhs.latitude, rhs.latitude) && deepEqualsMessages(lhs.longitude, rhs.longitude) && deepEqualsMessages(lhs.playbackStyle, rhs.playbackStyle)
}
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
hasher.combine("PlatformAsset")
deepHashMessages(value: id, hasher: &hasher)
deepHashMessages(value: name, hasher: &hasher)
deepHashMessages(value: type, hasher: &hasher)
deepHashMessages(value: createdAt, hasher: &hasher)
deepHashMessages(value: updatedAt, hasher: &hasher)
deepHashMessages(value: width, hasher: &hasher)
deepHashMessages(value: height, hasher: &hasher)
deepHashMessages(value: durationMs, hasher: &hasher)
deepHashMessages(value: orientation, hasher: &hasher)
deepHashMessages(value: isFavorite, hasher: &hasher)
deepHashMessages(value: adjustmentTime, hasher: &hasher)
deepHashMessages(value: latitude, hasher: &hasher)
deepHashMessages(value: longitude, hasher: &hasher)
deepHashMessages(value: playbackStyle, hasher: &hasher)
}
}
@@ -249,9 +314,19 @@ struct PlatformAlbum: Hashable {
]
}
static func == (lhs: PlatformAlbum, rhs: PlatformAlbum) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.id, rhs.id) && deepEqualsMessages(lhs.name, rhs.name) && deepEqualsMessages(lhs.updatedAt, rhs.updatedAt) && deepEqualsMessages(lhs.isCloud, rhs.isCloud) && deepEqualsMessages(lhs.assetCount, rhs.assetCount)
}
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
hasher.combine("PlatformAlbum")
deepHashMessages(value: id, hasher: &hasher)
deepHashMessages(value: name, hasher: &hasher)
deepHashMessages(value: updatedAt, hasher: &hasher)
deepHashMessages(value: isCloud, hasher: &hasher)
deepHashMessages(value: assetCount, hasher: &hasher)
}
}
@@ -286,9 +361,18 @@ struct SyncDelta: Hashable {
]
}
static func == (lhs: SyncDelta, rhs: SyncDelta) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.hasChanges, rhs.hasChanges) && deepEqualsMessages(lhs.updates, rhs.updates) && deepEqualsMessages(lhs.deletes, rhs.deletes) && deepEqualsMessages(lhs.assetAlbums, rhs.assetAlbums)
}
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
hasher.combine("SyncDelta")
deepHashMessages(value: hasChanges, hasher: &hasher)
deepHashMessages(value: updates, hasher: &hasher)
deepHashMessages(value: deletes, hasher: &hasher)
deepHashMessages(value: assetAlbums, hasher: &hasher)
}
}
@@ -319,9 +403,17 @@ struct HashResult: Hashable {
]
}
static func == (lhs: HashResult, rhs: HashResult) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.hash, rhs.hash)
}
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
hasher.combine("HashResult")
deepHashMessages(value: assetId, hasher: &hasher)
deepHashMessages(value: error, hasher: &hasher)
deepHashMessages(value: hash, hasher: &hasher)
}
}
@@ -352,9 +444,17 @@ struct CloudIdResult: Hashable {
]
}
static func == (lhs: CloudIdResult, rhs: CloudIdResult) -> Bool {
return deepEqualsMessages(lhs.toList(), rhs.toList()) }
if Swift.type(of: lhs) != Swift.type(of: rhs) {
return false
}
return deepEqualsMessages(lhs.assetId, rhs.assetId) && deepEqualsMessages(lhs.error, rhs.error) && deepEqualsMessages(lhs.cloudId, rhs.cloudId)
}
func hash(into hasher: inout Hasher) {
deepHashMessages(value: toList(), hasher: &hasher)
hasher.combine("CloudIdResult")
deepHashMessages(value: assetId, hasher: &hasher)
deepHashMessages(value: error, hasher: &hasher)
deepHashMessages(value: cloudId, hasher: &hasher)
}
}
+1 -1
View File
@@ -171,7 +171,7 @@ class NativeSyncApiImpl: ImmichPlugin, NativeSyncApi, FlutterPlugin {
id: asset.localIdentifier,
name: "",
type: 0,
durationInSeconds: 0,
durationMs: 0,
orientation: 0,
isFavorite: false,
playbackStyle: .unknown
@@ -21,7 +21,7 @@ extension PHAsset {
updatedAt: modificationDate.map { Int64($0.timeIntervalSince1970) },
width: Int64(pixelWidth),
height: Int64(pixelHeight),
durationInSeconds: Int64(duration),
durationMs: Int64(duration * 1000),
orientation: 0,
isFavorite: isFavorite,
adjustmentTime: adjustmentTimestamp,
+19
View File
@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
enum AspectRatioPreset {
free(ratio: null, label: 'Free', icon: Icons.crop_free_rounded),
square(ratio: 1.0, label: '1:1', icon: Icons.crop_square_rounded),
ratio16x9(ratio: 16 / 9, label: '16:9', icon: Icons.crop_16_9_rounded),
ratio3x2(ratio: 3 / 2, label: '3:2', icon: Icons.crop_3_2_rounded),
ratio7x5(ratio: 7 / 5, label: '7:5', icon: Icons.crop_7_5_rounded),
ratio9x16(ratio: 9 / 16, label: '9:16', icon: Icons.crop_16_9_rounded, iconRotated: true),
ratio2x3(ratio: 2 / 3, label: '2:3', icon: Icons.crop_3_2_rounded, iconRotated: true),
ratio5x7(ratio: 5 / 7, label: '5:7', icon: Icons.crop_7_5_rounded, iconRotated: true);
final double? ratio;
final String label;
final IconData icon;
final bool iconRotated;
const AspectRatioPreset({required this.ratio, required this.label, required this.icon, this.iconRotated = false});
}
-7
View File
@@ -1,9 +1,5 @@
import 'dart:io';
const int noDbId = -9223372036854775808; // from Isar
const double downloadCompleted = -1;
const double downloadFailed = -2;
const String kMobileMetadataKey = "mobile-app";
// Number of log entries to retain on app start
@@ -47,9 +43,6 @@ const List<(String, String)> kWidgetNames = [
('com.immich.widget.memory', 'app.alextran.immich.widget.MemoryReceiver'),
];
const double kUploadStatusFailed = -1.0;
const double kUploadStatusCanceled = -2.0;
const int kMinMonthsToEnableScrubberSnap = 12;
const String kImmichAppStoreLink = "https://apps.apple.com/app/immich/id1613945652";
-2
View File
@@ -11,8 +11,6 @@ enum TextSearchType { context, filename, description, ocr }
enum AssetVisibilityEnum { timeline, hidden, archive, locked }
enum SortUserBy { id }
enum ActionSource { timeline, viewer }
enum CleanupStep { selectDate, scan, delete }
@@ -1,3 +0,0 @@
abstract interface class IDatabaseRepository {
Future<T> transaction<T>(Future<T> Function() callback);
}
@@ -8,6 +8,7 @@ enum AlbumUserRole {
// do not change this order!
editor,
viewer,
owner,
}
// Model for an album stored in the server
@@ -1,3 +1,5 @@
import 'package:immich_mobile/domain/models/exif.model.dart';
part 'local_asset.model.dart';
part 'remote_asset.model.dart';
@@ -23,7 +25,7 @@ sealed class BaseAsset {
final DateTime updatedAt;
final int? width;
final int? height;
final int? durationInSeconds;
final int? durationMs;
final bool isFavorite;
final String? livePhotoVideoId;
final bool isEdited;
@@ -36,7 +38,7 @@ sealed class BaseAsset {
required this.updatedAt,
this.width,
this.height,
this.durationInSeconds,
this.durationMs,
this.isFavorite = false,
this.livePhotoVideoId,
required this.isEdited,
@@ -51,15 +53,17 @@ sealed class BaseAsset {
AssetPlaybackStyle get playbackStyle {
if (isVideo) return AssetPlaybackStyle.video;
if (isMotionPhoto) return AssetPlaybackStyle.livePhoto;
if (isImage && durationInSeconds != null && durationInSeconds! > 0) return AssetPlaybackStyle.imageAnimated;
if (isImage && durationMs != null && durationMs! > 0) {
return AssetPlaybackStyle.imageAnimated;
}
if (isImage) return AssetPlaybackStyle.image;
return AssetPlaybackStyle.unknown;
}
Duration get duration {
final durationInSeconds = this.durationInSeconds;
if (durationInSeconds != null) {
return Duration(seconds: durationInSeconds);
final durationMs = this.durationMs;
if (durationMs != null) {
return Duration(milliseconds: durationMs);
}
return const Duration();
}
@@ -69,6 +73,8 @@ sealed class BaseAsset {
bool get isLocalOnly => storage == AssetState.local;
bool get isRemoteOnly => storage == AssetState.remote;
bool get isEditable => false;
// Overridden in subclasses
AssetState get storage;
String? get localId;
@@ -84,7 +90,7 @@ sealed class BaseAsset {
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
durationMs: ${durationMs ?? "<NA>"},
isFavorite: $isFavorite,
isEdited: $isEdited,
}''';
@@ -100,7 +106,7 @@ sealed class BaseAsset {
updatedAt == other.updatedAt &&
width == other.width &&
height == other.height &&
durationInSeconds == other.durationInSeconds &&
durationMs == other.durationMs &&
isFavorite == other.isFavorite &&
isEdited == other.isEdited;
}
@@ -115,7 +121,7 @@ sealed class BaseAsset {
updatedAt.hashCode ^
width.hashCode ^
height.hashCode ^
durationInSeconds.hashCode ^
durationMs.hashCode ^
isFavorite.hashCode ^
isEdited.hashCode;
}
@@ -23,7 +23,7 @@ class LocalAsset extends BaseAsset {
required super.updatedAt,
super.width,
super.height,
super.durationInSeconds,
super.durationMs,
super.isFavorite = false,
super.livePhotoVideoId,
this.orientation = 0,
@@ -58,7 +58,7 @@ class LocalAsset extends BaseAsset {
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
durationMs: ${durationMs ?? "<NA>"},
playbackStyle: $playbackStyle,
remoteId: ${remoteId ?? "<NA>"},
cloudId: ${cloudId ?? "<NA>"},
@@ -108,7 +108,7 @@ class LocalAsset extends BaseAsset {
DateTime? updatedAt,
int? width,
int? height,
int? durationInSeconds,
int? durationMs,
bool? isFavorite,
int? orientation,
AssetPlaybackStyle? playbackStyle,
@@ -128,7 +128,7 @@ class LocalAsset extends BaseAsset {
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
durationMs: durationMs ?? this.durationMs,
isFavorite: isFavorite ?? this.isFavorite,
orientation: orientation ?? this.orientation,
playbackStyle: playbackStyle ?? this.playbackStyle,
@@ -22,7 +22,7 @@ class RemoteAsset extends BaseAsset {
required super.updatedAt,
super.width,
super.height,
super.durationInSeconds,
super.durationMs,
super.isFavorite = false,
this.thumbHash,
this.visibility = AssetVisibility.timeline,
@@ -43,6 +43,9 @@ class RemoteAsset extends BaseAsset {
@override
String get heroTag => '${localId ?? checksum}_$id';
@override
bool get isEditable => isImage && !isMotionPhoto && !isAnimatedImage;
@override
String toString() {
return '''Asset {
@@ -54,7 +57,7 @@ class RemoteAsset extends BaseAsset {
updatedAt: $updatedAt,
width: ${width ?? "<NA>"},
height: ${height ?? "<NA>"},
durationInSeconds: ${durationInSeconds ?? "<NA>"},
durationMs: ${durationMs ?? "<NA>"},
localId: ${localId ?? "<NA>"},
isFavorite: $isFavorite,
thumbHash: ${thumbHash ?? "<NA>"},
@@ -99,7 +102,7 @@ class RemoteAsset extends BaseAsset {
DateTime? updatedAt,
int? width,
int? height,
int? durationInSeconds,
int? durationMs,
bool? isFavorite,
String? thumbHash,
AssetVisibility? visibility,
@@ -118,7 +121,7 @@ class RemoteAsset extends BaseAsset {
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
durationMs: durationMs ?? this.durationMs,
isFavorite: isFavorite ?? this.isFavorite,
thumbHash: thumbHash ?? this.thumbHash,
visibility: visibility ?? this.visibility,
@@ -128,3 +131,81 @@ class RemoteAsset extends BaseAsset {
);
}
}
class RemoteAssetExif extends RemoteAsset {
final ExifInfo exifInfo;
const RemoteAssetExif({
required super.id,
super.localId,
required super.name,
required super.ownerId,
required super.checksum,
required super.type,
required super.createdAt,
required super.updatedAt,
super.width,
super.height,
super.durationMs,
super.isFavorite = false,
super.thumbHash,
super.visibility = AssetVisibility.timeline,
super.livePhotoVideoId,
super.stackId,
super.isEdited = false,
this.exifInfo = const ExifInfo(),
});
@override
bool operator ==(Object other) {
if (other is! RemoteAssetExif) return false;
if (identical(this, other)) return true;
return super == other && exifInfo == other.exifInfo;
}
@override
int get hashCode => super.hashCode ^ exifInfo.hashCode;
@override
RemoteAssetExif copyWith({
String? id,
String? localId,
String? name,
String? ownerId,
String? checksum,
AssetType? type,
DateTime? createdAt,
DateTime? updatedAt,
int? width,
int? height,
int? durationMs,
bool? isFavorite,
String? thumbHash,
AssetVisibility? visibility,
String? livePhotoVideoId,
String? stackId,
bool? isEdited,
ExifInfo? exifInfo,
}) {
return RemoteAssetExif(
id: id ?? this.id,
localId: localId ?? this.localId,
name: name ?? this.name,
ownerId: ownerId ?? this.ownerId,
checksum: checksum ?? this.checksum,
type: type ?? this.type,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
width: width ?? this.width,
height: height ?? this.height,
durationMs: durationMs ?? this.durationMs,
isFavorite: isFavorite ?? this.isFavorite,
thumbHash: thumbHash ?? this.thumbHash,
visibility: visibility ?? this.visibility,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
stackId: stackId ?? this.stackId,
isEdited: isEdited ?? this.isEdited,
exifInfo: exifInfo ?? this.exifInfo, // Use the new parameter
);
}
}
+18 -14
View File
@@ -1,21 +1,25 @@
import "package:openapi/api.dart" as api show AssetEditAction;
import "package:openapi/api.dart" show CropParameters, RotateParameters, MirrorParameters;
enum AssetEditAction { rotate, crop, mirror, other }
extension AssetEditActionExtension on AssetEditAction {
api.AssetEditAction? toDto() {
return switch (this) {
AssetEditAction.rotate => api.AssetEditAction.rotate,
AssetEditAction.crop => api.AssetEditAction.crop,
AssetEditAction.mirror => api.AssetEditAction.mirror,
AssetEditAction.other => null,
};
}
sealed class AssetEdit {
const AssetEdit();
}
class AssetEdit {
final AssetEditAction action;
final Map<String, dynamic> parameters;
class CropEdit extends AssetEdit {
final CropParameters parameters;
const AssetEdit({required this.action, required this.parameters});
const CropEdit(this.parameters);
}
class RotateEdit extends AssetEdit {
final RotateParameters parameters;
const RotateEdit(this.parameters);
}
class MirrorEdit extends AssetEdit {
final MirrorParameters parameters;
const MirrorEdit(this.parameters);
}
@@ -1,34 +0,0 @@
import 'dart:typed_data';
class DeviceAsset {
final String assetId;
final Uint8List hash;
final DateTime modifiedTime;
const DeviceAsset({required this.assetId, required this.hash, required this.modifiedTime});
@override
bool operator ==(covariant DeviceAsset other) {
if (identical(this, other)) return true;
return other.assetId == assetId && other.hash == hash && other.modifiedTime == modifiedTime;
}
@override
int get hashCode {
return assetId.hashCode ^ hash.hashCode ^ modifiedTime.hashCode;
}
@override
String toString() {
return 'DeviceAsset(assetId: $assetId, hash: $hash, modifiedTime: $modifiedTime)';
}
DeviceAsset copyWith({String? assetId, Uint8List? hash, DateTime? modifiedTime}) {
return DeviceAsset(
assetId: assetId ?? this.assetId,
hash: hash ?? this.hash,
modifiedTime: modifiedTime ?? this.modifiedTime,
);
}
}
+15 -1
View File
@@ -7,6 +7,8 @@ class ExifInfo {
final String? timeZone;
final DateTime? dateTimeOriginal;
final int? rating;
final int? width;
final int? height;
// GPS
final double? latitude;
@@ -27,7 +29,7 @@ class ExifInfo {
bool get hasCoordinates => latitude != null && longitude != null && latitude != 0 && longitude != 0;
String get exposureTime {
if (exposureSeconds == null) {
if (exposureSeconds == null || exposureSeconds! <= 0 || exposureSeconds!.isNaN) {
return "";
}
if (exposureSeconds! < 1) {
@@ -48,6 +50,8 @@ class ExifInfo {
this.timeZone,
this.dateTimeOriginal,
this.rating,
this.width,
this.height,
this.isFlipped = false,
this.latitude,
this.longitude,
@@ -74,6 +78,8 @@ class ExifInfo {
other.timeZone == timeZone &&
other.dateTimeOriginal == dateTimeOriginal &&
other.rating == rating &&
other.width == width &&
other.height == height &&
other.latitude == latitude &&
other.longitude == longitude &&
other.city == city &&
@@ -98,6 +104,8 @@ class ExifInfo {
timeZone.hashCode ^
dateTimeOriginal.hashCode ^
rating.hashCode ^
width.hashCode ^
height.hashCode ^
latitude.hashCode ^
longitude.hashCode ^
city.hashCode ^
@@ -123,6 +131,8 @@ isFlipped: $isFlipped,
timeZone: ${timeZone ?? 'NA'},
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
rating: ${rating ?? 'NA'},
width: ${width ?? 'NA'},
height: ${height ?? 'NA'},
latitude: ${latitude ?? 'NA'},
longitude: ${longitude ?? 'NA'},
city: ${city ?? 'NA'},
@@ -146,6 +156,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'},
String? timeZone,
DateTime? dateTimeOriginal,
int? rating,
int? width,
int? height,
double? latitude,
double? longitude,
String? city,
@@ -168,6 +180,8 @@ exposureSeconds: ${exposureSeconds ?? 'NA'},
timeZone: timeZone ?? this.timeZone,
dateTimeOriginal: dateTimeOriginal ?? this.dateTimeOriginal,
rating: rating ?? this.rating,
width: width ?? this.width,
height: height ?? this.height,
isFlipped: isFlipped ?? this.isFlipped,
latitude: latitude ?? this.latitude,
longitude: longitude ?? this.longitude,
@@ -1,12 +1,9 @@
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/exif.model.dart';
import 'package:immich_mobile/extensions/platform_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
typedef _AssetVideoDimension = ({double? width, double? height, bool isFlipped});
class AssetService {
final RemoteAssetRepository _remoteAssetRepository;
final DriftLocalAssetRepository _localAssetRepository;
@@ -58,49 +55,6 @@ class AssetService {
return _remoteAssetRepository.getExif(id);
}
Future<double> getAspectRatio(BaseAsset asset) async {
final dimension = asset is LocalAsset
? await _getLocalAssetDimensions(asset)
: await _getRemoteAssetDimensions(asset as RemoteAsset);
if (dimension.width == null || dimension.height == null || dimension.height == 0) {
return 1.0;
}
return dimension.isFlipped ? dimension.height! / dimension.width! : dimension.width! / dimension.height!;
}
Future<_AssetVideoDimension> _getLocalAssetDimensions(LocalAsset asset) async {
double? width = asset.width?.toDouble();
double? height = asset.height?.toDouble();
int orientation = asset.orientation;
if (width == null || height == null) {
final fetched = await _localAssetRepository.get(asset.id);
width = fetched?.width?.toDouble();
height = fetched?.height?.toDouble();
orientation = fetched?.orientation ?? 0;
}
// On Android, local assets need orientation correction for 90°/270° rotations
// On iOS, the Photos framework pre-corrects dimensions
final isFlipped = CurrentPlatform.isAndroid && (orientation == 90 || orientation == 270);
return (width: width, height: height, isFlipped: isFlipped);
}
Future<_AssetVideoDimension> _getRemoteAssetDimensions(RemoteAsset asset) async {
double? width = asset.width?.toDouble();
double? height = asset.height?.toDouble();
if (width == null || height == null) {
final fetched = await _remoteAssetRepository.get(asset.id);
width = fetched?.width?.toDouble();
height = fetched?.height?.toDouble();
}
return (width: width, height: height, isFlipped: false);
}
Future<List<(String, String)>> getPlaces(String userId) {
return _remoteAssetRepository.getPlaces(userId);
}
@@ -16,19 +16,16 @@ import 'package:immich_mobile/platform/background_worker_lock_api.g.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/background_sync.provider.dart';
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
import 'package:immich_mobile/providers/infrastructure/platform.provider.dart' show nativeSyncApiProvider;
import 'package:immich_mobile/providers/user.provider.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/services/auth.service.dart';
import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/services/foreground_upload.service.dart';
import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/utils/bootstrap.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:immich_mobile/wm_executor.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
class BackgroundWorkerFgService {
@@ -58,7 +55,6 @@ class BackgroundWorkerFgService {
class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
ProviderContainer? _ref;
final Isar _isar;
final Drift _drift;
final DriftLogger _driftLogger;
final BackgroundWorkerBgHostApi _backgroundHostApi;
@@ -67,18 +63,11 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
bool _isCleanedUp = false;
BackgroundWorkerBgService({required Isar isar, required Drift drift, required DriftLogger driftLogger})
: _isar = isar,
_drift = drift,
BackgroundWorkerBgService({required Drift drift, required DriftLogger driftLogger})
: _drift = drift,
_driftLogger = driftLogger,
_backgroundHostApi = BackgroundWorkerBgHostApi() {
_ref = ProviderContainer(
overrides: [
dbProvider.overrideWithValue(isar),
isarProvider.overrideWithValue(isar),
driftProvider.overrideWith(driftOverride(drift)),
],
);
_ref = ProviderContainer(overrides: [driftProvider.overrideWith(driftOverride(drift))]);
BackgroundWorkerFlutterApi.setUp(this);
}
@@ -102,7 +91,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
),
FileDownloader().trackTasksInGroup(kDownloadGroupLivePhoto, markDownloadedComplete: false),
FileDownloader().trackTasks(),
_ref?.read(fileMediaRepositoryProvider).enableBackgroundAccess(),
].nonNulls,
);
@@ -209,9 +197,6 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
backgroundSyncManager?.cancel(),
];
if (_isar.isOpen) {
cleanupFutures.add(_isar.close());
}
await Future.wait(cleanupFutures.nonNulls);
_logger.info("Background worker resources cleaned up");
} catch (error, stack) {
@@ -301,7 +286,6 @@ Future<void> backgroundSyncNativeEntrypoint() async {
WidgetsFlutterBinding.ensureInitialized();
DartPluginRegistrant.ensureInitialized();
final (isar, drift, logDB) = await Bootstrap.initDB();
await Bootstrap.initDomain(isar, drift, logDB, shouldBufferLogs: false, listenStoreUpdates: false);
await BackgroundWorkerBgService(isar: isar, drift: drift, driftLogger: logDB).init();
final (drift, logDB) = await Bootstrap.initDomain(shouldBufferLogs: false, listenStoreUpdates: false);
await BackgroundWorkerBgService(drift: drift, driftLogger: logDB).init();
}
@@ -337,7 +337,7 @@ class LocalSyncService {
a.createdAt.isAtSameMomentAs(b.createdAt) &&
a.width == b.width &&
a.height == b.height &&
a.durationInSeconds == b.durationInSeconds;
a.durationMs == b.durationMs;
}
final firstAdjustment = a.adjustmentTime?.millisecondsSinceEpoch ?? 0;
@@ -346,7 +346,7 @@ class LocalSyncService {
a.createdAt.isAtSameMomentAs(b.createdAt) &&
a.width == b.width &&
a.height == b.height &&
a.durationInSeconds == b.durationInSeconds &&
a.durationMs == b.durationMs &&
a.latitude == b.latitude &&
a.longitude == b.longitude;
}
@@ -432,7 +432,7 @@ extension PlatformToLocalAsset on PlatformAsset {
updatedAt: tryFromSecondsSinceEpoch(updatedAt, isUtc: true) ?? DateTime.timestamp(),
width: width,
height: height,
durationInSeconds: durationInSeconds,
durationMs: durationMs,
isFavorite: isFavorite,
orientation: orientation,
playbackStyle: _toPlaybackStyle(playbackStyle),
+3 -3
View File
@@ -15,7 +15,7 @@ import 'package:logging/logging.dart';
/// via [IStoreRepository]
class LogService {
final LogRepository _logRepository;
final IStoreRepository _storeRepository;
final DriftStoreRepository _storeRepository;
final List<LogMessage> _msgBuffer = [];
@@ -38,7 +38,7 @@ class LogService {
static Future<LogService> init({
required LogRepository logRepository,
required IStoreRepository storeRepository,
required DriftStoreRepository storeRepository,
bool shouldBuffer = true,
}) async {
_instance ??= await create(
@@ -51,7 +51,7 @@ class LogService {
static Future<LogService> create({
required LogRepository logRepository,
required IStoreRepository storeRepository,
required DriftStoreRepository storeRepository,
bool shouldBuffer = true,
}) async {
final instance = LogService._(logRepository, storeRepository, shouldBuffer);
@@ -7,8 +7,8 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/models/albums/album_search.model.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
class RemoteAlbumService {
final DriftRemoteAlbumRepository _repository;
@@ -28,10 +28,6 @@ class RemoteAlbumService {
return _repository.get(albumId);
}
Future<RemoteAlbum?> getByName(String albumName, String ownerId) {
return _repository.getByName(albumName, ownerId);
}
Future<List<RemoteAlbum>> sortAlbums(
List<RemoteAlbum> albums,
AlbumSortMode sortMode, {
@@ -86,8 +82,18 @@ class RemoteAlbumService {
return filtered;
}
Future<RemoteAlbum> createAlbum({required String title, required List<String> assetIds, String? description}) async {
final album = await _albumApiRepository.createDriftAlbum(title, description: description, assetIds: assetIds);
Future<RemoteAlbum> createAlbum({
required String title,
required UserDto owner,
required List<String> assetIds,
String? description,
}) async {
final album = await _albumApiRepository.createDriftAlbum(
title,
owner,
description: description,
assetIds: assetIds,
);
await _repository.create(album, assetIds);
return album;
@@ -101,8 +107,10 @@ class RemoteAlbumService {
bool? isActivityEnabled,
AlbumAssetOrder? order,
}) async {
final owner = await _repository.getOwner(albumId);
final updatedAlbum = await _albumApiRepository.updateAlbum(
albumId,
owner,
name: name,
description: description,
thumbnailAssetId: thumbnailAssetId,
+1 -42
View File
@@ -1,10 +1,9 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/search_result.model.dart';
import 'package:immich_mobile/extensions/asset_extensions.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/repositories/search_api.repository.dart';
import 'package:immich_mobile/models/search/search_filter.model.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart' as api show AssetVisibility;
import 'package:openapi/api.dart' hide AssetVisibility;
class SearchService {
@@ -52,43 +51,3 @@ class SearchService {
return null;
}
}
extension on AssetResponseDto {
RemoteAsset toDto() {
return RemoteAsset(
id: id,
name: originalFileName,
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
ownerId: ownerId,
visibility: switch (visibility) {
api.AssetVisibility.timeline => AssetVisibility.timeline,
api.AssetVisibility.hidden => AssetVisibility.hidden,
api.AssetVisibility.archive => AssetVisibility.archive,
api.AssetVisibility.locked => AssetVisibility.locked,
_ => AssetVisibility.timeline,
},
durationInSeconds: duration.toDuration()?.inSeconds ?? 0,
height: height?.toInt(),
width: width?.toInt(),
isFavorite: isFavorite,
livePhotoVideoId: livePhotoVideoId,
thumbHash: thumbhash,
localId: null,
type: type.toAssetType(),
stackId: stack?.id,
isEdited: isEdited,
);
}
}
extension on AssetTypeEnum {
AssetType toAssetType() => switch (this) {
AssetTypeEnum.IMAGE => AssetType.image,
AssetTypeEnum.VIDEO => AssetType.video,
AssetTypeEnum.AUDIO => AssetType.audio,
AssetTypeEnum.OTHER => AssetType.other,
_ => throw Exception('Unknown AssetType value: $this'),
};
}
@@ -6,13 +6,13 @@ import 'package:immich_mobile/infrastructure/repositories/store.repository.dart'
/// Provides access to a persistent key-value store with an in-memory cache.
/// Listens for repository changes to keep the cache updated.
class StoreService {
final IStoreRepository _storeRepository;
final DriftStoreRepository _storeRepository;
/// In-memory cache. Keys are [StoreKey.id]
final Map<int, Object?> _cache = {};
StreamSubscription<List<StoreDto>>? _storeUpdateSubscription;
StoreService._({required IStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
StoreService._({required DriftStoreRepository isarStoreRepository}) : _storeRepository = isarStoreRepository;
// TODO: Temporary typedef to make minimal changes. Remove this and make the presentation layer access store through a provider
static StoreService? _instance;
@@ -24,12 +24,12 @@ class StoreService {
}
// TODO: Replace the implementation with the one from create after removing the typedef
static Future<StoreService> init({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
static Future<StoreService> init({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async {
_instance ??= await create(storeRepository: storeRepository, listenUpdates: listenUpdates);
return _instance!;
}
static Future<StoreService> create({required IStoreRepository storeRepository, bool listenUpdates = true}) async {
static Future<StoreService> create({required DriftStoreRepository storeRepository, bool listenUpdates = true}) async {
final instance = StoreService._(isarStoreRepository: storeRepository);
await instance.populateCache();
if (listenUpdates) {
@@ -91,8 +91,6 @@ class StoreService {
await _storeRepository.deleteAll();
_cache.clear();
}
bool get isBetaTimelineEnabled => tryGet(StoreKey.betaTimeline) ?? true;
}
class StoreKeyNotFoundException implements Exception {
@@ -1,8 +1,11 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/local_album.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/remote_album.repository.dart';
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
import 'package:immich_mobile/providers/infrastructure/store.provider.dart';
import 'package:immich_mobile/repositories/drift_album_api_repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
@@ -12,6 +15,7 @@ final syncLinkedAlbumServiceProvider = Provider(
ref.watch(localAlbumRepository),
ref.watch(remoteAlbumRepository),
ref.watch(driftAlbumApiRepositoryProvider),
ref.watch(storeServiceProvider),
),
);
@@ -19,8 +23,14 @@ class SyncLinkedAlbumService {
final DriftLocalAlbumRepository _localAlbumRepository;
final DriftRemoteAlbumRepository _remoteAlbumRepository;
final DriftAlbumApiRepository _albumApiRepository;
final StoreService _storeService;
SyncLinkedAlbumService(this._localAlbumRepository, this._remoteAlbumRepository, this._albumApiRepository);
SyncLinkedAlbumService(
this._localAlbumRepository,
this._remoteAlbumRepository,
this._albumApiRepository,
this._storeService,
);
final _log = Logger("SyncLinkedAlbumService");
@@ -103,7 +113,11 @@ class SyncLinkedAlbumService {
/// Creates a new remote album and links it to the local album
Future<void> _createAndLinkNewRemoteAlbum(LocalAlbum localAlbum) async {
dPrint(() => "Creating new remote album for local album: ${localAlbum.name}");
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(localAlbum.name, assetIds: []);
final newRemoteAlbum = await _albumApiRepository.createDriftAlbum(
localAlbum.name,
_storeService.get(StoreKey.currentUser),
assetIds: [],
);
await _remoteAlbumRepository.create(newRemoteAlbum, []);
return _localAlbumRepository.linkRemoteAlbum(localAlbum.id, newRemoteAlbum.id);
}
@@ -225,6 +225,8 @@ class SyncStreamService {
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'partner backfill');
case SyncEntityType.albumV1:
return _syncStreamRepository.updateAlbumsV1(data.cast());
case SyncEntityType.albumV2:
return _syncStreamRepository.updateAlbumsV2(data.cast());
case SyncEntityType.albumDeleteV1:
return _syncStreamRepository.deleteAlbumsV1(data.cast());
case SyncEntityType.albumUserV1:
@@ -34,6 +34,7 @@ enum TimelineOrigin {
search,
deepLink,
albumActivities,
folder,
}
class TimelineFactory {
+3 -19
View File
@@ -4,23 +4,17 @@ import 'dart:typed_data';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
import 'package:immich_mobile/infrastructure/repositories/user.repository.dart';
import 'package:immich_mobile/infrastructure/repositories/user_api.repository.dart';
import 'package:logging/logging.dart';
class UserService {
final Logger _log = Logger("UserService");
final IsarUserRepository _isarUserRepository;
final UserApiRepository _userApiRepository;
final StoreService _storeService;
UserService({
required IsarUserRepository isarUserRepository,
required UserApiRepository userApiRepository,
required StoreService storeService,
}) : _isarUserRepository = isarUserRepository,
_userApiRepository = userApiRepository,
_storeService = storeService;
UserService({required UserApiRepository userApiRepository, required StoreService storeService})
: _userApiRepository = userApiRepository,
_storeService = storeService;
UserDto getMyUser() {
return _storeService.get(StoreKey.currentUser);
@@ -38,7 +32,6 @@ class UserService {
final user = await _userApiRepository.getMyUser();
if (user == null) return null;
await _storeService.put(StoreKey.currentUser, user);
await _isarUserRepository.update(user);
return user;
}
@@ -47,19 +40,10 @@ class UserService {
final path = await _userApiRepository.createProfileImage(name: name, data: image);
final updatedUser = getMyUser();
await _storeService.put(StoreKey.currentUser, updatedUser);
await _isarUserRepository.update(updatedUser);
return path;
} catch (e) {
_log.warning("Failed to upload profile image", e);
return null;
}
}
Future<List<UserDto>> getAll() async {
return await _isarUserRepository.getAll();
}
Future<void> deleteAll() {
return _isarUserRepository.deleteAll();
}
}
@@ -80,12 +80,14 @@ Future<void> _processCloudIdMappingsInBatches(
AssetMetadataBulkUpsertItemDto(
assetId: mapping.remoteAssetId,
key: kMobileMetadataKey,
value: RemoteAssetMobileAppMetadata(
cloudId: mapping.localAsset.cloudId,
createdAt: mapping.localAsset.createdAt.toIso8601String(),
adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(),
latitude: mapping.localAsset.latitude?.toString(),
longitude: mapping.localAsset.longitude?.toString(),
value: Map<String, Object>.from(
RemoteAssetMobileAppMetadata(
cloudId: mapping.localAsset.cloudId,
createdAt: mapping.localAsset.createdAt.toIso8601String(),
adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(),
latitude: mapping.localAsset.latitude?.toString(),
longitude: mapping.localAsset.longitude?.toString(),
).toJson(),
),
),
);
-1
View File
@@ -1 +0,0 @@
This directory contains entity that is stored in the local storage.
-192
View File
@@ -1,192 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/utils/datetime_comparison.dart';
import 'package:isar/isar.dart';
// ignore: implementation_imports
import 'package:isar/src/common/isar_links_common.dart';
import 'package:openapi/api.dart';
part 'album.entity.g.dart';
@Collection(inheritance: false)
class Album {
@protected
Album({
this.remoteId,
this.localId,
required this.name,
required this.createdAt,
required this.modifiedAt,
this.description,
this.startDate,
this.endDate,
this.lastModifiedAssetTimestamp,
required this.shared,
required this.activityEnabled,
this.sortOrder = SortOrder.desc,
});
// fields stored in DB
Id id = Isar.autoIncrement;
@Index(unique: false, replace: false, type: IndexType.hash)
String? remoteId;
@Index(unique: false, replace: false, type: IndexType.hash)
String? localId;
String name;
String? description;
DateTime createdAt;
DateTime modifiedAt;
DateTime? startDate;
DateTime? endDate;
DateTime? lastModifiedAssetTimestamp;
bool shared;
bool activityEnabled;
@enumerated
SortOrder sortOrder;
final IsarLink<User> owner = IsarLink<User>();
final IsarLink<Asset> thumbnail = IsarLink<Asset>();
final IsarLinks<User> sharedUsers = IsarLinks<User>();
final IsarLinks<Asset> assets = IsarLinks<Asset>();
// transient fields
@ignore
bool isAll = false;
@ignore
String? remoteThumbnailAssetId;
@ignore
int remoteAssetCount = 0;
// getters
@ignore
bool get isRemote => remoteId != null;
@ignore
bool get isLocal => localId != null;
@ignore
int get assetCount => assets.length;
@ignore
String? get ownerId => owner.value?.id;
@ignore
String? get ownerName {
// Guard null owner
if (owner.value == null) {
return null;
}
final name = <String>[];
if (owner.value?.name != null) {
name.add(owner.value!.name);
}
return name.join(' ');
}
@ignore
String get eTagKeyAssetCount => "device-album-$localId-asset-count";
// the following getter are needed because Isar links do not make data
// accessible in an object freshly created (not loaded from DB)
@ignore
Iterable<User> get remoteUsers =>
sharedUsers.isEmpty ? (sharedUsers as IsarLinksCommon<User>).addedObjects : sharedUsers;
@ignore
Iterable<Asset> get remoteAssets => assets.isEmpty ? (assets as IsarLinksCommon<Asset>).addedObjects : assets;
@override
bool operator ==(other) {
if (other is! Album) return false;
return id == other.id &&
remoteId == other.remoteId &&
localId == other.localId &&
name == other.name &&
description == other.description &&
createdAt.isAtSameMomentAs(other.createdAt) &&
modifiedAt.isAtSameMomentAs(other.modifiedAt) &&
isAtSameMomentAs(startDate, other.startDate) &&
isAtSameMomentAs(endDate, other.endDate) &&
isAtSameMomentAs(lastModifiedAssetTimestamp, other.lastModifiedAssetTimestamp) &&
shared == other.shared &&
activityEnabled == other.activityEnabled &&
owner.value == other.owner.value &&
thumbnail.value == other.thumbnail.value &&
sharedUsers.length == other.sharedUsers.length &&
assets.length == other.assets.length;
}
@override
@ignore
int get hashCode =>
id.hashCode ^
remoteId.hashCode ^
localId.hashCode ^
name.hashCode ^
createdAt.hashCode ^
modifiedAt.hashCode ^
startDate.hashCode ^
endDate.hashCode ^
description.hashCode ^
lastModifiedAssetTimestamp.hashCode ^
shared.hashCode ^
activityEnabled.hashCode ^
owner.value.hashCode ^
thumbnail.value.hashCode ^
sharedUsers.length.hashCode ^
assets.length.hashCode;
static Future<Album> remote(AlbumResponseDto dto) async {
final Isar db = Isar.getInstance()!;
final Album a = Album(
remoteId: dto.id,
name: dto.albumName,
createdAt: dto.createdAt,
modifiedAt: dto.updatedAt,
description: dto.description,
lastModifiedAssetTimestamp: dto.lastModifiedAssetTimestamp,
shared: dto.shared,
startDate: dto.startDate,
endDate: dto.endDate,
activityEnabled: dto.isActivityEnabled,
);
a.remoteAssetCount = dto.assetCount;
a.owner.value = await db.users.getById(dto.ownerId);
if (dto.order != null) {
a.sortOrder = dto.order == AssetOrder.asc ? SortOrder.asc : SortOrder.desc;
}
if (dto.albumThumbnailAssetId != null) {
a.thumbnail.value = await db.assets.where().remoteIdEqualTo(dto.albumThumbnailAssetId).findFirst();
}
if (dto.albumUsers.isNotEmpty) {
final users = await db.users.getAllById(dto.albumUsers.map((e) => e.user.id).toList(growable: false));
a.sharedUsers.addAll(users.cast());
}
if (dto.assets.isNotEmpty) {
final assets = await db.assets.getAllByRemoteId(dto.assets.map((e) => e.id));
a.assets.addAll(assets);
}
return a;
}
@override
String toString() => 'remoteId: $remoteId name: $name description: $description';
}
extension AssetsHelper on IsarCollection<Album> {
Future<Album> store(Album a) async {
await put(a);
await a.owner.save();
await a.thumbnail.save();
await a.sharedUsers.save();
await a.assets.save();
return a;
}
}
File diff suppressed because it is too large Load Diff
@@ -1,10 +0,0 @@
import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:isar/isar.dart';
part 'android_device_asset.entity.g.dart';
@Collection()
class AndroidDeviceAsset extends DeviceAsset {
AndroidDeviceAsset({required this.id, required super.hash});
Id id;
}
-463
View File
@@ -1,463 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'android_device_asset.entity.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetAndroidDeviceAssetCollection on Isar {
IsarCollection<AndroidDeviceAsset> get androidDeviceAssets =>
this.collection();
}
const AndroidDeviceAssetSchema = CollectionSchema(
name: r'AndroidDeviceAsset',
id: -6758387181232899335,
properties: {
r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList),
},
estimateSize: _androidDeviceAssetEstimateSize,
serialize: _androidDeviceAssetSerialize,
deserialize: _androidDeviceAssetDeserialize,
deserializeProp: _androidDeviceAssetDeserializeProp,
idName: r'id',
indexes: {
r'hash': IndexSchema(
id: -7973251393006690288,
name: r'hash',
unique: false,
replace: false,
properties: [
IndexPropertySchema(
name: r'hash',
type: IndexType.hash,
caseSensitive: false,
),
],
),
},
links: {},
embeddedSchemas: {},
getId: _androidDeviceAssetGetId,
getLinks: _androidDeviceAssetGetLinks,
attach: _androidDeviceAssetAttach,
version: '3.3.0-dev.3',
);
int _androidDeviceAssetEstimateSize(
AndroidDeviceAsset object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.hash.length;
return bytesCount;
}
void _androidDeviceAssetSerialize(
AndroidDeviceAsset object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeByteList(offsets[0], object.hash);
}
AndroidDeviceAsset _androidDeviceAssetDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = AndroidDeviceAsset(
hash: reader.readByteList(offsets[0]) ?? [],
id: id,
);
return object;
}
P _androidDeviceAssetDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readByteList(offset) ?? []) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _androidDeviceAssetGetId(AndroidDeviceAsset object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _androidDeviceAssetGetLinks(
AndroidDeviceAsset object,
) {
return [];
}
void _androidDeviceAssetAttach(
IsarCollection<dynamic> col,
Id id,
AndroidDeviceAsset object,
) {
object.id = id;
}
extension AndroidDeviceAssetQueryWhereSort
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QWhere> {
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension AndroidDeviceAssetQueryWhere
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QWhereClause> {
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
idGreaterThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
idLessThan(Id id, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
hashEqualTo(List<int> hash) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IndexWhereClause.equalTo(indexName: r'hash', value: [hash]),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterWhereClause>
hashNotEqualTo(List<int> hash) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IndexWhereClause.between(
indexName: r'hash',
lower: [],
upper: [hash],
includeUpper: false,
),
)
.addWhereClause(
IndexWhereClause.between(
indexName: r'hash',
lower: [hash],
includeLower: false,
upper: [],
),
);
} else {
return query
.addWhereClause(
IndexWhereClause.between(
indexName: r'hash',
lower: [hash],
includeLower: false,
upper: [],
),
)
.addWhereClause(
IndexWhereClause.between(
indexName: r'hash',
lower: [],
upper: [hash],
includeUpper: false,
),
);
}
});
}
}
extension AndroidDeviceAssetQueryFilter
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
hashElementEqualTo(int value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'hash', value: value),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
hashElementGreaterThan(int value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'hash',
value: value,
),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
hashElementLessThan(int value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'hash',
value: value,
),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
hashElementBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'hash',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
hashLengthEqualTo(int length) {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'hash', length, true, length, true);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
hashIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'hash', 0, true, 0, true);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
hashIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'hash', 0, false, 999999, true);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
hashLengthLessThan(int length, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'hash', 0, true, length, include);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
hashLengthGreaterThan(int length, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'hash', length, include, 999999, true);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
hashLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'hash',
lower,
includeLower,
upper,
includeUpper,
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
idGreaterThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
idLessThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterFilterCondition>
idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension AndroidDeviceAssetQueryObject
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {}
extension AndroidDeviceAssetQueryLinks
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QFilterCondition> {}
extension AndroidDeviceAssetQuerySortBy
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QSortBy> {}
extension AndroidDeviceAssetQuerySortThenBy
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QSortThenBy> {
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterSortBy>
thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QAfterSortBy>
thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
}
extension AndroidDeviceAssetQueryWhereDistinct
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QDistinct> {
QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QDistinct>
distinctByHash() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'hash');
});
}
}
extension AndroidDeviceAssetQueryProperty
on QueryBuilder<AndroidDeviceAsset, AndroidDeviceAsset, QQueryProperty> {
QueryBuilder<AndroidDeviceAsset, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<AndroidDeviceAsset, List<int>, QQueryOperations> hashProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'hash');
});
}
}
-575
View File
@@ -1,575 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:immich_mobile/constants/enums.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart' as entity;
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
import 'package:path/path.dart' as p;
import 'package:photo_manager/photo_manager.dart' show AssetEntity;
part 'asset.entity.g.dart';
/// Asset (online or local)
@Collection(inheritance: false)
class Asset {
Asset.remote(AssetResponseDto remote)
: remoteId = remote.id,
checksum = remote.checksum,
fileCreatedAt = remote.fileCreatedAt,
fileModifiedAt = remote.fileModifiedAt,
updatedAt = remote.updatedAt,
durationInSeconds = remote.duration.toDuration()?.inSeconds ?? 0,
type = remote.type.toAssetType(),
fileName = remote.originalFileName,
height = remote.exifInfo?.exifImageHeight?.toInt(),
width = remote.exifInfo?.exifImageWidth?.toInt(),
livePhotoVideoId = remote.livePhotoVideoId,
ownerId = fastHash(remote.ownerId),
exifInfo = remote.exifInfo == null ? null : ExifDtoConverter.fromDto(remote.exifInfo!),
isFavorite = remote.isFavorite,
isArchived = remote.isArchived,
isTrashed = remote.isTrashed,
isOffline = remote.isOffline,
// workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app
// stack handling to properly handle it
stackPrimaryAssetId = remote.stack?.primaryAssetId == remote.id ? null : remote.stack?.primaryAssetId,
stackCount = remote.stack?.assetCount ?? 0,
stackId = remote.stack?.id,
thumbhash = remote.thumbhash,
visibility = getVisibility(remote.visibility);
Asset({
this.id = Isar.autoIncrement,
required this.checksum,
this.remoteId,
required this.localId,
required this.ownerId,
required this.fileCreatedAt,
required this.fileModifiedAt,
required this.updatedAt,
required this.durationInSeconds,
required this.type,
this.width,
this.height,
required this.fileName,
this.livePhotoVideoId,
this.exifInfo,
this.isFavorite = false,
this.isArchived = false,
this.isTrashed = false,
this.stackId,
this.stackPrimaryAssetId,
this.stackCount = 0,
this.isOffline = false,
this.thumbhash,
this.visibility = AssetVisibilityEnum.timeline,
});
@ignore
AssetEntity? _local;
@ignore
AssetEntity? get local {
if (isLocal && _local == null) {
_local = AssetEntity(
id: localId!,
typeInt: isImage ? 1 : 2,
width: width ?? 0,
height: height ?? 0,
duration: durationInSeconds,
createDateSecond: fileCreatedAt.millisecondsSinceEpoch ~/ 1000,
modifiedDateSecond: fileModifiedAt.millisecondsSinceEpoch ~/ 1000,
title: fileName,
);
}
return _local;
}
set local(AssetEntity? assetEntity) => _local = assetEntity;
@ignore
bool _didUpdateLocal = false;
@ignore
Future<AssetEntity> get localAsync async {
final local = this.local;
if (local == null) {
throw Exception('Asset $fileName has no local data');
}
final updatedLocal = _didUpdateLocal ? local : await local.obtainForNewProperties();
if (updatedLocal == null) {
throw Exception('Could not fetch local data for $fileName');
}
this.local = updatedLocal;
_didUpdateLocal = true;
return updatedLocal;
}
Id id = Isar.autoIncrement;
/// stores the raw SHA1 bytes as a base64 String
/// because Isar cannot sort lists of byte arrays
String checksum;
String? thumbhash;
@Index(unique: false, replace: false, type: IndexType.hash)
String? remoteId;
@Index(unique: false, replace: false, type: IndexType.hash)
String? localId;
@Index(unique: true, replace: false, composite: [CompositeIndex("checksum", type: IndexType.hash)])
int ownerId;
DateTime fileCreatedAt;
DateTime fileModifiedAt;
DateTime updatedAt;
int durationInSeconds;
@Enumerated(EnumType.ordinal)
AssetType type;
short? width;
short? height;
String fileName;
String? livePhotoVideoId;
bool isFavorite;
bool isArchived;
bool isTrashed;
bool isOffline;
@ignore
ExifInfo? exifInfo;
String? stackId;
String? stackPrimaryAssetId;
int stackCount;
@Enumerated(EnumType.ordinal)
AssetVisibilityEnum visibility;
/// Returns null if the asset has no sync access to the exif info
@ignore
double? get aspectRatio {
final orientatedWidth = this.orientatedWidth;
final orientatedHeight = this.orientatedHeight;
if (orientatedWidth != null && orientatedHeight != null && orientatedWidth > 0 && orientatedHeight > 0) {
return orientatedWidth.toDouble() / orientatedHeight.toDouble();
}
return null;
}
/// `true` if this [Asset] is present on the device
@ignore
bool get isLocal => localId != null;
@ignore
bool get isInDb => id != Isar.autoIncrement;
@ignore
String get name => p.withoutExtension(fileName);
/// `true` if this [Asset] is present on the server
@ignore
bool get isRemote => remoteId != null;
@ignore
bool get isImage => type == AssetType.image;
@ignore
bool get isVideo => type == AssetType.video;
@ignore
bool get isMotionPhoto => livePhotoVideoId != null;
@ignore
AssetState get storage {
if (isRemote && isLocal) {
return AssetState.merged;
} else if (isRemote) {
return AssetState.remote;
} else if (isLocal) {
return AssetState.local;
} else {
throw Exception("Asset has illegal state: $this");
}
}
@ignore
Duration get duration => Duration(seconds: durationInSeconds);
// ignore: invalid_annotation_target
@ignore
set byteHash(List<int> hash) => checksum = base64.encode(hash);
/// Returns null if the asset has no sync access to the exif info
@ignore
@pragma('vm:prefer-inline')
bool? get isFlipped {
final exifInfo = this.exifInfo;
if (exifInfo != null) {
return exifInfo.isFlipped;
}
if (_didUpdateLocal && Platform.isAndroid) {
final local = this.local;
if (local == null) {
throw Exception('Asset $fileName has no local data');
}
return local.orientation == 90 || local.orientation == 270;
}
return null;
}
/// Returns null if the asset has no sync access to the exif info
@ignore
@pragma('vm:prefer-inline')
int? get orientatedHeight {
final isFlipped = this.isFlipped;
if (isFlipped == null) {
return null;
}
return isFlipped ? width : height;
}
/// Returns null if the asset has no sync access to the exif info
@ignore
@pragma('vm:prefer-inline')
int? get orientatedWidth {
final isFlipped = this.isFlipped;
if (isFlipped == null) {
return null;
}
return isFlipped ? height : width;
}
@override
bool operator ==(other) {
if (other is! Asset) return false;
if (identical(this, other)) return true;
return id == other.id &&
checksum == other.checksum &&
remoteId == other.remoteId &&
localId == other.localId &&
ownerId == other.ownerId &&
fileCreatedAt.isAtSameMomentAs(other.fileCreatedAt) &&
fileModifiedAt.isAtSameMomentAs(other.fileModifiedAt) &&
updatedAt.isAtSameMomentAs(other.updatedAt) &&
durationInSeconds == other.durationInSeconds &&
type == other.type &&
width == other.width &&
height == other.height &&
fileName == other.fileName &&
livePhotoVideoId == other.livePhotoVideoId &&
isFavorite == other.isFavorite &&
isLocal == other.isLocal &&
isArchived == other.isArchived &&
isTrashed == other.isTrashed &&
stackCount == other.stackCount &&
stackPrimaryAssetId == other.stackPrimaryAssetId &&
stackId == other.stackId;
}
@override
@ignore
int get hashCode =>
id.hashCode ^
checksum.hashCode ^
remoteId.hashCode ^
localId.hashCode ^
ownerId.hashCode ^
fileCreatedAt.hashCode ^
fileModifiedAt.hashCode ^
updatedAt.hashCode ^
durationInSeconds.hashCode ^
type.hashCode ^
width.hashCode ^
height.hashCode ^
fileName.hashCode ^
livePhotoVideoId.hashCode ^
isFavorite.hashCode ^
isLocal.hashCode ^
isArchived.hashCode ^
isTrashed.hashCode ^
stackCount.hashCode ^
stackPrimaryAssetId.hashCode ^
stackId.hashCode;
/// Returns `true` if this [Asset] can updated with values from parameter [a]
bool canUpdate(Asset a) {
assert(isInDb);
assert(checksum == a.checksum);
assert(a.storage != AssetState.merged);
return a.updatedAt.isAfter(updatedAt) ||
a.isRemote && !isRemote ||
a.isLocal && !isLocal ||
width == null && a.width != null ||
height == null && a.height != null ||
livePhotoVideoId == null && a.livePhotoVideoId != null ||
isFavorite != a.isFavorite ||
isArchived != a.isArchived ||
isTrashed != a.isTrashed ||
isOffline != a.isOffline ||
a.exifInfo?.latitude != exifInfo?.latitude ||
a.exifInfo?.longitude != exifInfo?.longitude ||
// no local stack count or different count from remote
a.thumbhash != thumbhash ||
stackId != a.stackId ||
stackCount != a.stackCount ||
stackPrimaryAssetId == null && a.stackPrimaryAssetId != null ||
visibility != a.visibility;
}
/// Returns a new [Asset] with values from this and merged & updated with [a]
Asset updatedCopy(Asset a) {
assert(canUpdate(a));
if (a.updatedAt.isAfter(updatedAt)) {
// take most values from newer asset
// keep vales that can never be set by the asset not in DB
if (a.isRemote) {
return a.copyWith(
id: id,
localId: localId,
width: a.width ?? width,
height: a.height ?? height,
exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo,
);
} else if (isRemote) {
return copyWith(
localId: localId ?? a.localId,
width: width ?? a.width,
height: height ?? a.height,
exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id),
);
} else {
// TODO: Revisit this and remove all bool field assignments
return a.copyWith(
id: id,
remoteId: remoteId,
livePhotoVideoId: livePhotoVideoId,
// workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app
// stack handling to properly handle it
stackId: stackId,
stackPrimaryAssetId: stackPrimaryAssetId == remoteId ? null : stackPrimaryAssetId,
stackCount: stackCount,
isFavorite: isFavorite,
isArchived: isArchived,
isTrashed: isTrashed,
isOffline: isOffline,
);
}
} else {
// fill in potentially missing values, i.e. merge assets
if (a.isRemote) {
// values from remote take precedence
return copyWith(
remoteId: a.remoteId,
width: a.width,
height: a.height,
livePhotoVideoId: a.livePhotoVideoId,
// workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app
// stack handling to properly handle it
stackId: a.stackId,
stackPrimaryAssetId: a.stackPrimaryAssetId == a.remoteId ? null : a.stackPrimaryAssetId,
stackCount: a.stackCount,
// isFavorite + isArchived are not set by device-only assets
isFavorite: a.isFavorite,
isArchived: a.isArchived,
isTrashed: a.isTrashed,
isOffline: a.isOffline,
exifInfo: a.exifInfo?.copyWith(assetId: id) ?? exifInfo,
thumbhash: a.thumbhash,
);
} else {
// add only missing values (and set isLocal to true)
return copyWith(
localId: localId ?? a.localId,
width: width ?? a.width,
height: height ?? a.height,
exifInfo: exifInfo ?? a.exifInfo?.copyWith(assetId: id), // updated to use assetId
);
}
}
}
Asset copyWith({
Id? id,
String? checksum,
String? remoteId,
String? localId,
int? ownerId,
DateTime? fileCreatedAt,
DateTime? fileModifiedAt,
DateTime? updatedAt,
int? durationInSeconds,
AssetType? type,
short? width,
short? height,
String? fileName,
String? livePhotoVideoId,
bool? isFavorite,
bool? isArchived,
bool? isTrashed,
bool? isOffline,
ExifInfo? exifInfo,
String? stackId,
String? stackPrimaryAssetId,
int? stackCount,
String? thumbhash,
AssetVisibilityEnum? visibility,
}) => Asset(
id: id ?? this.id,
checksum: checksum ?? this.checksum,
remoteId: remoteId ?? this.remoteId,
localId: localId ?? this.localId,
ownerId: ownerId ?? this.ownerId,
fileCreatedAt: fileCreatedAt ?? this.fileCreatedAt,
fileModifiedAt: fileModifiedAt ?? this.fileModifiedAt,
updatedAt: updatedAt ?? this.updatedAt,
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
type: type ?? this.type,
width: width ?? this.width,
height: height ?? this.height,
fileName: fileName ?? this.fileName,
livePhotoVideoId: livePhotoVideoId ?? this.livePhotoVideoId,
isFavorite: isFavorite ?? this.isFavorite,
isArchived: isArchived ?? this.isArchived,
isTrashed: isTrashed ?? this.isTrashed,
isOffline: isOffline ?? this.isOffline,
exifInfo: exifInfo ?? this.exifInfo,
stackId: stackId ?? this.stackId,
stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId,
stackCount: stackCount ?? this.stackCount,
thumbhash: thumbhash ?? this.thumbhash,
visibility: visibility ?? this.visibility,
);
Future<void> put(Isar db) async {
await db.assets.put(this);
if (exifInfo != null) {
await db.exifInfos.put(entity.ExifInfo.fromDto(exifInfo!.copyWith(assetId: id)));
}
}
static int compareById(Asset a, Asset b) => a.id.compareTo(b.id);
static int compareByLocalId(Asset a, Asset b) => compareToNullable(a.localId, b.localId);
static int compareByChecksum(Asset a, Asset b) => a.checksum.compareTo(b.checksum);
static int compareByOwnerChecksum(Asset a, Asset b) {
final int ownerIdOrder = a.ownerId.compareTo(b.ownerId);
if (ownerIdOrder != 0) return ownerIdOrder;
return compareByChecksum(a, b);
}
static int compareByOwnerChecksumCreatedModified(Asset a, Asset b) {
final int ownerIdOrder = a.ownerId.compareTo(b.ownerId);
if (ownerIdOrder != 0) return ownerIdOrder;
final int checksumOrder = compareByChecksum(a, b);
if (checksumOrder != 0) return checksumOrder;
final int createdOrder = a.fileCreatedAt.compareTo(b.fileCreatedAt);
if (createdOrder != 0) return createdOrder;
return a.fileModifiedAt.compareTo(b.fileModifiedAt);
}
@override
String toString() {
return """
{
"id": ${id == Isar.autoIncrement ? '"N/A"' : id},
"remoteId": "${remoteId ?? "N/A"}",
"localId": "${localId ?? "N/A"}",
"checksum": "$checksum",
"ownerId": $ownerId,
"livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}",
"stackId": "${stackId ?? "N/A"}",
"stackPrimaryAssetId": "${stackPrimaryAssetId ?? "N/A"}",
"stackCount": "$stackCount",
"fileCreatedAt": "$fileCreatedAt",
"fileModifiedAt": "$fileModifiedAt",
"updatedAt": "$updatedAt",
"durationInSeconds": $durationInSeconds,
"type": "$type",
"fileName": "$fileName",
"isFavorite": $isFavorite,
"isRemote": $isRemote,
"storage": "$storage",
"width": ${width ?? "N/A"},
"height": ${height ?? "N/A"},
"isArchived": $isArchived,
"isTrashed": $isTrashed,
"isOffline": $isOffline,
"visibility": "$visibility",
}""";
}
static getVisibility(AssetVisibility visibility) => switch (visibility) {
AssetVisibility.archive => AssetVisibilityEnum.archive,
AssetVisibility.hidden => AssetVisibilityEnum.hidden,
AssetVisibility.locked => AssetVisibilityEnum.locked,
AssetVisibility.timeline || _ => AssetVisibilityEnum.timeline,
};
}
enum AssetType {
// do not change this order!
other,
image,
video,
audio,
}
extension AssetTypeEnumHelper on AssetTypeEnum {
AssetType toAssetType() => switch (this) {
AssetTypeEnum.IMAGE => AssetType.image,
AssetTypeEnum.VIDEO => AssetType.video,
AssetTypeEnum.AUDIO => AssetType.audio,
AssetTypeEnum.OTHER => AssetType.other,
_ => throw Exception(),
};
}
/// Describes where the information of this asset came from:
/// only from the local device, only from the remote server or merged from both
enum AssetState { local, remote, merged }
extension AssetsHelper on IsarCollection<Asset> {
Future<int> deleteAllByRemoteId(Iterable<String> ids) => ids.isEmpty ? Future.value(0) : remote(ids).deleteAll();
Future<int> deleteAllByLocalId(Iterable<String> ids) => ids.isEmpty ? Future.value(0) : local(ids).deleteAll();
Future<List<Asset>> getAllByRemoteId(Iterable<String> ids) => ids.isEmpty ? Future.value([]) : remote(ids).findAll();
Future<List<Asset>> getAllByLocalId(Iterable<String> ids) => ids.isEmpty ? Future.value([]) : local(ids).findAll();
Future<Asset?> getByRemoteId(String id) => where().remoteIdEqualTo(id).findFirst();
QueryBuilder<Asset, Asset, QAfterWhereClause> remote(Iterable<String> ids) =>
where().anyOf(ids, (q, String e) => q.remoteIdEqualTo(e));
QueryBuilder<Asset, Asset, QAfterWhereClause> local(Iterable<String> ids) {
return where().anyOf(ids, (q, String e) => q.localIdEqualTo(e));
}
}
File diff suppressed because it is too large Load Diff
@@ -1,22 +0,0 @@
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
part 'backup_album.entity.g.dart';
@Collection(inheritance: false)
class BackupAlbum {
String id;
DateTime lastBackup;
@Enumerated(EnumType.ordinal)
BackupSelection selection;
BackupAlbum(this.id, this.lastBackup, this.selection);
Id get isarId => fastHash(id);
BackupAlbum copyWith({String? id, DateTime? lastBackup, BackupSelection? selection}) {
return BackupAlbum(id ?? this.id, lastBackup ?? this.lastBackup, selection ?? this.selection);
}
}
enum BackupSelection { none, select, exclude }
-679
View File
@@ -1,679 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'backup_album.entity.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetBackupAlbumCollection on Isar {
IsarCollection<BackupAlbum> get backupAlbums => this.collection();
}
const BackupAlbumSchema = CollectionSchema(
name: r'BackupAlbum',
id: 8308487201128361847,
properties: {
r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string),
r'lastBackup': PropertySchema(
id: 1,
name: r'lastBackup',
type: IsarType.dateTime,
),
r'selection': PropertySchema(
id: 2,
name: r'selection',
type: IsarType.byte,
enumMap: _BackupAlbumselectionEnumValueMap,
),
},
estimateSize: _backupAlbumEstimateSize,
serialize: _backupAlbumSerialize,
deserialize: _backupAlbumDeserialize,
deserializeProp: _backupAlbumDeserializeProp,
idName: r'isarId',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _backupAlbumGetId,
getLinks: _backupAlbumGetLinks,
attach: _backupAlbumAttach,
version: '3.3.0-dev.3',
);
int _backupAlbumEstimateSize(
BackupAlbum object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.id.length * 3;
return bytesCount;
}
void _backupAlbumSerialize(
BackupAlbum object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.id);
writer.writeDateTime(offsets[1], object.lastBackup);
writer.writeByte(offsets[2], object.selection.index);
}
BackupAlbum _backupAlbumDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = BackupAlbum(
reader.readString(offsets[0]),
reader.readDateTime(offsets[1]),
_BackupAlbumselectionValueEnumMap[reader.readByteOrNull(offsets[2])] ??
BackupSelection.none,
);
return object;
}
P _backupAlbumDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readString(offset)) as P;
case 1:
return (reader.readDateTime(offset)) as P;
case 2:
return (_BackupAlbumselectionValueEnumMap[reader.readByteOrNull(
offset,
)] ??
BackupSelection.none)
as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
const _BackupAlbumselectionEnumValueMap = {
'none': 0,
'select': 1,
'exclude': 2,
};
const _BackupAlbumselectionValueEnumMap = {
0: BackupSelection.none,
1: BackupSelection.select,
2: BackupSelection.exclude,
};
Id _backupAlbumGetId(BackupAlbum object) {
return object.isarId;
}
List<IsarLinkBase<dynamic>> _backupAlbumGetLinks(BackupAlbum object) {
return [];
}
void _backupAlbumAttach(
IsarCollection<dynamic> col,
Id id,
BackupAlbum object,
) {}
extension BackupAlbumQueryWhereSort
on QueryBuilder<BackupAlbum, BackupAlbum, QWhere> {
QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhere> anyIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension BackupAlbumQueryWhere
on QueryBuilder<BackupAlbum, BackupAlbum, QWhereClause> {
QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhereClause> isarIdEqualTo(
Id isarId,
) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(lower: isarId, upper: isarId),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhereClause> isarIdNotEqualTo(
Id isarId,
) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
);
}
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhereClause> isarIdGreaterThan(
Id isarId, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: include),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhereClause> isarIdLessThan(
Id isarId, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: include),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterWhereClause> isarIdBetween(
Id lowerIsarId,
Id upperIsarId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerIsarId,
includeLower: includeLower,
upper: upperIsarId,
includeUpper: includeUpper,
),
);
});
}
}
extension BackupAlbumQueryFilter
on QueryBuilder<BackupAlbum, BackupAlbum, QFilterCondition> {
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idContains(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idMatches(
String pattern, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'id',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: ''),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> idIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'id', value: ''),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> isarIdEqualTo(
Id value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'isarId', value: value),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
isarIdGreaterThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'isarId',
value: value,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> isarIdLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'isarId',
value: value,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition> isarIdBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'isarId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
lastBackupEqualTo(DateTime value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'lastBackup', value: value),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
lastBackupGreaterThan(DateTime value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'lastBackup',
value: value,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
lastBackupLessThan(DateTime value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'lastBackup',
value: value,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
lastBackupBetween(
DateTime lower,
DateTime upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'lastBackup',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
selectionEqualTo(BackupSelection value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'selection', value: value),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
selectionGreaterThan(BackupSelection value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'selection',
value: value,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
selectionLessThan(BackupSelection value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'selection',
value: value,
),
);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterFilterCondition>
selectionBetween(
BackupSelection lower,
BackupSelection upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'selection',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension BackupAlbumQueryObject
on QueryBuilder<BackupAlbum, BackupAlbum, QFilterCondition> {}
extension BackupAlbumQueryLinks
on QueryBuilder<BackupAlbum, BackupAlbum, QFilterCondition> {}
extension BackupAlbumQuerySortBy
on QueryBuilder<BackupAlbum, BackupAlbum, QSortBy> {
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortByLastBackup() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastBackup', Sort.asc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortByLastBackupDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastBackup', Sort.desc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortBySelection() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'selection', Sort.asc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> sortBySelectionDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'selection', Sort.desc);
});
}
}
extension BackupAlbumQuerySortThenBy
on QueryBuilder<BackupAlbum, BackupAlbum, QSortThenBy> {
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.asc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByIsarIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.desc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByLastBackup() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastBackup', Sort.asc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenByLastBackupDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastBackup', Sort.desc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenBySelection() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'selection', Sort.asc);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QAfterSortBy> thenBySelectionDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'selection', Sort.desc);
});
}
}
extension BackupAlbumQueryWhereDistinct
on QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> {
QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> distinctById({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> distinctByLastBackup() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'lastBackup');
});
}
QueryBuilder<BackupAlbum, BackupAlbum, QDistinct> distinctBySelection() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'selection');
});
}
}
extension BackupAlbumQueryProperty
on QueryBuilder<BackupAlbum, BackupAlbum, QQueryProperty> {
QueryBuilder<BackupAlbum, int, QQueryOperations> isarIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isarId');
});
}
QueryBuilder<BackupAlbum, String, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<BackupAlbum, DateTime, QQueryOperations> lastBackupProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'lastBackup');
});
}
QueryBuilder<BackupAlbum, BackupSelection, QQueryOperations>
selectionProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'selection');
});
}
}
@@ -1,8 +0,0 @@
import 'package:isar/isar.dart';
class DeviceAsset {
DeviceAsset({required this.hash});
@Index(unique: false, type: IndexType.hash)
List<byte> hash;
}
@@ -1,11 +0,0 @@
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
part 'duplicated_asset.entity.g.dart';
@Collection(inheritance: false)
class DuplicatedAsset {
String id;
DuplicatedAsset(this.id);
Id get isarId => fastHash(id);
}
-444
View File
@@ -1,444 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'duplicated_asset.entity.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetDuplicatedAssetCollection on Isar {
IsarCollection<DuplicatedAsset> get duplicatedAssets => this.collection();
}
const DuplicatedAssetSchema = CollectionSchema(
name: r'DuplicatedAsset',
id: -2679334728174694496,
properties: {
r'id': PropertySchema(id: 0, name: r'id', type: IsarType.string),
},
estimateSize: _duplicatedAssetEstimateSize,
serialize: _duplicatedAssetSerialize,
deserialize: _duplicatedAssetDeserialize,
deserializeProp: _duplicatedAssetDeserializeProp,
idName: r'isarId',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _duplicatedAssetGetId,
getLinks: _duplicatedAssetGetLinks,
attach: _duplicatedAssetAttach,
version: '3.3.0-dev.3',
);
int _duplicatedAssetEstimateSize(
DuplicatedAsset object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.id.length * 3;
return bytesCount;
}
void _duplicatedAssetSerialize(
DuplicatedAsset object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeString(offsets[0], object.id);
}
DuplicatedAsset _duplicatedAssetDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = DuplicatedAsset(reader.readString(offsets[0]));
return object;
}
P _duplicatedAssetDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readString(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _duplicatedAssetGetId(DuplicatedAsset object) {
return object.isarId;
}
List<IsarLinkBase<dynamic>> _duplicatedAssetGetLinks(DuplicatedAsset object) {
return [];
}
void _duplicatedAssetAttach(
IsarCollection<dynamic> col,
Id id,
DuplicatedAsset object,
) {}
extension DuplicatedAssetQueryWhereSort
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QWhere> {
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhere> anyIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension DuplicatedAssetQueryWhere
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QWhereClause> {
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause>
isarIdEqualTo(Id isarId) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(lower: isarId, upper: isarId),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause>
isarIdNotEqualTo(Id isarId) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
);
}
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause>
isarIdGreaterThan(Id isarId, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: include),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause>
isarIdLessThan(Id isarId, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: include),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterWhereClause>
isarIdBetween(
Id lowerIsarId,
Id upperIsarId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerIsarId,
includeLower: includeLower,
upper: upperIsarId,
includeUpper: includeUpper,
),
);
});
}
}
extension DuplicatedAssetQueryFilter
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QFilterCondition> {
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
idEqualTo(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
idGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
idLessThan(String value, {bool include = false, bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
idBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
idStartsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
idEndsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
idContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
idMatches(String pattern, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'id',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
idIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: ''),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
idIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'id', value: ''),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
isarIdEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'isarId', value: value),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
isarIdGreaterThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'isarId',
value: value,
),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
isarIdLessThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'isarId',
value: value,
),
);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterFilterCondition>
isarIdBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'isarId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension DuplicatedAssetQueryObject
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QFilterCondition> {}
extension DuplicatedAssetQueryLinks
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QFilterCondition> {}
extension DuplicatedAssetQuerySortBy
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QSortBy> {
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> sortById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> sortByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
}
extension DuplicatedAssetQuerySortThenBy
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QSortThenBy> {
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy> thenByIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.asc);
});
}
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QAfterSortBy>
thenByIsarIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.desc);
});
}
}
extension DuplicatedAssetQueryWhereDistinct
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QDistinct> {
QueryBuilder<DuplicatedAsset, DuplicatedAsset, QDistinct> distinctById({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
});
}
}
extension DuplicatedAssetQueryProperty
on QueryBuilder<DuplicatedAsset, DuplicatedAsset, QQueryProperty> {
QueryBuilder<DuplicatedAsset, int, QQueryOperations> isarIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isarId');
});
}
QueryBuilder<DuplicatedAsset, String, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
}
-14
View File
@@ -1,14 +0,0 @@
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
part 'etag.entity.g.dart';
@Collection(inheritance: false)
class ETag {
ETag({required this.id, this.assetCount, this.time});
Id get isarId => fastHash(id);
@Index(unique: true, replace: true, type: IndexType.hash)
String id;
int? assetCount;
DateTime? time;
}
-796
View File
@@ -1,796 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'etag.entity.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetETagCollection on Isar {
IsarCollection<ETag> get eTags => this.collection();
}
const ETagSchema = CollectionSchema(
name: r'ETag',
id: -644290296585643859,
properties: {
r'assetCount': PropertySchema(
id: 0,
name: r'assetCount',
type: IsarType.long,
),
r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string),
r'time': PropertySchema(id: 2, name: r'time', type: IsarType.dateTime),
},
estimateSize: _eTagEstimateSize,
serialize: _eTagSerialize,
deserialize: _eTagDeserialize,
deserializeProp: _eTagDeserializeProp,
idName: r'isarId',
indexes: {
r'id': IndexSchema(
id: -3268401673993471357,
name: r'id',
unique: true,
replace: true,
properties: [
IndexPropertySchema(
name: r'id',
type: IndexType.hash,
caseSensitive: true,
),
],
),
},
links: {},
embeddedSchemas: {},
getId: _eTagGetId,
getLinks: _eTagGetLinks,
attach: _eTagAttach,
version: '3.3.0-dev.3',
);
int _eTagEstimateSize(
ETag object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.id.length * 3;
return bytesCount;
}
void _eTagSerialize(
ETag object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeLong(offsets[0], object.assetCount);
writer.writeString(offsets[1], object.id);
writer.writeDateTime(offsets[2], object.time);
}
ETag _eTagDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = ETag(
assetCount: reader.readLongOrNull(offsets[0]),
id: reader.readString(offsets[1]),
time: reader.readDateTimeOrNull(offsets[2]),
);
return object;
}
P _eTagDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readLongOrNull(offset)) as P;
case 1:
return (reader.readString(offset)) as P;
case 2:
return (reader.readDateTimeOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _eTagGetId(ETag object) {
return object.isarId;
}
List<IsarLinkBase<dynamic>> _eTagGetLinks(ETag object) {
return [];
}
void _eTagAttach(IsarCollection<dynamic> col, Id id, ETag object) {}
extension ETagByIndex on IsarCollection<ETag> {
Future<ETag?> getById(String id) {
return getByIndex(r'id', [id]);
}
ETag? getByIdSync(String id) {
return getByIndexSync(r'id', [id]);
}
Future<bool> deleteById(String id) {
return deleteByIndex(r'id', [id]);
}
bool deleteByIdSync(String id) {
return deleteByIndexSync(r'id', [id]);
}
Future<List<ETag?>> getAllById(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return getAllByIndex(r'id', values);
}
List<ETag?> getAllByIdSync(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return getAllByIndexSync(r'id', values);
}
Future<int> deleteAllById(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return deleteAllByIndex(r'id', values);
}
int deleteAllByIdSync(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return deleteAllByIndexSync(r'id', values);
}
Future<Id> putById(ETag object) {
return putByIndex(r'id', object);
}
Id putByIdSync(ETag object, {bool saveLinks = true}) {
return putByIndexSync(r'id', object, saveLinks: saveLinks);
}
Future<List<Id>> putAllById(List<ETag> objects) {
return putAllByIndex(r'id', objects);
}
List<Id> putAllByIdSync(List<ETag> objects, {bool saveLinks = true}) {
return putAllByIndexSync(r'id', objects, saveLinks: saveLinks);
}
}
extension ETagQueryWhereSort on QueryBuilder<ETag, ETag, QWhere> {
QueryBuilder<ETag, ETag, QAfterWhere> anyIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension ETagQueryWhere on QueryBuilder<ETag, ETag, QWhereClause> {
QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdEqualTo(Id isarId) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(lower: isarId, upper: isarId),
);
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdNotEqualTo(Id isarId) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
);
}
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdGreaterThan(
Id isarId, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: include),
);
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdLessThan(
Id isarId, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: include),
);
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> isarIdBetween(
Id lowerIsarId,
Id upperIsarId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerIsarId,
includeLower: includeLower,
upper: upperIsarId,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> idEqualTo(String id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IndexWhereClause.equalTo(indexName: r'id', value: [id]),
);
});
}
QueryBuilder<ETag, ETag, QAfterWhereClause> idNotEqualTo(String id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IndexWhereClause.between(
indexName: r'id',
lower: [],
upper: [id],
includeUpper: false,
),
)
.addWhereClause(
IndexWhereClause.between(
indexName: r'id',
lower: [id],
includeLower: false,
upper: [],
),
);
} else {
return query
.addWhereClause(
IndexWhereClause.between(
indexName: r'id',
lower: [id],
includeLower: false,
upper: [],
),
)
.addWhereClause(
IndexWhereClause.between(
indexName: r'id',
lower: [],
upper: [id],
includeUpper: false,
),
);
}
});
}
}
extension ETagQueryFilter on QueryBuilder<ETag, ETag, QFilterCondition> {
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'assetCount'),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'assetCount'),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountEqualTo(
int? value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'assetCount', value: value),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountGreaterThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'assetCount',
value: value,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountLessThan(
int? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'assetCount',
value: value,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> assetCountBetween(
int? lower,
int? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'assetCount',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idLessThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idStartsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idContains(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idMatches(
String pattern, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'id',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: ''),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> idIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'id', value: ''),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'isarId', value: value),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'isarId',
value: value,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'isarId',
value: value,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> isarIdBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'isarId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeIsNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNull(property: r'time'),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeIsNotNull() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
const FilterCondition.isNotNull(property: r'time'),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeEqualTo(DateTime? value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'time', value: value),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeGreaterThan(
DateTime? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'time',
value: value,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeLessThan(
DateTime? value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'time',
value: value,
),
);
});
}
QueryBuilder<ETag, ETag, QAfterFilterCondition> timeBetween(
DateTime? lower,
DateTime? upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'time',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension ETagQueryObject on QueryBuilder<ETag, ETag, QFilterCondition> {}
extension ETagQueryLinks on QueryBuilder<ETag, ETag, QFilterCondition> {}
extension ETagQuerySortBy on QueryBuilder<ETag, ETag, QSortBy> {
QueryBuilder<ETag, ETag, QAfterSortBy> sortByAssetCount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'assetCount', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortByAssetCountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'assetCount', Sort.desc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortByTime() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'time', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> sortByTimeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'time', Sort.desc);
});
}
}
extension ETagQuerySortThenBy on QueryBuilder<ETag, ETag, QSortThenBy> {
QueryBuilder<ETag, ETag, QAfterSortBy> thenByAssetCount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'assetCount', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByAssetCountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'assetCount', Sort.desc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByIsarIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.desc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByTime() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'time', Sort.asc);
});
}
QueryBuilder<ETag, ETag, QAfterSortBy> thenByTimeDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'time', Sort.desc);
});
}
}
extension ETagQueryWhereDistinct on QueryBuilder<ETag, ETag, QDistinct> {
QueryBuilder<ETag, ETag, QDistinct> distinctByAssetCount() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'assetCount');
});
}
QueryBuilder<ETag, ETag, QDistinct> distinctById({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
});
}
QueryBuilder<ETag, ETag, QDistinct> distinctByTime() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'time');
});
}
}
extension ETagQueryProperty on QueryBuilder<ETag, ETag, QQueryProperty> {
QueryBuilder<ETag, int, QQueryOperations> isarIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isarId');
});
}
QueryBuilder<ETag, int?, QQueryOperations> assetCountProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'assetCount');
});
}
QueryBuilder<ETag, String, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<ETag, DateTime?, QQueryOperations> timeProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'time');
});
}
}
@@ -1,14 +0,0 @@
import 'package:immich_mobile/entities/device_asset.entity.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
part 'ios_device_asset.entity.g.dart';
@Collection()
class IOSDeviceAsset extends DeviceAsset {
IOSDeviceAsset({required this.id, required super.hash});
@Index(replace: true, unique: true, type: IndexType.hash)
String id;
Id get isarId => fastHash(id);
}
-766
View File
@@ -1,766 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'ios_device_asset.entity.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetIOSDeviceAssetCollection on Isar {
IsarCollection<IOSDeviceAsset> get iOSDeviceAssets => this.collection();
}
const IOSDeviceAssetSchema = CollectionSchema(
name: r'IOSDeviceAsset',
id: -1671546753821948030,
properties: {
r'hash': PropertySchema(id: 0, name: r'hash', type: IsarType.byteList),
r'id': PropertySchema(id: 1, name: r'id', type: IsarType.string),
},
estimateSize: _iOSDeviceAssetEstimateSize,
serialize: _iOSDeviceAssetSerialize,
deserialize: _iOSDeviceAssetDeserialize,
deserializeProp: _iOSDeviceAssetDeserializeProp,
idName: r'isarId',
indexes: {
r'id': IndexSchema(
id: -3268401673993471357,
name: r'id',
unique: true,
replace: true,
properties: [
IndexPropertySchema(
name: r'id',
type: IndexType.hash,
caseSensitive: true,
),
],
),
r'hash': IndexSchema(
id: -7973251393006690288,
name: r'hash',
unique: false,
replace: false,
properties: [
IndexPropertySchema(
name: r'hash',
type: IndexType.hash,
caseSensitive: false,
),
],
),
},
links: {},
embeddedSchemas: {},
getId: _iOSDeviceAssetGetId,
getLinks: _iOSDeviceAssetGetLinks,
attach: _iOSDeviceAssetAttach,
version: '3.3.0-dev.3',
);
int _iOSDeviceAssetEstimateSize(
IOSDeviceAsset object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
bytesCount += 3 + object.hash.length;
bytesCount += 3 + object.id.length * 3;
return bytesCount;
}
void _iOSDeviceAssetSerialize(
IOSDeviceAsset object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeByteList(offsets[0], object.hash);
writer.writeString(offsets[1], object.id);
}
IOSDeviceAsset _iOSDeviceAssetDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = IOSDeviceAsset(
hash: reader.readByteList(offsets[0]) ?? [],
id: reader.readString(offsets[1]),
);
return object;
}
P _iOSDeviceAssetDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readByteList(offset) ?? []) as P;
case 1:
return (reader.readString(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _iOSDeviceAssetGetId(IOSDeviceAsset object) {
return object.isarId;
}
List<IsarLinkBase<dynamic>> _iOSDeviceAssetGetLinks(IOSDeviceAsset object) {
return [];
}
void _iOSDeviceAssetAttach(
IsarCollection<dynamic> col,
Id id,
IOSDeviceAsset object,
) {}
extension IOSDeviceAssetByIndex on IsarCollection<IOSDeviceAsset> {
Future<IOSDeviceAsset?> getById(String id) {
return getByIndex(r'id', [id]);
}
IOSDeviceAsset? getByIdSync(String id) {
return getByIndexSync(r'id', [id]);
}
Future<bool> deleteById(String id) {
return deleteByIndex(r'id', [id]);
}
bool deleteByIdSync(String id) {
return deleteByIndexSync(r'id', [id]);
}
Future<List<IOSDeviceAsset?>> getAllById(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return getAllByIndex(r'id', values);
}
List<IOSDeviceAsset?> getAllByIdSync(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return getAllByIndexSync(r'id', values);
}
Future<int> deleteAllById(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return deleteAllByIndex(r'id', values);
}
int deleteAllByIdSync(List<String> idValues) {
final values = idValues.map((e) => [e]).toList();
return deleteAllByIndexSync(r'id', values);
}
Future<Id> putById(IOSDeviceAsset object) {
return putByIndex(r'id', object);
}
Id putByIdSync(IOSDeviceAsset object, {bool saveLinks = true}) {
return putByIndexSync(r'id', object, saveLinks: saveLinks);
}
Future<List<Id>> putAllById(List<IOSDeviceAsset> objects) {
return putAllByIndex(r'id', objects);
}
List<Id> putAllByIdSync(
List<IOSDeviceAsset> objects, {
bool saveLinks = true,
}) {
return putAllByIndexSync(r'id', objects, saveLinks: saveLinks);
}
}
extension IOSDeviceAssetQueryWhereSort
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QWhere> {
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhere> anyIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension IOSDeviceAssetQueryWhere
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QWhereClause> {
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> isarIdEqualTo(
Id isarId,
) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(lower: isarId, upper: isarId),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
isarIdNotEqualTo(Id isarId) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: false),
);
}
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
isarIdGreaterThan(Id isarId, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: isarId, includeLower: include),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
isarIdLessThan(Id isarId, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: isarId, includeUpper: include),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> isarIdBetween(
Id lowerIsarId,
Id upperIsarId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerIsarId,
includeLower: includeLower,
upper: upperIsarId,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> idEqualTo(
String id,
) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IndexWhereClause.equalTo(indexName: r'id', value: [id]),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> idNotEqualTo(
String id,
) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IndexWhereClause.between(
indexName: r'id',
lower: [],
upper: [id],
includeUpper: false,
),
)
.addWhereClause(
IndexWhereClause.between(
indexName: r'id',
lower: [id],
includeLower: false,
upper: [],
),
);
} else {
return query
.addWhereClause(
IndexWhereClause.between(
indexName: r'id',
lower: [id],
includeLower: false,
upper: [],
),
)
.addWhereClause(
IndexWhereClause.between(
indexName: r'id',
lower: [],
upper: [id],
includeUpper: false,
),
);
}
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause> hashEqualTo(
List<int> hash,
) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IndexWhereClause.equalTo(indexName: r'hash', value: [hash]),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterWhereClause>
hashNotEqualTo(List<int> hash) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IndexWhereClause.between(
indexName: r'hash',
lower: [],
upper: [hash],
includeUpper: false,
),
)
.addWhereClause(
IndexWhereClause.between(
indexName: r'hash',
lower: [hash],
includeLower: false,
upper: [],
),
);
} else {
return query
.addWhereClause(
IndexWhereClause.between(
indexName: r'hash',
lower: [hash],
includeLower: false,
upper: [],
),
)
.addWhereClause(
IndexWhereClause.between(
indexName: r'hash',
lower: [],
upper: [hash],
includeUpper: false,
),
);
}
});
}
}
extension IOSDeviceAssetQueryFilter
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
hashElementEqualTo(int value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'hash', value: value),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
hashElementGreaterThan(int value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'hash',
value: value,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
hashElementLessThan(int value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'hash',
value: value,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
hashElementBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'hash',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
hashLengthEqualTo(int length) {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'hash', length, true, length, true);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
hashIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'hash', 0, true, 0, true);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
hashIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'hash', 0, false, 999999, true);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
hashLengthLessThan(int length, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'hash', 0, true, length, include);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
hashLengthGreaterThan(int length, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(r'hash', length, include, 999999, true);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
hashLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'hash',
lower,
includeLower,
upper,
includeUpper,
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idEqualTo(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
idGreaterThan(
String value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
idLessThan(String value, {bool include = false, bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idBetween(
String lower,
String upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
idStartsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.startsWith(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
idEndsWith(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.endsWith(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
idContains(String value, {bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.contains(
property: r'id',
value: value,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition> idMatches(
String pattern, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.matches(
property: r'id',
wildcard: pattern,
caseSensitive: caseSensitive,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
idIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: ''),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
idIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(property: r'id', value: ''),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
isarIdEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'isarId', value: value),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
isarIdGreaterThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'isarId',
value: value,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
isarIdLessThan(Id value, {bool include = false}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'isarId',
value: value,
),
);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterFilterCondition>
isarIdBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'isarId',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension IOSDeviceAssetQueryObject
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {}
extension IOSDeviceAssetQueryLinks
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QFilterCondition> {}
extension IOSDeviceAssetQuerySortBy
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QSortBy> {
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> sortById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> sortByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
}
extension IOSDeviceAssetQuerySortThenBy
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QSortThenBy> {
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy> thenByIsarId() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.asc);
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QAfterSortBy>
thenByIsarIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'isarId', Sort.desc);
});
}
}
extension IOSDeviceAssetQueryWhereDistinct
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> {
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> distinctByHash() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'hash');
});
}
QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QDistinct> distinctById({
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'id', caseSensitive: caseSensitive);
});
}
}
extension IOSDeviceAssetQueryProperty
on QueryBuilder<IOSDeviceAsset, IOSDeviceAsset, QQueryProperty> {
QueryBuilder<IOSDeviceAsset, int, QQueryOperations> isarIdProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'isarId');
});
}
QueryBuilder<IOSDeviceAsset, List<int>, QQueryOperations> hashProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'hash');
});
}
QueryBuilder<IOSDeviceAsset, String, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
}
-34
View File
@@ -1,38 +1,4 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/services/store.service.dart';
// ignore: non_constant_identifier_names
final Store = StoreService.I;
class SSLClientCertStoreVal {
final Uint8List data;
final String? password;
const SSLClientCertStoreVal(this.data, this.password);
Future<void> save() async {
final b64Str = base64Encode(data);
await Store.put(StoreKey.sslClientCertData, b64Str);
if (password != null) {
await Store.put(StoreKey.sslClientPasswd, password!);
}
}
static SSLClientCertStoreVal? load() {
final b64Str = Store.tryGet<String>(StoreKey.sslClientCertData);
if (b64Str == null) {
return null;
}
final Uint8List certData = base64Decode(b64Str);
final passwd = Store.tryGet<String>(StoreKey.sslClientPasswd);
return SSLClientCertStoreVal(certData, passwd);
}
static Future<void> delete() async {
await Store.delete(StoreKey.sslClientCertData);
await Store.delete(StoreKey.sslClientPasswd);
}
}
+68 -13
View File
@@ -1,17 +1,72 @@
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/utils/timezone.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/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:openapi/api.dart' as api;
extension TZExtension on Asset {
/// Returns the created time of the asset from the exif info (if available) or from
/// the fileCreatedAt field, adjusted to the timezone value from the exif info along with
/// the timezone offset in [Duration]
(DateTime, Duration) getTZAdjustedTimeAndOffset() {
DateTime dt = fileCreatedAt.toLocal();
extension DTOToAsset on api.AssetResponseDto {
RemoteAsset toDto() {
return RemoteAsset(
id: id,
name: originalFileName,
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
height: height?.toInt(),
width: width?.toInt(),
isFavorite: isFavorite,
livePhotoVideoId: livePhotoVideoId,
thumbHash: thumbhash,
localId: null,
type: type.toAssetType(),
stackId: stack?.id,
isEdited: isEdited,
);
}
if (exifInfo?.dateTimeOriginal != null) {
return applyTimezoneOffset(dateTime: exifInfo!.dateTimeOriginal!, timeZone: exifInfo?.timeZone);
}
return (dt, dt.timeZoneOffset);
RemoteAssetExif toDtoWithExif() {
return RemoteAssetExif(
id: id,
name: originalFileName,
checksum: checksum,
createdAt: fileCreatedAt,
updatedAt: updatedAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
height: height?.toInt(),
width: width?.toInt(),
isFavorite: isFavorite,
livePhotoVideoId: livePhotoVideoId,
thumbHash: thumbhash,
localId: null,
type: type.toAssetType(),
stackId: stack?.id,
isEdited: isEdited,
exifInfo: exifInfo != null ? ExifDtoConverter.fromDto(exifInfo!) : const ExifInfo(),
);
}
}
extension on api.AssetVisibility {
AssetVisibility toAssetVisibility() => switch (this) {
api.AssetVisibility.timeline => AssetVisibility.timeline,
api.AssetVisibility.hidden => AssetVisibility.hidden,
api.AssetVisibility.archive => AssetVisibility.archive,
api.AssetVisibility.locked => AssetVisibility.locked,
_ => AssetVisibility.timeline,
};
}
extension on api.AssetTypeEnum {
AssetType toAssetType() => switch (this) {
api.AssetTypeEnum.IMAGE => AssetType.image,
api.AssetTypeEnum.VIDEO => AssetType.video,
api.AssetTypeEnum.AUDIO => AssetType.audio,
api.AssetTypeEnum.OTHER => AssetType.other,
_ => throw Exception('Unknown AssetType value: $this'),
};
}
@@ -1,9 +1,6 @@
import 'dart:typed_data';
import 'package:collection/collection.dart';
import 'package:immich_mobile/domain/models/user.model.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/utils/hash.dart';
extension ListExtension<E> on List<E> {
List<E> uniqueConsecutive({int Function(E a, E b)? compare, void Function(E a, E b)? onDuplicate}) {
@@ -40,31 +37,6 @@ extension IntListExtension on Iterable<int> {
}
}
extension AssetListExtension on Iterable<Asset> {
/// Returns the assets that are already available in the Immich server
Iterable<Asset> remoteOnly({void Function()? errorCallback}) {
final bool onlyRemote = every((e) => e.isRemote);
if (!onlyRemote) {
if (errorCallback != null) errorCallback();
return where((a) => a.isRemote);
}
return this;
}
/// Returns the assets that are owned by the user passed to the [owner] param
/// If [owner] is null, an empty list is returned
Iterable<Asset> ownedOnly(UserDto? owner, {void Function()? errorCallback}) {
if (owner == null) return [];
final isarUserId = fastHash(owner.id);
final bool onlyOwned = every((e) => e.ownerId == isarUserId);
if (!onlyOwned) {
if (errorCallback != null) errorCallback();
return where((a) => a.ownerId == isarUserId);
}
return this;
}
}
extension SortedByProperty<T> on Iterable<T> {
Iterable<T> sortedByField(Comparable Function(T e) key) {
return sorted((a, b) => key(a).compareTo(key(b)));
@@ -0,0 +1,3 @@
extension Let<T extends Object> on T {
R let<R>(R Function(T) transform) => transform(this);
}
+8 -3
View File
@@ -7,11 +7,16 @@ extension StringExtension on String {
}
extension DurationExtension on String {
/// Parses and returns the string of format HH:MM:SS as a duration object else null
/// Parses and returns the string of format HH:MM:SS.ffffff as a duration object else null
Duration? toDuration() {
try {
final parts = split(':').map((e) => double.parse(e).toInt()).toList(growable: false);
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
final parts = split(':');
final hours = double.parse(parts[0]).toInt();
final minutes = double.parse(parts[1]).toInt();
final secondsParts = parts[2].split('.');
final seconds = int.parse(secondsParts[0]);
final milliseconds = secondsParts.length > 1 ? (double.parse('0.${secondsParts[1]}') * 1000).round() : 0;
return Duration(hours: hours, minutes: minutes, seconds: seconds, milliseconds: milliseconds);
} catch (e) {
return null;
}
@@ -1,7 +1,7 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:intl/message_format.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:intl/message_format.dart';
extension StringTranslateExtension on String {
String t({BuildContext? context, Map<String, Object>? args}) {
@@ -1,8 +1,10 @@
import 'package:drift/drift.dart';
import 'package:immich_mobile/domain/models/asset_edit.model.dart';
import 'package:immich_mobile/extensions/object_extensions.dart';
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
import 'package:openapi/api.dart' hide AssetEditAction;
@TableIndex.sql('CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)')
class AssetEditEntity extends Table with DriftDefaultsMixin {
@@ -27,7 +29,12 @@ final JsonTypeConverter2<Map<String, Object?>, Uint8List, Object?> editParameter
);
extension AssetEditEntityDataDomainEx on AssetEditEntityData {
AssetEdit toDto() {
return AssetEdit(action: action, parameters: parameters);
AssetEdit? toDto() {
return switch (action) {
AssetEditAction.crop => CropParameters.fromJson(parameters)?.let(CropEdit.new),
AssetEditAction.rotate => RotateParameters.fromJson(parameters)?.let(RotateEdit.new),
AssetEditAction.mirror => MirrorParameters.fromJson(parameters)?.let(MirrorEdit.new),
AssetEditAction.other => null,
};
}
}
@@ -1,25 +0,0 @@
import 'dart:typed_data';
import 'package:immich_mobile/domain/models/device_asset.model.dart';
import 'package:immich_mobile/utils/hash.dart';
import 'package:isar/isar.dart';
part 'device_asset.entity.g.dart';
@Collection(inheritance: false)
class DeviceAssetEntity {
Id get id => fastHash(assetId);
@Index(replace: true, unique: true, type: IndexType.hash)
final String assetId;
@Index(unique: false, type: IndexType.hash)
final List<byte> hash;
final DateTime modifiedTime;
const DeviceAssetEntity({required this.assetId, required this.hash, required this.modifiedTime});
DeviceAsset toModel() => DeviceAsset(assetId: assetId, hash: Uint8List.fromList(hash), modifiedTime: modifiedTime);
static DeviceAssetEntity fromDto(DeviceAsset dto) =>
DeviceAssetEntity(assetId: dto.assetId, hash: dto.hash, modifiedTime: dto.modifiedTime);
}

Some files were not shown because too many files have changed in this diff Show More