mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
Merge branch 'chore/makefile_split_setup' into chore/pnpm_alt_small
This commit is contained in:
commit
733de34f0a
@ -73,10 +73,8 @@ install_dependencies() {
|
|||||||
log "Installing dependencies"
|
log "Installing dependencies"
|
||||||
(
|
(
|
||||||
cd "${IMMICH_WORKSPACE}" || exit 1
|
cd "${IMMICH_WORKSPACE}" || exit 1
|
||||||
run_cmd make ci-server
|
export CI=1 FROZEN=1 OFFLINE=1
|
||||||
run_cmd make ci-sdk
|
run_cmd make setup-web-dev setup-server-dev
|
||||||
run_cmd make build-sdk
|
|
||||||
run_cmd make ci-web
|
|
||||||
)
|
)
|
||||||
log ""
|
log ""
|
||||||
}
|
}
|
||||||
|
4
.github/.prettierignore
vendored
Normal file
4
.github/.prettierignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Ignore files for PNPM, NPM and YARN
|
||||||
|
pnpm-lock.yaml
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
3
Makefile
3
Makefile
@ -106,4 +106,5 @@ clean:
|
|||||||
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
|
||||||
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
command -v docker >/dev/null 2>&1 && docker compose -f ./e2e/docker-compose.yml rm -v -f || true
|
||||||
|
|
||||||
setup-dev: install-server install-sdk build-sdk install-web
|
setup-server-dev: install-server
|
||||||
|
setup-web-dev: install-sdk build-sdk install-web
|
||||||
|
@ -60,6 +60,7 @@ import { io, type Socket } from 'socket.io-client';
|
|||||||
import { loginDto, signupDto } from 'src/fixtures';
|
import { loginDto, signupDto } from 'src/fixtures';
|
||||||
import { makeRandomImage } from 'src/generators';
|
import { makeRandomImage } from 'src/generators';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
|
export type { Emitter } from '@socket.io/component-emitter';
|
||||||
|
|
||||||
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
|
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
|
||||||
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
|
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
|
||||||
@ -84,10 +85,10 @@ export const immichAdmin = (args: string[]) =>
|
|||||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||||
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
const executeCommand = (command: string, args: string[]) => {
|
const executeCommand = (command: string, args: string[], options?: { cwd?: string }) => {
|
||||||
let _resolve: (value: CommandResponse) => void;
|
let _resolve: (value: CommandResponse) => void;
|
||||||
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
|
||||||
const child = spawn(command, args, { stdio: 'pipe' });
|
const child = spawn(command, args, { stdio: 'pipe', cwd: options?.cwd });
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = '';
|
||||||
let stderr = '';
|
let stderr = '';
|
||||||
|
@ -719,6 +719,7 @@
|
|||||||
"default_locale": "Default Locale",
|
"default_locale": "Default Locale",
|
||||||
"default_locale_description": "Format dates and numbers based on your browser locale",
|
"default_locale_description": "Format dates and numbers based on your browser locale",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
"delete_action_prompt": "{count} deleted permanently",
|
||||||
"delete_album": "Delete album",
|
"delete_album": "Delete album",
|
||||||
"delete_api_key_prompt": "Are you sure you want to delete this API key?",
|
"delete_api_key_prompt": "Are you sure you want to delete this API key?",
|
||||||
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
"delete_dialog_alert": "These items will be permanently deleted from Immich and from your device",
|
||||||
@ -1842,6 +1843,7 @@
|
|||||||
"total": "Total",
|
"total": "Total",
|
||||||
"total_usage": "Total usage",
|
"total_usage": "Total usage",
|
||||||
"trash": "Trash",
|
"trash": "Trash",
|
||||||
|
"trash_action_prompt": "{count} moved to trash",
|
||||||
"trash_all": "Trash All",
|
"trash_all": "Trash All",
|
||||||
"trash_count": "Trash {count, number}",
|
"trash_count": "Trash {count, number}",
|
||||||
"trash_delete_asset": "Trash/Delete Asset",
|
"trash_delete_asset": "Trash/Delete Asset",
|
||||||
@ -1859,9 +1861,11 @@
|
|||||||
"unable_to_change_pin_code": "Unable to change PIN code",
|
"unable_to_change_pin_code": "Unable to change PIN code",
|
||||||
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
"unable_to_setup_pin_code": "Unable to setup PIN code",
|
||||||
"unarchive": "Unarchive",
|
"unarchive": "Unarchive",
|
||||||
|
"unarchive_action_prompt": "{count} removed from Archive",
|
||||||
"unarchived_count": "{count, plural, other {Unarchived #}}",
|
"unarchived_count": "{count, plural, other {Unarchived #}}",
|
||||||
"undo": "Undo",
|
"undo": "Undo",
|
||||||
"unfavorite": "Unfavorite",
|
"unfavorite": "Unfavorite",
|
||||||
|
"unfavorite_action_prompt": "{count} removed from Favorites",
|
||||||
"unhide_person": "Unhide person",
|
"unhide_person": "Unhide person",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"unknown_country": "Unknown Country",
|
"unknown_country": "Unknown Country",
|
||||||
|
98
machine-learning/uv.lock
generated
98
machine-learning/uv.lock
generated
@ -517,16 +517,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.115.13"
|
version = "0.115.14"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "starlette" },
|
{ name = "starlette" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/20/64/ec0788201b5554e2a87c49af26b77a4d132f807a0fa9675257ac92c6aa0e/fastapi-0.115.13.tar.gz", hash = "sha256:55d1d25c2e1e0a0a50aceb1c8705cd932def273c102bff0b1c1da88b3c6eb307", size = 295680, upload-time = "2025-06-17T11:49:45.575Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ca/53/8c38a874844a8b0fa10dd8adf3836ac154082cf88d3f22b544e9ceea0a15/fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739", size = 296263, upload-time = "2025-06-26T15:29:08.21Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/4a/e17764385382062b0edbb35a26b7cf76d71e27e456546277a42ba6545c6e/fastapi-0.115.13-py3-none-any.whl", hash = "sha256:0a0cab59afa7bab22f5eb347f8c9864b681558c278395e94035a741fc10cd865", size = 95315, upload-time = "2025-06-17T11:49:44.106Z" },
|
{ url = "https://files.pythonhosted.org/packages/53/50/b1222562c6d270fea83e9c9075b8e8600b8479150a18e4516a6138b980d1/fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca", size = 95514, upload-time = "2025-06-26T15:29:06.49Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -900,7 +900,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "huggingface-hub"
|
name = "huggingface-hub"
|
||||||
version = "0.33.0"
|
version = "0.33.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "filelock" },
|
{ name = "filelock" },
|
||||||
@ -912,9 +912,9 @@ dependencies = [
|
|||||||
{ name = "tqdm" },
|
{ name = "tqdm" },
|
||||||
{ name = "typing-extensions" },
|
{ name = "typing-extensions" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/91/8a/1362d565fefabaa4185cf3ae842a98dbc5b35146f5694f7080f043a6952f/huggingface_hub-0.33.0.tar.gz", hash = "sha256:aa31f70d29439d00ff7a33837c03f1f9dd83971ce4e29ad664d63ffb17d3bb97", size = 426179, upload-time = "2025-06-11T17:08:07.913Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/fa/42/8a95c5632080ae312c0498744b2b852195e10b05a20b1be11c5141092f4c/huggingface_hub-0.33.2.tar.gz", hash = "sha256:84221defaec8fa09c090390cd68c78b88e3c4c2b7befba68d3dc5aacbc3c2c5f", size = 426637, upload-time = "2025-07-02T06:26:05.156Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/fb/53587a89fbc00799e4179796f51b3ad713c5de6bb680b2becb6d37c94649/huggingface_hub-0.33.0-py3-none-any.whl", hash = "sha256:e8668875b40c68f9929150d99727d39e5ebb8a05a98e4191b908dc7ded9074b3", size = 514799, upload-time = "2025-06-11T17:08:05.757Z" },
|
{ url = "https://files.pythonhosted.org/packages/44/f4/5f3f22e762ad1965f01122b42dae5bf0e009286e2dba601ce1d0dba72424/huggingface_hub-0.33.2-py3-none-any.whl", hash = "sha256:3749498bfa91e8cde2ddc2c1db92c79981f40e66434c20133b39e5928ac9bcc5", size = 515373, upload-time = "2025-07-02T06:26:03.072Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1044,7 +1044,7 @@ requires-dist = [
|
|||||||
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" },
|
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.15.0,<2" },
|
||||||
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.15.0,<2" },
|
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.15.0,<2" },
|
||||||
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.15.0,<2" },
|
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.15.0,<2" },
|
||||||
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" },
|
{ name = "onnxruntime-gpu", marker = "extra == 'cuda'", specifier = ">=1.17.0,<2", index = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" },
|
||||||
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" },
|
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.17.1,<1.19.0" },
|
||||||
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
|
||||||
{ name = "orjson", specifier = ">=3.9.5" },
|
{ name = "orjson", specifier = ">=3.9.5" },
|
||||||
@ -1568,7 +1568,7 @@ wheels = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "onnxruntime-gpu"
|
name = "onnxruntime-gpu"
|
||||||
version = "1.19.2"
|
version = "1.19.2"
|
||||||
source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/" }
|
source = { registry = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "coloredlogs" },
|
{ name = "coloredlogs" },
|
||||||
{ name = "flatbuffers" },
|
{ name = "flatbuffers" },
|
||||||
@ -1936,16 +1936,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic-settings"
|
name = "pydantic-settings"
|
||||||
version = "2.9.1"
|
version = "2.10.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "typing-inspection" },
|
{ name = "typing-inspection" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/67/1d/42628a2c33e93f8e9acbde0d5d735fa0850f3e6a2f8cb1eb6c40b9a732ac/pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268", size = 163234, upload-time = "2025-04-18T16:44:48.265Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/68/85/1ea668bbab3c50071ca613c6ab30047fb36ab0da1b92fa8f17bbc38fd36c/pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee", size = 172583, upload-time = "2025-06-24T13:26:46.841Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/5f/d6d641b490fd3ec2c4c13b4244d68deea3a1b970a97be64f34fb5504ff72/pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef", size = 44356, upload-time = "2025-04-18T16:44:46.617Z" },
|
{ url = "https://files.pythonhosted.org/packages/58/f0/427018098906416f580e3cf1366d3b1abfb408a0652e9f31600c24a1903c/pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796", size = 45235, upload-time = "2025-06-24T13:26:45.485Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2304,27 +2304,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.12.0"
|
version = "0.12.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/6c/3d/d9a195676f25d00dbfcf3cf95fdd4c685c497fcfa7e862a44ac5e4e96480/ruff-0.12.2.tar.gz", hash = "sha256:d7b4f55cd6f325cb7621244f19c873c565a08aff5a4ba9c69aa7355f3f7afd3e", size = 4432239, upload-time = "2025-07-03T16:40:19.566Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/b6/2098d0126d2d3318fd5bec3ad40d06c25d377d95749f7a0c5af17129b3b1/ruff-0.12.2-py3-none-linux_armv6l.whl", hash = "sha256:093ea2b221df1d2b8e7ad92fc6ffdca40a2cb10d8564477a987b44fd4008a7be", size = 10369761, upload-time = "2025-07-03T16:39:38.847Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" },
|
{ url = "https://files.pythonhosted.org/packages/b1/4b/5da0142033dbe155dc598cfb99262d8ee2449d76920ea92c4eeb9547c208/ruff-0.12.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:09e4cf27cc10f96b1708100fa851e0daf21767e9709e1649175355280e0d950e", size = 11155659, upload-time = "2025-07-03T16:39:42.294Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" },
|
{ url = "https://files.pythonhosted.org/packages/3e/21/967b82550a503d7c5c5c127d11c935344b35e8c521f52915fc858fb3e473/ruff-0.12.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8ae64755b22f4ff85e9c52d1f82644abd0b6b6b6deedceb74bd71f35c24044cc", size = 10537769, upload-time = "2025-07-03T16:39:44.75Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/91/00cff7102e2ec71a4890fb7ba1803f2cdb122d82787c7d7cf8041fe8cbc1/ruff-0.12.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eb3a6b2db4d6e2c77e682f0b988d4d61aff06860158fdb413118ca133d57922", size = 10717602, upload-time = "2025-07-03T16:39:47.652Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" },
|
{ url = "https://files.pythonhosted.org/packages/9b/eb/928814daec4e1ba9115858adcda44a637fb9010618721937491e4e2283b8/ruff-0.12.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73448de992d05517170fc37169cbca857dfeaeaa8c2b9be494d7bcb0d36c8f4b", size = 10198772, upload-time = "2025-07-03T16:39:49.641Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" },
|
{ url = "https://files.pythonhosted.org/packages/50/fa/f15089bc20c40f4f72334f9145dde55ab2b680e51afb3b55422effbf2fb6/ruff-0.12.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3b8b94317cbc2ae4a2771af641739f933934b03555e51515e6e021c64441532d", size = 11845173, upload-time = "2025-07-03T16:39:52.069Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" },
|
{ url = "https://files.pythonhosted.org/packages/43/9f/1f6f98f39f2b9302acc161a4a2187b1e3a97634fe918a8e731e591841cf4/ruff-0.12.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:45fc42c3bf1d30d2008023a0a9a0cfb06bf9835b147f11fe0679f21ae86d34b1", size = 12553002, upload-time = "2025-07-03T16:39:54.551Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/70/08991ac46e38ddd231c8f4fd05ef189b1b94be8883e8c0c146a025c20a19/ruff-0.12.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce48f675c394c37e958bf229fb5c1e843e20945a6d962cf3ea20b7a107dcd9f4", size = 12171330, upload-time = "2025-07-03T16:39:57.55Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" },
|
{ url = "https://files.pythonhosted.org/packages/88/a9/5a55266fec474acfd0a1c73285f19dd22461d95a538f29bba02edd07a5d9/ruff-0.12.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:793d8859445ea47591272021a81391350205a4af65a9392401f418a95dfb75c9", size = 11774717, upload-time = "2025-07-03T16:39:59.78Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" },
|
{ url = "https://files.pythonhosted.org/packages/87/e5/0c270e458fc73c46c0d0f7cf970bb14786e5fdb88c87b5e423a4bd65232b/ruff-0.12.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6932323db80484dda89153da3d8e58164d01d6da86857c79f1961934354992da", size = 11646659, upload-time = "2025-07-03T16:40:01.934Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/b6/45ab96070c9752af37f0be364d849ed70e9ccede07675b0ec4e3ef76b63b/ruff-0.12.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6aa7e623a3a11538108f61e859ebf016c4f14a7e6e4eba1980190cacb57714ce", size = 10604012, upload-time = "2025-07-03T16:40:04.363Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" },
|
{ url = "https://files.pythonhosted.org/packages/86/91/26a6e6a424eb147cc7627eebae095cfa0b4b337a7c1c413c447c9ebb72fd/ruff-0.12.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2a4a20aeed74671b2def096bdf2eac610c7d8ffcbf4fb0e627c06947a1d7078d", size = 10176799, upload-time = "2025-07-03T16:40:06.514Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/0c/9f344583465a61c8918a7cda604226e77b2c548daf8ef7c2bfccf2b37200/ruff-0.12.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:71a4c550195612f486c9d1f2b045a600aeba851b298c667807ae933478fcef04", size = 11241507, upload-time = "2025-07-03T16:40:08.708Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" },
|
{ url = "https://files.pythonhosted.org/packages/1c/b7/99c34ded8fb5f86c0280278fa89a0066c3760edc326e935ce0b1550d315d/ruff-0.12.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:4987b8f4ceadf597c927beee65a5eaf994c6e2b631df963f86d8ad1bdea99342", size = 11717609, upload-time = "2025-07-03T16:40:10.836Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" },
|
{ url = "https://files.pythonhosted.org/packages/51/de/8589fa724590faa057e5a6d171e7f2f6cffe3287406ef40e49c682c07d89/ruff-0.12.2-py3-none-win32.whl", hash = "sha256:369ffb69b70cd55b6c3fc453b9492d98aed98062db9fec828cdfd069555f5f1a", size = 10523823, upload-time = "2025-07-03T16:40:13.203Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" },
|
{ url = "https://files.pythonhosted.org/packages/94/47/8abf129102ae4c90cba0c2199a1a9b0fa896f6f806238d6f8c14448cc748/ruff-0.12.2-py3-none-win_amd64.whl", hash = "sha256:dca8a3b6d6dc9810ed8f328d406516bf4d660c00caeaef36eb831cf4871b0639", size = 11629831, upload-time = "2025-07-03T16:40:15.478Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" },
|
{ url = "https://files.pythonhosted.org/packages/e2/1f/72d2946e3cc7456bb837e88000eb3437e55f80db339c840c04015a11115d/ruff-0.12.2-py3-none-win_arm64.whl", hash = "sha256:48d6c6bfb4761df68bc05ae630e24f506755e702d4fb08f08460be778c7ccb12", size = 10735334, upload-time = "2025-07-03T16:40:17.677Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2504,27 +2504,27 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokenizers"
|
name = "tokenizers"
|
||||||
version = "0.21.1"
|
version = "0.21.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "huggingface-hub" },
|
{ name = "huggingface-hub" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/92/76/5ac0c97f1117b91b7eb7323dcd61af80d72f790b4df71249a7850c195f30/tokenizers-0.21.1.tar.gz", hash = "sha256:a1bb04dc5b448985f86ecd4b05407f5a8d97cb2c0532199b2a302a604a0165ab", size = 343256, upload-time = "2025-03-13T10:51:18.189Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ab/2d/b0fce2b8201635f60e8c95990080f58461cc9ca3d5026de2e900f38a7f21/tokenizers-0.21.2.tar.gz", hash = "sha256:fdc7cffde3e2113ba0e6cc7318c40e3438a4d74bbc62bf04bcc63bdfb082ac77", size = 351545, upload-time = "2025-06-24T10:24:52.449Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/1f/328aee25f9115bf04262e8b4e5a2050b7b7cf44b59c74e982db7270c7f30/tokenizers-0.21.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:e78e413e9e668ad790a29456e677d9d3aa50a9ad311a40905d6861ba7692cf41", size = 2780767, upload-time = "2025-03-13T10:51:09.459Z" },
|
{ url = "https://files.pythonhosted.org/packages/1d/cc/2936e2d45ceb130a21d929743f1e9897514691bec123203e10837972296f/tokenizers-0.21.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:342b5dfb75009f2255ab8dec0041287260fed5ce00c323eb6bab639066fef8ec", size = 2875206, upload-time = "2025-06-24T10:24:42.755Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/1a/4526797f3719b0287853f12c5ad563a9be09d446c44ac784cdd7c50f76ab/tokenizers-0.21.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:cd51cd0a91ecc801633829fcd1fda9cf8682ed3477c6243b9a095539de4aecf3", size = 2650555, upload-time = "2025-03-13T10:51:07.692Z" },
|
{ url = "https://files.pythonhosted.org/packages/6c/e6/33f41f2cc7861faeba8988e7a77601407bf1d9d28fc79c5903f8f77df587/tokenizers-0.21.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:126df3205d6f3a93fea80c7a8a266a78c1bd8dd2fe043386bafdd7736a23e45f", size = 2732655, upload-time = "2025-06-24T10:24:41.56Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/4d/7a/a209b29f971a9fdc1da86f917fe4524564924db50d13f0724feed37b2a4d/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28da6b72d4fb14ee200a1bd386ff74ade8992d7f725f2bde2c495a9a98cf4d9f", size = 2937541, upload-time = "2025-03-13T10:50:56.679Z" },
|
{ url = "https://files.pythonhosted.org/packages/33/2b/1791eb329c07122a75b01035b1a3aa22ad139f3ce0ece1b059b506d9d9de/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a32cd81be21168bd0d6a0f0962d60177c447a1aa1b1e48fa6ec9fc728ee0b12", size = 3019202, upload-time = "2025-06-24T10:24:31.791Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/1e/b788b50ffc6191e0b1fc2b0d49df8cff16fe415302e5ceb89f619d12c5bc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34d8cfde551c9916cb92014e040806122295a6800914bab5865deb85623931cf", size = 2819058, upload-time = "2025-03-13T10:50:59.525Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/15/fd2d8104faa9f86ac68748e6f7ece0b5eb7983c7efc3a2c197cb98c99030/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8bd8999538c405133c2ab999b83b17c08b7fc1b48c1ada2469964605a709ef91", size = 2934539, upload-time = "2025-06-24T10:24:34.567Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/aa/3626dfa09a0ecc5b57a8c58eeaeb7dd7ca9a37ad9dd681edab5acd55764c/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aaa852d23e125b73d283c98f007e06d4595732104b65402f46e8ef24b588d9f8", size = 3133278, upload-time = "2025-03-13T10:51:04.678Z" },
|
{ url = "https://files.pythonhosted.org/packages/a5/2e/53e8fd053e1f3ffbe579ca5f9546f35ac67cf0039ed357ad7ec57f5f5af0/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5e9944e61239b083a41cf8fc42802f855e1dca0f499196df37a8ce219abac6eb", size = 3248665, upload-time = "2025-06-24T10:24:39.024Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/4d/8fbc203838b3d26269f944a89459d94c858f5b3f9a9b6ee9728cdcf69161/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a21a15d5c8e603331b8a59548bbe113564136dc0f5ad8306dd5033459a226da0", size = 3144253, upload-time = "2025-03-13T10:51:01.261Z" },
|
{ url = "https://files.pythonhosted.org/packages/00/15/79713359f4037aa8f4d1f06ffca35312ac83629da062670e8830917e2153/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:514cd43045c5d546f01142ff9c79a96ea69e4b5cda09e3027708cb2e6d5762ab", size = 3451305, upload-time = "2025-06-24T10:24:36.133Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d8/1b/2bd062adeb7c7511b847b32e356024980c0ffcf35f28947792c2d8ad2288/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2fdbd4c067c60a0ac7eca14b6bd18a5bebace54eb757c706b47ea93204f7a37c", size = 3398225, upload-time = "2025-03-13T10:51:03.243Z" },
|
{ url = "https://files.pythonhosted.org/packages/38/5f/959f3a8756fc9396aeb704292777b84f02a5c6f25c3fc3ba7530db5feb2c/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1b9405822527ec1e0f7d8d2fdb287a5730c3a6518189c968254a8441b21faae", size = 3214757, upload-time = "2025-06-24T10:24:37.784Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/63/38be071b0c8e06840bc6046991636bcb30c27f6bb1e670f4f4bc87cf49cc/tokenizers-0.21.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd9a0061e403546f7377df940e866c3e678d7d4e9643d0461ea442b4f89e61a", size = 3038874, upload-time = "2025-03-13T10:51:06.235Z" },
|
{ url = "https://files.pythonhosted.org/packages/c5/74/f41a432a0733f61f3d21b288de6dfa78f7acff309c6f0f323b2833e9189f/tokenizers-0.21.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed9a4d51c395103ad24f8e7eb976811c57fbec2af9f133df471afcd922e5020", size = 3121887, upload-time = "2025-06-24T10:24:40.293Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/83/afa94193c09246417c23a3c75a8a0a96bf44ab5630a3015538d0c316dd4b/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:db9484aeb2e200c43b915a1a0150ea885e35f357a5a8fabf7373af333dcc8dbf", size = 9014448, upload-time = "2025-03-13T10:51:10.927Z" },
|
{ url = "https://files.pythonhosted.org/packages/3c/6a/bc220a11a17e5d07b0dfb3b5c628621d4dcc084bccd27cfaead659963016/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2c41862df3d873665ec78b6be36fcc30a26e3d4902e9dd8608ed61d49a48bc19", size = 9091965, upload-time = "2025-06-24T10:24:44.431Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/b3/0e1a37d4f84c0f014d43701c11eb8072704f6efe8d8fc2dcdb79c47d76de/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed248ab5279e601a30a4d67bdb897ecbe955a50f1e7bb62bd99f07dd11c2f5b6", size = 8937877, upload-time = "2025-03-13T10:51:12.688Z" },
|
{ url = "https://files.pythonhosted.org/packages/6c/bd/ac386d79c4ef20dc6f39c4706640c24823dca7ebb6f703bfe6b5f0292d88/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:ed21dc7e624e4220e21758b2e62893be7101453525e3d23264081c9ef9a6d00d", size = 9053372, upload-time = "2025-06-24T10:24:46.455Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/33/ff08f50e6d615eb180a4a328c65907feb6ded0b8f990ec923969759dc379/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:9ac78b12e541d4ce67b4dfd970e44c060a2147b9b2a21f509566d556a509c67d", size = 9186645, upload-time = "2025-03-13T10:51:14.723Z" },
|
{ url = "https://files.pythonhosted.org/packages/63/7b/5440bf203b2a5358f074408f7f9c42884849cd9972879e10ee6b7a8c3b3d/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:0e73770507e65a0e0e2a1affd6b03c36e3bc4377bd10c9ccf51a82c77c0fe365", size = 9298632, upload-time = "2025-06-24T10:24:48.446Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/aa/8ae85f69a9f6012c6f8011c6f4aa1c96154c816e9eea2e1b758601157833/tokenizers-0.21.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e5a69c1a4496b81a5ee5d2c1f3f7fbdf95e90a0196101b0ee89ed9956b8a168f", size = 9384380, upload-time = "2025-03-13T10:51:16.526Z" },
|
{ url = "https://files.pythonhosted.org/packages/a4/d2/faa1acac3f96a7427866e94ed4289949b2524f0c1878512516567d80563c/tokenizers-0.21.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:106746e8aa9014a12109e58d540ad5465b4c183768ea96c03cbc24c44d329958", size = 9470074, upload-time = "2025-06-24T10:24:50.378Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/5b/a5d98c89f747455e8b7a9504910c865d5e51da55e825a7ae641fb5ff0a58/tokenizers-0.21.1-cp39-abi3-win32.whl", hash = "sha256:1039a3a5734944e09de1d48761ade94e00d0fa760c0e0551151d4dd851ba63e3", size = 2239506, upload-time = "2025-03-13T10:51:20.643Z" },
|
{ url = "https://files.pythonhosted.org/packages/d8/a5/896e1ef0707212745ae9f37e84c7d50269411aef2e9ccd0de63623feecdf/tokenizers-0.21.2-cp39-abi3-win32.whl", hash = "sha256:cabda5a6d15d620b6dfe711e1af52205266d05b379ea85a8a301b3593c60e962", size = 2330115, upload-time = "2025-06-24T10:24:55.069Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/b6/072a8e053ae600dcc2ac0da81a23548e3b523301a442a6ca900e92ac35be/tokenizers-0.21.1-cp39-abi3-win_amd64.whl", hash = "sha256:0f0dcbcc9f6e13e675a66d7a5f2f225a736745ce484c1a4e07476a89ccdad382", size = 2435481, upload-time = "2025-03-13T10:51:19.243Z" },
|
{ url = "https://files.pythonhosted.org/packages/13/c3/cc2755ee10be859c4338c962a35b9a663788c0c0b50c0bdd8078fb6870cf/tokenizers-0.21.2-cp39-abi3-win_amd64.whl", hash = "sha256:58747bb898acdb1007f37a7bbe614346e98dc28708ffb66a3fd50ce169ac6c98", size = 2509918, upload-time = "2025-06-24T10:24:53.71Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2628,16 +2628,16 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.34.3"
|
version = "0.35.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "h11" },
|
{ name = "h11" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
|
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.optional-dependencies]
|
[package.optional-dependencies]
|
||||||
|
@ -5,6 +5,7 @@ import android.content.Context
|
|||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.provider.MediaStore
|
import android.provider.MediaStore
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
@ -152,7 +153,8 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val name = cursor.getString(bucketNameColumn)
|
// MediaStore might return null for bucket name (commonly for the Root Directory), so default to "Internal Storage"
|
||||||
|
val name = cursor.getStringOrNull(bucketNameColumn) ?: "Internal Storage"
|
||||||
val updatedAt = cursor.getLong(dateModified)
|
val updatedAt = cursor.getLong(dateModified)
|
||||||
albums.add(PlatformAlbum(id, name, updatedAt, false, 0))
|
albums.add(PlatformAlbum(id, name, updatedAt, false, 0))
|
||||||
albumsCount[id] = 1
|
albumsCount[id] = 1
|
||||||
|
1332
mobile/drift_schemas/main/drift_schema_v1.json
generated
1332
mobile/drift_schemas/main/drift_schema_v1.json
generated
File diff suppressed because one or more lines are too long
@ -1,5 +1,5 @@
|
|||||||
part 'remote_asset.model.dart';
|
|
||||||
part 'local_asset.model.dart';
|
part 'local_asset.model.dart';
|
||||||
|
part 'remote_asset.model.dart';
|
||||||
|
|
||||||
enum AssetType {
|
enum AssetType {
|
||||||
// do not change this order!
|
// do not change this order!
|
||||||
@ -40,7 +40,24 @@ sealed class BaseAsset {
|
|||||||
|
|
||||||
bool get isImage => type == AssetType.image;
|
bool get isImage => type == AssetType.image;
|
||||||
bool get isVideo => type == AssetType.video;
|
bool get isVideo => type == AssetType.video;
|
||||||
|
|
||||||
|
double? get aspectRatio {
|
||||||
|
if (width != null && height != null && height! > 0) {
|
||||||
|
return width! / height!;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get hasRemote =>
|
||||||
|
storage == AssetState.remote || storage == AssetState.merged;
|
||||||
|
bool get hasLocal =>
|
||||||
|
storage == AssetState.local || storage == AssetState.merged;
|
||||||
|
bool get isLocalOnly => storage == AssetState.local;
|
||||||
|
bool get isRemoteOnly => storage == AssetState.remote;
|
||||||
|
|
||||||
|
// Overridden in subclasses
|
||||||
AssetState get storage;
|
AssetState get storage;
|
||||||
|
String get heroTag;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
|
@ -22,6 +22,9 @@ class LocalAsset extends BaseAsset {
|
|||||||
AssetState get storage =>
|
AssetState get storage =>
|
||||||
remoteId == null ? AssetState.local : AssetState.merged;
|
remoteId == null ? AssetState.local : AssetState.merged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get heroTag => '${id}_${remoteId ?? checksum}';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''LocalAsset {
|
return '''LocalAsset {
|
||||||
|
@ -36,6 +36,9 @@ class RemoteAsset extends BaseAsset {
|
|||||||
AssetState get storage =>
|
AssetState get storage =>
|
||||||
localId == null ? AssetState.remote : AssetState.merged;
|
localId == null ? AssetState.remote : AssetState.merged;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get heroTag => '${localId ?? checksum}_$id';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Asset {
|
return '''Asset {
|
||||||
@ -75,4 +78,38 @@ class RemoteAsset extends BaseAsset {
|
|||||||
localId.hashCode ^
|
localId.hashCode ^
|
||||||
thumbHash.hashCode ^
|
thumbHash.hashCode ^
|
||||||
visibility.hashCode;
|
visibility.hashCode;
|
||||||
|
|
||||||
|
RemoteAsset copyWith({
|
||||||
|
String? id,
|
||||||
|
String? localId,
|
||||||
|
String? name,
|
||||||
|
String? ownerId,
|
||||||
|
String? checksum,
|
||||||
|
AssetType? type,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
int? width,
|
||||||
|
int? height,
|
||||||
|
int? durationInSeconds,
|
||||||
|
bool? isFavorite,
|
||||||
|
String? thumbHash,
|
||||||
|
AssetVisibility? visibility,
|
||||||
|
}) {
|
||||||
|
return RemoteAsset(
|
||||||
|
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,
|
||||||
|
durationInSeconds: durationInSeconds ?? this.durationInSeconds,
|
||||||
|
isFavorite: isFavorite ?? this.isFavorite,
|
||||||
|
thumbHash: thumbHash ?? this.thumbHash,
|
||||||
|
visibility: visibility ?? this.visibility,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ class ExifInfo {
|
|||||||
final int? fileSize;
|
final int? fileSize;
|
||||||
final String? description;
|
final String? description;
|
||||||
final bool isFlipped;
|
final bool isFlipped;
|
||||||
|
final double? width;
|
||||||
|
final double? height;
|
||||||
final String? orientation;
|
final String? orientation;
|
||||||
final String? timeZone;
|
final String? timeZone;
|
||||||
final DateTime? dateTimeOriginal;
|
final DateTime? dateTimeOriginal;
|
||||||
@ -45,6 +47,8 @@ class ExifInfo {
|
|||||||
this.fileSize,
|
this.fileSize,
|
||||||
this.description,
|
this.description,
|
||||||
this.orientation,
|
this.orientation,
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
this.timeZone,
|
this.timeZone,
|
||||||
this.dateTimeOriginal,
|
this.dateTimeOriginal,
|
||||||
this.isFlipped = false,
|
this.isFlipped = false,
|
||||||
@ -68,6 +72,9 @@ class ExifInfo {
|
|||||||
|
|
||||||
return other.fileSize == fileSize &&
|
return other.fileSize == fileSize &&
|
||||||
other.description == description &&
|
other.description == description &&
|
||||||
|
other.isFlipped == isFlipped &&
|
||||||
|
other.width == width &&
|
||||||
|
other.height == height &&
|
||||||
other.orientation == orientation &&
|
other.orientation == orientation &&
|
||||||
other.timeZone == timeZone &&
|
other.timeZone == timeZone &&
|
||||||
other.dateTimeOriginal == dateTimeOriginal &&
|
other.dateTimeOriginal == dateTimeOriginal &&
|
||||||
@ -91,6 +98,9 @@ class ExifInfo {
|
|||||||
return fileSize.hashCode ^
|
return fileSize.hashCode ^
|
||||||
description.hashCode ^
|
description.hashCode ^
|
||||||
orientation.hashCode ^
|
orientation.hashCode ^
|
||||||
|
isFlipped.hashCode ^
|
||||||
|
width.hashCode ^
|
||||||
|
height.hashCode ^
|
||||||
timeZone.hashCode ^
|
timeZone.hashCode ^
|
||||||
dateTimeOriginal.hashCode ^
|
dateTimeOriginal.hashCode ^
|
||||||
latitude.hashCode ^
|
latitude.hashCode ^
|
||||||
@ -114,6 +124,9 @@ class ExifInfo {
|
|||||||
fileSize: ${fileSize ?? 'NA'},
|
fileSize: ${fileSize ?? 'NA'},
|
||||||
description: ${description ?? 'NA'},
|
description: ${description ?? 'NA'},
|
||||||
orientation: ${orientation ?? 'NA'},
|
orientation: ${orientation ?? 'NA'},
|
||||||
|
width: ${width ?? 'NA'},
|
||||||
|
height: ${height ?? 'NA'},
|
||||||
|
isFlipped: $isFlipped,
|
||||||
timeZone: ${timeZone ?? 'NA'},
|
timeZone: ${timeZone ?? 'NA'},
|
||||||
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
|
dateTimeOriginal: ${dateTimeOriginal ?? 'NA'},
|
||||||
latitude: ${latitude ?? 'NA'},
|
latitude: ${latitude ?? 'NA'},
|
||||||
|
166
mobile/lib/domain/models/memory.model.dart
Normal file
166
mobile/lib/domain/models/memory.model.dart
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
// ignore_for_file: public_member_api_docs, sort_constructors_first
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
|
||||||
|
enum MemoryTypeEnum {
|
||||||
|
// do not change this order!
|
||||||
|
onThisDay,
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemoryData {
|
||||||
|
final int year;
|
||||||
|
|
||||||
|
const MemoryData({
|
||||||
|
required this.year,
|
||||||
|
});
|
||||||
|
|
||||||
|
MemoryData copyWith({
|
||||||
|
int? year,
|
||||||
|
}) {
|
||||||
|
return MemoryData(
|
||||||
|
year: year ?? this.year,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'year': year,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory MemoryData.fromMap(Map<String, dynamic> map) {
|
||||||
|
return MemoryData(
|
||||||
|
year: map['year'] as int,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory MemoryData.fromJson(String source) =>
|
||||||
|
MemoryData.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'MemoryData(year: $year)';
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant MemoryData other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.year == year;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => year.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Model for a memory stored in the server
|
||||||
|
class DriftMemory {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final DateTime? deletedAt;
|
||||||
|
final String ownerId;
|
||||||
|
|
||||||
|
// enum
|
||||||
|
final MemoryTypeEnum type;
|
||||||
|
final MemoryData data;
|
||||||
|
final bool isSaved;
|
||||||
|
final DateTime memoryAt;
|
||||||
|
final DateTime? seenAt;
|
||||||
|
final DateTime? showAt;
|
||||||
|
final DateTime? hideAt;
|
||||||
|
final List<RemoteAsset> assets;
|
||||||
|
|
||||||
|
const DriftMemory({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.type,
|
||||||
|
required this.data,
|
||||||
|
required this.isSaved,
|
||||||
|
required this.memoryAt,
|
||||||
|
this.seenAt,
|
||||||
|
this.showAt,
|
||||||
|
this.hideAt,
|
||||||
|
required this.assets,
|
||||||
|
});
|
||||||
|
|
||||||
|
DriftMemory copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
DateTime? deletedAt,
|
||||||
|
String? ownerId,
|
||||||
|
MemoryTypeEnum? type,
|
||||||
|
MemoryData? data,
|
||||||
|
bool? isSaved,
|
||||||
|
DateTime? memoryAt,
|
||||||
|
DateTime? seenAt,
|
||||||
|
DateTime? showAt,
|
||||||
|
DateTime? hideAt,
|
||||||
|
List<RemoteAsset>? assets,
|
||||||
|
}) {
|
||||||
|
return DriftMemory(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
type: type ?? this.type,
|
||||||
|
data: data ?? this.data,
|
||||||
|
isSaved: isSaved ?? this.isSaved,
|
||||||
|
memoryAt: memoryAt ?? this.memoryAt,
|
||||||
|
seenAt: seenAt ?? this.seenAt,
|
||||||
|
showAt: showAt ?? this.showAt,
|
||||||
|
hideAt: hideAt ?? this.hideAt,
|
||||||
|
assets: assets ?? this.assets,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Memory(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, ownerId: $ownerId, type: $type, data: $data, isSaved: $isSaved, memoryAt: $memoryAt, seenAt: $seenAt, showAt: $showAt, hideAt: $hideAt, assets: $assets)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant DriftMemory other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
final listEquals = const DeepCollectionEquality().equals;
|
||||||
|
|
||||||
|
return other.id == id &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.updatedAt == updatedAt &&
|
||||||
|
other.deletedAt == deletedAt &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
|
other.type == type &&
|
||||||
|
other.data == data &&
|
||||||
|
other.isSaved == isSaved &&
|
||||||
|
other.memoryAt == memoryAt &&
|
||||||
|
other.seenAt == seenAt &&
|
||||||
|
other.showAt == showAt &&
|
||||||
|
other.hideAt == hideAt &&
|
||||||
|
listEquals(other.assets, assets);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
createdAt.hashCode ^
|
||||||
|
updatedAt.hashCode ^
|
||||||
|
deletedAt.hashCode ^
|
||||||
|
ownerId.hashCode ^
|
||||||
|
type.hashCode ^
|
||||||
|
data.hashCode ^
|
||||||
|
isSaved.hashCode ^
|
||||||
|
memoryAt.hashCode ^
|
||||||
|
seenAt.hashCode ^
|
||||||
|
showAt.hashCode ^
|
||||||
|
hideAt.hashCode ^
|
||||||
|
assets.hashCode;
|
||||||
|
}
|
||||||
|
}
|
@ -3,7 +3,11 @@ import 'package:immich_mobile/domain/models/store.model.dart';
|
|||||||
enum Setting<T> {
|
enum Setting<T> {
|
||||||
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
|
tilesPerRow<int>(StoreKey.tilesPerRow, 4),
|
||||||
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
|
groupAssetsBy<int>(StoreKey.groupAssetsBy, 0),
|
||||||
showStorageIndicator<bool>(StoreKey.storageIndicator, true);
|
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
|
||||||
|
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
||||||
|
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
|
||||||
|
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
||||||
|
;
|
||||||
|
|
||||||
const Setting(this.storeKey, this.defaultValue);
|
const Setting(this.storeKey, this.defaultValue);
|
||||||
|
|
||||||
|
30
mobile/lib/domain/services/asset.service.dart
Normal file
30
mobile/lib/domain/services/asset.service.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
|
|
||||||
|
class AssetService {
|
||||||
|
final RemoteAssetRepository _remoteAssetRepository;
|
||||||
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
|
|
||||||
|
const AssetService({
|
||||||
|
required RemoteAssetRepository remoteAssetRepository,
|
||||||
|
required DriftLocalAssetRepository localAssetRepository,
|
||||||
|
}) : _remoteAssetRepository = remoteAssetRepository,
|
||||||
|
_localAssetRepository = localAssetRepository;
|
||||||
|
|
||||||
|
Stream<BaseAsset?> watchAsset(BaseAsset asset) {
|
||||||
|
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).id;
|
||||||
|
return asset is LocalAsset
|
||||||
|
? _localAssetRepository.watchAsset(id)
|
||||||
|
: _remoteAssetRepository.watchAsset(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
||||||
|
if (asset is LocalAsset || asset is! RemoteAsset) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _remoteAssetRepository.getExif(asset.id);
|
||||||
|
}
|
||||||
|
}
|
15
mobile/lib/domain/services/memory.service.dart
Normal file
15
mobile/lib/domain/services/memory.service.dart
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
class DriftMemoryService {
|
||||||
|
final log = Logger("DriftMemoryService");
|
||||||
|
|
||||||
|
final DriftMemoryRepository _repository;
|
||||||
|
|
||||||
|
DriftMemoryService(this._repository);
|
||||||
|
|
||||||
|
Future<List<DriftMemory>> getMemoryLane(String ownerId) {
|
||||||
|
return _repository.getAll(ownerId);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,11 @@
|
|||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||||
|
|
||||||
|
// Singleton instance of SettingsService, to use in places
|
||||||
|
// where reactivity is not required
|
||||||
|
// ignore: non_constant_identifier_names
|
||||||
|
final AppSetting = SettingsService(storeService: StoreService.I);
|
||||||
|
|
||||||
class SettingsService {
|
class SettingsService {
|
||||||
final StoreService _storeService;
|
final StoreService _storeService;
|
||||||
|
|
||||||
|
@ -146,6 +146,14 @@ class SyncStreamService {
|
|||||||
// to acknowledge that the client has processed all the backfill events
|
// to acknowledge that the client has processed all the backfill events
|
||||||
case SyncEntityType.syncAckV1:
|
case SyncEntityType.syncAckV1:
|
||||||
return;
|
return;
|
||||||
|
case SyncEntityType.memoryV1:
|
||||||
|
return _syncStreamRepository.updateMemoriesV1(data.cast());
|
||||||
|
case SyncEntityType.memoryDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteMemoriesV1(data.cast());
|
||||||
|
case SyncEntityType.memoryToAssetV1:
|
||||||
|
return _syncStreamRepository.updateMemoryAssetsV1(data.cast());
|
||||||
|
case SyncEntityType.memoryToAssetDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteMemoryAssetsV1(data.cast());
|
||||||
default:
|
default:
|
||||||
_logger.warning("Unknown sync data type: $type");
|
_logger.warning("Unknown sync data type: $type");
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
import 'package:immich_mobile/domain/models/timeline.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/setting.service.dart';
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/timeline.repository.dart';
|
||||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||||
|
|
||||||
@ -57,14 +58,19 @@ class TimelineFactory {
|
|||||||
class TimelineService {
|
class TimelineService {
|
||||||
final TimelineAssetSource _assetSource;
|
final TimelineAssetSource _assetSource;
|
||||||
final TimelineBucketSource _bucketSource;
|
final TimelineBucketSource _bucketSource;
|
||||||
|
int _totalAssets = 0;
|
||||||
|
int get totalAssets => _totalAssets;
|
||||||
|
|
||||||
TimelineService({
|
TimelineService({
|
||||||
required TimelineAssetSource assetSource,
|
required TimelineAssetSource assetSource,
|
||||||
required TimelineBucketSource bucketSource,
|
required TimelineBucketSource bucketSource,
|
||||||
}) : _assetSource = assetSource,
|
}) : _assetSource = assetSource,
|
||||||
_bucketSource = bucketSource {
|
_bucketSource = bucketSource {
|
||||||
_bucketSubscription =
|
_bucketSubscription = _bucketSource().listen((buckets) {
|
||||||
_bucketSource().listen((_) => unawaited(reloadBucket()));
|
_totalAssets =
|
||||||
|
buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
||||||
|
unawaited(_reloadBucket());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
final AsyncMutex _mutex = AsyncMutex();
|
final AsyncMutex _mutex = AsyncMutex();
|
||||||
@ -74,8 +80,9 @@ class TimelineService {
|
|||||||
|
|
||||||
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
Stream<List<Bucket>> Function() get watchBuckets => _bucketSource;
|
||||||
|
|
||||||
Future<void> reloadBucket() => _mutex.run(() async {
|
Future<void> _reloadBucket() => _mutex.run(() async {
|
||||||
_buffer = await _assetSource(_bufferOffset, _buffer.length);
|
_buffer = await _assetSource(_bufferOffset, _buffer.length);
|
||||||
|
EventStream.shared.emit(const TimelineReloadEvent());
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<List<BaseAsset>> loadAssets(int index, int count) =>
|
Future<List<BaseAsset>> loadAssets(int index, int count) =>
|
||||||
@ -117,6 +124,7 @@ class TimelineService {
|
|||||||
index >= _bufferOffset && index + count <= _bufferOffset + _buffer.length;
|
index >= _bufferOffset && index + count <= _bufferOffset + _buffer.length;
|
||||||
|
|
||||||
List<BaseAsset> getAssets(int index, int count) {
|
List<BaseAsset> getAssets(int index, int count) {
|
||||||
|
assert(index + count <= totalAssets);
|
||||||
if (!hasRange(index, count)) {
|
if (!hasRange(index, count)) {
|
||||||
throw RangeError('TimelineService::getAssets Index out of range');
|
throw RangeError('TimelineService::getAssets Index out of range');
|
||||||
}
|
}
|
||||||
@ -124,6 +132,17 @@ class TimelineService {
|
|||||||
return _buffer.slice(start, start + count);
|
return _buffer.slice(start, start + count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pre-cache assets around the given index for asset viewer
|
||||||
|
Future<void> preCacheAssets(int index) =>
|
||||||
|
_mutex.run(() => _loadAssets(index, 5));
|
||||||
|
|
||||||
|
BaseAsset getAsset(int index) {
|
||||||
|
if (!hasRange(index, 1)) {
|
||||||
|
throw RangeError('TimelineService::getAsset Index out of range');
|
||||||
|
}
|
||||||
|
return _buffer.elementAt(index - _bufferOffset);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
await _bucketSubscription?.cancel();
|
await _bucketSubscription?.cancel();
|
||||||
_bucketSubscription = null;
|
_bucketSubscription = null;
|
||||||
|
52
mobile/lib/domain/utils/event_stream.dart
Normal file
52
mobile/lib/domain/utils/event_stream.dart
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
sealed class Event {
|
||||||
|
const Event();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineReloadEvent extends Event {
|
||||||
|
const TimelineReloadEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewerOpenBottomSheetEvent extends Event {
|
||||||
|
const ViewerOpenBottomSheetEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
class EventStream {
|
||||||
|
EventStream._();
|
||||||
|
|
||||||
|
static final EventStream shared = EventStream._();
|
||||||
|
|
||||||
|
final StreamController<Event> _controller =
|
||||||
|
StreamController<Event>.broadcast();
|
||||||
|
|
||||||
|
void emit(Event event) {
|
||||||
|
_controller.add(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<T> where<T extends Event>() {
|
||||||
|
if (T == Event) {
|
||||||
|
return _controller.stream as Stream<T>;
|
||||||
|
}
|
||||||
|
return _controller.stream.where((event) => event is T).cast<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamSubscription<T> listen<T extends Event>(
|
||||||
|
void Function(T event)? onData, {
|
||||||
|
Function? onError,
|
||||||
|
void Function()? onDone,
|
||||||
|
bool? cancelOnError,
|
||||||
|
}) {
|
||||||
|
return where<T>().listen(
|
||||||
|
onData,
|
||||||
|
onError: onError,
|
||||||
|
onDone: onDone,
|
||||||
|
cancelOnError: cancelOnError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Closes the stream controller
|
||||||
|
void dispose() {
|
||||||
|
_controller.close();
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:drift/drift.dart' hide Query;
|
import 'package:drift/drift.dart' hide Query;
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart' as domain;
|
import 'package:immich_mobile/domain/models/exif.model.dart' as domain;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||||
@ -132,6 +133,8 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin {
|
|||||||
|
|
||||||
TextColumn get model => text().nullable()();
|
TextColumn get model => text().nullable()();
|
||||||
|
|
||||||
|
TextColumn get lens => text().nullable()();
|
||||||
|
|
||||||
TextColumn get orientation => text().nullable()();
|
TextColumn get orientation => text().nullable()();
|
||||||
|
|
||||||
TextColumn get timeZone => text().nullable()();
|
TextColumn get timeZone => text().nullable()();
|
||||||
@ -143,3 +146,27 @@ class RemoteExifEntity extends Table with DriftDefaultsMixin {
|
|||||||
@override
|
@override
|
||||||
Set<Column> get primaryKey => {assetId};
|
Set<Column> get primaryKey => {assetId};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension RemoteExifEntityDataDomainEx on RemoteExifEntityData {
|
||||||
|
domain.ExifInfo toDto() => domain.ExifInfo(
|
||||||
|
fileSize: fileSize,
|
||||||
|
dateTimeOriginal: dateTimeOriginal,
|
||||||
|
timeZone: timeZone,
|
||||||
|
make: make,
|
||||||
|
model: model,
|
||||||
|
iso: iso,
|
||||||
|
city: city,
|
||||||
|
state: state,
|
||||||
|
country: country,
|
||||||
|
description: description,
|
||||||
|
orientation: orientation,
|
||||||
|
latitude: latitude,
|
||||||
|
longitude: longitude,
|
||||||
|
f: fNumber?.toDouble(),
|
||||||
|
mm: focalLength?.toDouble(),
|
||||||
|
lens: lens,
|
||||||
|
width: width?.toDouble(),
|
||||||
|
height: height?.toDouble(),
|
||||||
|
isFlipped: ExifDtoConverter.isOrientationFlipped(orientation),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -27,6 +27,7 @@ typedef $$RemoteExifEntityTableCreateCompanionBuilder
|
|||||||
i0.Value<int?> iso,
|
i0.Value<int?> iso,
|
||||||
i0.Value<String?> make,
|
i0.Value<String?> make,
|
||||||
i0.Value<String?> model,
|
i0.Value<String?> model,
|
||||||
|
i0.Value<String?> lens,
|
||||||
i0.Value<String?> orientation,
|
i0.Value<String?> orientation,
|
||||||
i0.Value<String?> timeZone,
|
i0.Value<String?> timeZone,
|
||||||
i0.Value<int?> rating,
|
i0.Value<int?> rating,
|
||||||
@ -51,6 +52,7 @@ typedef $$RemoteExifEntityTableUpdateCompanionBuilder
|
|||||||
i0.Value<int?> iso,
|
i0.Value<int?> iso,
|
||||||
i0.Value<String?> make,
|
i0.Value<String?> make,
|
||||||
i0.Value<String?> model,
|
i0.Value<String?> model,
|
||||||
|
i0.Value<String?> lens,
|
||||||
i0.Value<String?> orientation,
|
i0.Value<String?> orientation,
|
||||||
i0.Value<String?> timeZone,
|
i0.Value<String?> timeZone,
|
||||||
i0.Value<int?> rating,
|
i0.Value<int?> rating,
|
||||||
@ -150,6 +152,9 @@ class $$RemoteExifEntityTableFilterComposer
|
|||||||
i0.ColumnFilters<String> get model => $composableBuilder(
|
i0.ColumnFilters<String> get model => $composableBuilder(
|
||||||
column: $table.model, builder: (column) => i0.ColumnFilters(column));
|
column: $table.model, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get lens => $composableBuilder(
|
||||||
|
column: $table.lens, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
i0.ColumnFilters<String> get orientation => $composableBuilder(
|
i0.ColumnFilters<String> get orientation => $composableBuilder(
|
||||||
column: $table.orientation,
|
column: $table.orientation,
|
||||||
builder: (column) => i0.ColumnFilters(column));
|
builder: (column) => i0.ColumnFilters(column));
|
||||||
@ -249,6 +254,9 @@ class $$RemoteExifEntityTableOrderingComposer
|
|||||||
i0.ColumnOrderings<String> get model => $composableBuilder(
|
i0.ColumnOrderings<String> get model => $composableBuilder(
|
||||||
column: $table.model, builder: (column) => i0.ColumnOrderings(column));
|
column: $table.model, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get lens => $composableBuilder(
|
||||||
|
column: $table.lens, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
i0.ColumnOrderings<String> get orientation => $composableBuilder(
|
i0.ColumnOrderings<String> get orientation => $composableBuilder(
|
||||||
column: $table.orientation,
|
column: $table.orientation,
|
||||||
builder: (column) => i0.ColumnOrderings(column));
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
@ -345,6 +353,9 @@ class $$RemoteExifEntityTableAnnotationComposer
|
|||||||
i0.GeneratedColumn<String> get model =>
|
i0.GeneratedColumn<String> get model =>
|
||||||
$composableBuilder(column: $table.model, builder: (column) => column);
|
$composableBuilder(column: $table.model, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get lens =>
|
||||||
|
$composableBuilder(column: $table.lens, builder: (column) => column);
|
||||||
|
|
||||||
i0.GeneratedColumn<String> get orientation => $composableBuilder(
|
i0.GeneratedColumn<String> get orientation => $composableBuilder(
|
||||||
column: $table.orientation, builder: (column) => column);
|
column: $table.orientation, builder: (column) => column);
|
||||||
|
|
||||||
@ -424,6 +435,7 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
|
|||||||
i0.Value<int?> iso = const i0.Value.absent(),
|
i0.Value<int?> iso = const i0.Value.absent(),
|
||||||
i0.Value<String?> make = const i0.Value.absent(),
|
i0.Value<String?> make = const i0.Value.absent(),
|
||||||
i0.Value<String?> model = const i0.Value.absent(),
|
i0.Value<String?> model = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> lens = const i0.Value.absent(),
|
||||||
i0.Value<String?> orientation = const i0.Value.absent(),
|
i0.Value<String?> orientation = const i0.Value.absent(),
|
||||||
i0.Value<String?> timeZone = const i0.Value.absent(),
|
i0.Value<String?> timeZone = const i0.Value.absent(),
|
||||||
i0.Value<int?> rating = const i0.Value.absent(),
|
i0.Value<int?> rating = const i0.Value.absent(),
|
||||||
@ -447,6 +459,7 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
|
|||||||
iso: iso,
|
iso: iso,
|
||||||
make: make,
|
make: make,
|
||||||
model: model,
|
model: model,
|
||||||
|
lens: lens,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
timeZone: timeZone,
|
timeZone: timeZone,
|
||||||
rating: rating,
|
rating: rating,
|
||||||
@ -470,6 +483,7 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
|
|||||||
i0.Value<int?> iso = const i0.Value.absent(),
|
i0.Value<int?> iso = const i0.Value.absent(),
|
||||||
i0.Value<String?> make = const i0.Value.absent(),
|
i0.Value<String?> make = const i0.Value.absent(),
|
||||||
i0.Value<String?> model = const i0.Value.absent(),
|
i0.Value<String?> model = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> lens = const i0.Value.absent(),
|
||||||
i0.Value<String?> orientation = const i0.Value.absent(),
|
i0.Value<String?> orientation = const i0.Value.absent(),
|
||||||
i0.Value<String?> timeZone = const i0.Value.absent(),
|
i0.Value<String?> timeZone = const i0.Value.absent(),
|
||||||
i0.Value<int?> rating = const i0.Value.absent(),
|
i0.Value<int?> rating = const i0.Value.absent(),
|
||||||
@ -493,6 +507,7 @@ class $$RemoteExifEntityTableTableManager extends i0.RootTableManager<
|
|||||||
iso: iso,
|
iso: iso,
|
||||||
make: make,
|
make: make,
|
||||||
model: model,
|
model: model,
|
||||||
|
lens: lens,
|
||||||
orientation: orientation,
|
orientation: orientation,
|
||||||
timeZone: timeZone,
|
timeZone: timeZone,
|
||||||
rating: rating,
|
rating: rating,
|
||||||
@ -666,6 +681,12 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
|
|||||||
late final i0.GeneratedColumn<String> model = i0.GeneratedColumn<String>(
|
late final i0.GeneratedColumn<String> model = i0.GeneratedColumn<String>(
|
||||||
'model', aliasedName, true,
|
'model', aliasedName, true,
|
||||||
type: i0.DriftSqlType.string, requiredDuringInsert: false);
|
type: i0.DriftSqlType.string, requiredDuringInsert: false);
|
||||||
|
static const i0.VerificationMeta _lensMeta =
|
||||||
|
const i0.VerificationMeta('lens');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> lens = i0.GeneratedColumn<String>(
|
||||||
|
'lens', aliasedName, true,
|
||||||
|
type: i0.DriftSqlType.string, requiredDuringInsert: false);
|
||||||
static const i0.VerificationMeta _orientationMeta =
|
static const i0.VerificationMeta _orientationMeta =
|
||||||
const i0.VerificationMeta('orientation');
|
const i0.VerificationMeta('orientation');
|
||||||
@override
|
@override
|
||||||
@ -709,6 +730,7 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
|
|||||||
iso,
|
iso,
|
||||||
make,
|
make,
|
||||||
model,
|
model,
|
||||||
|
lens,
|
||||||
orientation,
|
orientation,
|
||||||
timeZone,
|
timeZone,
|
||||||
rating,
|
rating,
|
||||||
@ -803,6 +825,10 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
|
|||||||
context.handle(
|
context.handle(
|
||||||
_modelMeta, model.isAcceptableOrUnknown(data['model']!, _modelMeta));
|
_modelMeta, model.isAcceptableOrUnknown(data['model']!, _modelMeta));
|
||||||
}
|
}
|
||||||
|
if (data.containsKey('lens')) {
|
||||||
|
context.handle(
|
||||||
|
_lensMeta, lens.isAcceptableOrUnknown(data['lens']!, _lensMeta));
|
||||||
|
}
|
||||||
if (data.containsKey('orientation')) {
|
if (data.containsKey('orientation')) {
|
||||||
context.handle(
|
context.handle(
|
||||||
_orientationMeta,
|
_orientationMeta,
|
||||||
@ -868,6 +894,8 @@ class $RemoteExifEntityTable extends i2.RemoteExifEntity
|
|||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}make']),
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}make']),
|
||||||
model: attachedDatabase.typeMapping
|
model: attachedDatabase.typeMapping
|
||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}model']),
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}model']),
|
||||||
|
lens: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}lens']),
|
||||||
orientation: attachedDatabase.typeMapping
|
orientation: attachedDatabase.typeMapping
|
||||||
.read(i0.DriftSqlType.string, data['${effectivePrefix}orientation']),
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}orientation']),
|
||||||
timeZone: attachedDatabase.typeMapping
|
timeZone: attachedDatabase.typeMapping
|
||||||
@ -909,6 +937,7 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
final int? iso;
|
final int? iso;
|
||||||
final String? make;
|
final String? make;
|
||||||
final String? model;
|
final String? model;
|
||||||
|
final String? lens;
|
||||||
final String? orientation;
|
final String? orientation;
|
||||||
final String? timeZone;
|
final String? timeZone;
|
||||||
final int? rating;
|
final int? rating;
|
||||||
@ -931,6 +960,7 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
this.iso,
|
this.iso,
|
||||||
this.make,
|
this.make,
|
||||||
this.model,
|
this.model,
|
||||||
|
this.lens,
|
||||||
this.orientation,
|
this.orientation,
|
||||||
this.timeZone,
|
this.timeZone,
|
||||||
this.rating,
|
this.rating,
|
||||||
@ -987,6 +1017,9 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
if (!nullToAbsent || model != null) {
|
if (!nullToAbsent || model != null) {
|
||||||
map['model'] = i0.Variable<String>(model);
|
map['model'] = i0.Variable<String>(model);
|
||||||
}
|
}
|
||||||
|
if (!nullToAbsent || lens != null) {
|
||||||
|
map['lens'] = i0.Variable<String>(lens);
|
||||||
|
}
|
||||||
if (!nullToAbsent || orientation != null) {
|
if (!nullToAbsent || orientation != null) {
|
||||||
map['orientation'] = i0.Variable<String>(orientation);
|
map['orientation'] = i0.Variable<String>(orientation);
|
||||||
}
|
}
|
||||||
@ -1024,6 +1057,7 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
iso: serializer.fromJson<int?>(json['iso']),
|
iso: serializer.fromJson<int?>(json['iso']),
|
||||||
make: serializer.fromJson<String?>(json['make']),
|
make: serializer.fromJson<String?>(json['make']),
|
||||||
model: serializer.fromJson<String?>(json['model']),
|
model: serializer.fromJson<String?>(json['model']),
|
||||||
|
lens: serializer.fromJson<String?>(json['lens']),
|
||||||
orientation: serializer.fromJson<String?>(json['orientation']),
|
orientation: serializer.fromJson<String?>(json['orientation']),
|
||||||
timeZone: serializer.fromJson<String?>(json['timeZone']),
|
timeZone: serializer.fromJson<String?>(json['timeZone']),
|
||||||
rating: serializer.fromJson<int?>(json['rating']),
|
rating: serializer.fromJson<int?>(json['rating']),
|
||||||
@ -1051,6 +1085,7 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
'iso': serializer.toJson<int?>(iso),
|
'iso': serializer.toJson<int?>(iso),
|
||||||
'make': serializer.toJson<String?>(make),
|
'make': serializer.toJson<String?>(make),
|
||||||
'model': serializer.toJson<String?>(model),
|
'model': serializer.toJson<String?>(model),
|
||||||
|
'lens': serializer.toJson<String?>(lens),
|
||||||
'orientation': serializer.toJson<String?>(orientation),
|
'orientation': serializer.toJson<String?>(orientation),
|
||||||
'timeZone': serializer.toJson<String?>(timeZone),
|
'timeZone': serializer.toJson<String?>(timeZone),
|
||||||
'rating': serializer.toJson<int?>(rating),
|
'rating': serializer.toJson<int?>(rating),
|
||||||
@ -1076,6 +1111,7 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
i0.Value<int?> iso = const i0.Value.absent(),
|
i0.Value<int?> iso = const i0.Value.absent(),
|
||||||
i0.Value<String?> make = const i0.Value.absent(),
|
i0.Value<String?> make = const i0.Value.absent(),
|
||||||
i0.Value<String?> model = const i0.Value.absent(),
|
i0.Value<String?> model = const i0.Value.absent(),
|
||||||
|
i0.Value<String?> lens = const i0.Value.absent(),
|
||||||
i0.Value<String?> orientation = const i0.Value.absent(),
|
i0.Value<String?> orientation = const i0.Value.absent(),
|
||||||
i0.Value<String?> timeZone = const i0.Value.absent(),
|
i0.Value<String?> timeZone = const i0.Value.absent(),
|
||||||
i0.Value<int?> rating = const i0.Value.absent(),
|
i0.Value<int?> rating = const i0.Value.absent(),
|
||||||
@ -1101,6 +1137,7 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
iso: iso.present ? iso.value : this.iso,
|
iso: iso.present ? iso.value : this.iso,
|
||||||
make: make.present ? make.value : this.make,
|
make: make.present ? make.value : this.make,
|
||||||
model: model.present ? model.value : this.model,
|
model: model.present ? model.value : this.model,
|
||||||
|
lens: lens.present ? lens.value : this.lens,
|
||||||
orientation: orientation.present ? orientation.value : this.orientation,
|
orientation: orientation.present ? orientation.value : this.orientation,
|
||||||
timeZone: timeZone.present ? timeZone.value : this.timeZone,
|
timeZone: timeZone.present ? timeZone.value : this.timeZone,
|
||||||
rating: rating.present ? rating.value : this.rating,
|
rating: rating.present ? rating.value : this.rating,
|
||||||
@ -1132,6 +1169,7 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
iso: data.iso.present ? data.iso.value : this.iso,
|
iso: data.iso.present ? data.iso.value : this.iso,
|
||||||
make: data.make.present ? data.make.value : this.make,
|
make: data.make.present ? data.make.value : this.make,
|
||||||
model: data.model.present ? data.model.value : this.model,
|
model: data.model.present ? data.model.value : this.model,
|
||||||
|
lens: data.lens.present ? data.lens.value : this.lens,
|
||||||
orientation:
|
orientation:
|
||||||
data.orientation.present ? data.orientation.value : this.orientation,
|
data.orientation.present ? data.orientation.value : this.orientation,
|
||||||
timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone,
|
timeZone: data.timeZone.present ? data.timeZone.value : this.timeZone,
|
||||||
@ -1162,6 +1200,7 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
..write('iso: $iso, ')
|
..write('iso: $iso, ')
|
||||||
..write('make: $make, ')
|
..write('make: $make, ')
|
||||||
..write('model: $model, ')
|
..write('model: $model, ')
|
||||||
|
..write('lens: $lens, ')
|
||||||
..write('orientation: $orientation, ')
|
..write('orientation: $orientation, ')
|
||||||
..write('timeZone: $timeZone, ')
|
..write('timeZone: $timeZone, ')
|
||||||
..write('rating: $rating, ')
|
..write('rating: $rating, ')
|
||||||
@ -1189,6 +1228,7 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
iso,
|
iso,
|
||||||
make,
|
make,
|
||||||
model,
|
model,
|
||||||
|
lens,
|
||||||
orientation,
|
orientation,
|
||||||
timeZone,
|
timeZone,
|
||||||
rating,
|
rating,
|
||||||
@ -1215,6 +1255,7 @@ class RemoteExifEntityData extends i0.DataClass
|
|||||||
other.iso == this.iso &&
|
other.iso == this.iso &&
|
||||||
other.make == this.make &&
|
other.make == this.make &&
|
||||||
other.model == this.model &&
|
other.model == this.model &&
|
||||||
|
other.lens == this.lens &&
|
||||||
other.orientation == this.orientation &&
|
other.orientation == this.orientation &&
|
||||||
other.timeZone == this.timeZone &&
|
other.timeZone == this.timeZone &&
|
||||||
other.rating == this.rating &&
|
other.rating == this.rating &&
|
||||||
@ -1240,6 +1281,7 @@ class RemoteExifEntityCompanion
|
|||||||
final i0.Value<int?> iso;
|
final i0.Value<int?> iso;
|
||||||
final i0.Value<String?> make;
|
final i0.Value<String?> make;
|
||||||
final i0.Value<String?> model;
|
final i0.Value<String?> model;
|
||||||
|
final i0.Value<String?> lens;
|
||||||
final i0.Value<String?> orientation;
|
final i0.Value<String?> orientation;
|
||||||
final i0.Value<String?> timeZone;
|
final i0.Value<String?> timeZone;
|
||||||
final i0.Value<int?> rating;
|
final i0.Value<int?> rating;
|
||||||
@ -1262,6 +1304,7 @@ class RemoteExifEntityCompanion
|
|||||||
this.iso = const i0.Value.absent(),
|
this.iso = const i0.Value.absent(),
|
||||||
this.make = const i0.Value.absent(),
|
this.make = const i0.Value.absent(),
|
||||||
this.model = const i0.Value.absent(),
|
this.model = const i0.Value.absent(),
|
||||||
|
this.lens = const i0.Value.absent(),
|
||||||
this.orientation = const i0.Value.absent(),
|
this.orientation = const i0.Value.absent(),
|
||||||
this.timeZone = const i0.Value.absent(),
|
this.timeZone = const i0.Value.absent(),
|
||||||
this.rating = const i0.Value.absent(),
|
this.rating = const i0.Value.absent(),
|
||||||
@ -1285,6 +1328,7 @@ class RemoteExifEntityCompanion
|
|||||||
this.iso = const i0.Value.absent(),
|
this.iso = const i0.Value.absent(),
|
||||||
this.make = const i0.Value.absent(),
|
this.make = const i0.Value.absent(),
|
||||||
this.model = const i0.Value.absent(),
|
this.model = const i0.Value.absent(),
|
||||||
|
this.lens = const i0.Value.absent(),
|
||||||
this.orientation = const i0.Value.absent(),
|
this.orientation = const i0.Value.absent(),
|
||||||
this.timeZone = const i0.Value.absent(),
|
this.timeZone = const i0.Value.absent(),
|
||||||
this.rating = const i0.Value.absent(),
|
this.rating = const i0.Value.absent(),
|
||||||
@ -1308,6 +1352,7 @@ class RemoteExifEntityCompanion
|
|||||||
i0.Expression<int>? iso,
|
i0.Expression<int>? iso,
|
||||||
i0.Expression<String>? make,
|
i0.Expression<String>? make,
|
||||||
i0.Expression<String>? model,
|
i0.Expression<String>? model,
|
||||||
|
i0.Expression<String>? lens,
|
||||||
i0.Expression<String>? orientation,
|
i0.Expression<String>? orientation,
|
||||||
i0.Expression<String>? timeZone,
|
i0.Expression<String>? timeZone,
|
||||||
i0.Expression<int>? rating,
|
i0.Expression<int>? rating,
|
||||||
@ -1331,6 +1376,7 @@ class RemoteExifEntityCompanion
|
|||||||
if (iso != null) 'iso': iso,
|
if (iso != null) 'iso': iso,
|
||||||
if (make != null) 'make': make,
|
if (make != null) 'make': make,
|
||||||
if (model != null) 'model': model,
|
if (model != null) 'model': model,
|
||||||
|
if (lens != null) 'lens': lens,
|
||||||
if (orientation != null) 'orientation': orientation,
|
if (orientation != null) 'orientation': orientation,
|
||||||
if (timeZone != null) 'time_zone': timeZone,
|
if (timeZone != null) 'time_zone': timeZone,
|
||||||
if (rating != null) 'rating': rating,
|
if (rating != null) 'rating': rating,
|
||||||
@ -1356,6 +1402,7 @@ class RemoteExifEntityCompanion
|
|||||||
i0.Value<int?>? iso,
|
i0.Value<int?>? iso,
|
||||||
i0.Value<String?>? make,
|
i0.Value<String?>? make,
|
||||||
i0.Value<String?>? model,
|
i0.Value<String?>? model,
|
||||||
|
i0.Value<String?>? lens,
|
||||||
i0.Value<String?>? orientation,
|
i0.Value<String?>? orientation,
|
||||||
i0.Value<String?>? timeZone,
|
i0.Value<String?>? timeZone,
|
||||||
i0.Value<int?>? rating,
|
i0.Value<int?>? rating,
|
||||||
@ -1378,6 +1425,7 @@ class RemoteExifEntityCompanion
|
|||||||
iso: iso ?? this.iso,
|
iso: iso ?? this.iso,
|
||||||
make: make ?? this.make,
|
make: make ?? this.make,
|
||||||
model: model ?? this.model,
|
model: model ?? this.model,
|
||||||
|
lens: lens ?? this.lens,
|
||||||
orientation: orientation ?? this.orientation,
|
orientation: orientation ?? this.orientation,
|
||||||
timeZone: timeZone ?? this.timeZone,
|
timeZone: timeZone ?? this.timeZone,
|
||||||
rating: rating ?? this.rating,
|
rating: rating ?? this.rating,
|
||||||
@ -1439,6 +1487,9 @@ class RemoteExifEntityCompanion
|
|||||||
if (model.present) {
|
if (model.present) {
|
||||||
map['model'] = i0.Variable<String>(model.value);
|
map['model'] = i0.Variable<String>(model.value);
|
||||||
}
|
}
|
||||||
|
if (lens.present) {
|
||||||
|
map['lens'] = i0.Variable<String>(lens.value);
|
||||||
|
}
|
||||||
if (orientation.present) {
|
if (orientation.present) {
|
||||||
map['orientation'] = i0.Variable<String>(orientation.value);
|
map['orientation'] = i0.Variable<String>(orientation.value);
|
||||||
}
|
}
|
||||||
@ -1474,6 +1525,7 @@ class RemoteExifEntityCompanion
|
|||||||
..write('iso: $iso, ')
|
..write('iso: $iso, ')
|
||||||
..write('make: $make, ')
|
..write('make: $make, ')
|
||||||
..write('model: $model, ')
|
..write('model: $model, ')
|
||||||
|
..write('lens: $lens, ')
|
||||||
..write('orientation: $orientation, ')
|
..write('orientation: $orientation, ')
|
||||||
..write('timeZone: $timeZone, ')
|
..write('timeZone: $timeZone, ')
|
||||||
..write('rating: $rating, ')
|
..write('rating: $rating, ')
|
||||||
|
@ -28,5 +28,8 @@ extension LocalAssetEntityDataDomainEx on LocalAssetEntityData {
|
|||||||
updatedAt: updatedAt,
|
updatedAt: updatedAt,
|
||||||
durationInSeconds: durationInSeconds,
|
durationInSeconds: durationInSeconds,
|
||||||
isFavorite: isFavorite,
|
isFavorite: isFavorite,
|
||||||
|
height: height,
|
||||||
|
width: width,
|
||||||
|
remoteId: null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
36
mobile/lib/infrastructure/entities/memory.entity.dart
Normal file
36
mobile/lib/infrastructure/entities/memory.entity.dart
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
|
class MemoryEntity extends Table with DriftDefaultsMixin {
|
||||||
|
const MemoryEntity();
|
||||||
|
|
||||||
|
TextColumn get id => text()();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
DateTimeColumn get deletedAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
TextColumn get ownerId =>
|
||||||
|
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
IntColumn get type => intEnum<MemoryTypeEnum>()();
|
||||||
|
|
||||||
|
TextColumn get data => text()();
|
||||||
|
|
||||||
|
BoolColumn get isSaved => boolean().withDefault(const Constant(false))();
|
||||||
|
|
||||||
|
DateTimeColumn get memoryAt => dateTime()();
|
||||||
|
|
||||||
|
DateTimeColumn get seenAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
DateTimeColumn get showAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
DateTimeColumn get hideAt => dateTime().nullable()();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
970
mobile/lib/infrastructure/entities/memory.entity.drift.dart
generated
Normal file
970
mobile/lib/infrastructure/entities/memory.entity.drift.dart
generated
Normal file
@ -0,0 +1,970 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart' as i0;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||||
|
as i1;
|
||||||
|
import 'package:immich_mobile/domain/models/memory.model.dart' as i2;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart' as i3;
|
||||||
|
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i4;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||||
|
as i5;
|
||||||
|
import 'package:drift/internal/modular.dart' as i6;
|
||||||
|
|
||||||
|
typedef $$MemoryEntityTableCreateCompanionBuilder = i1.MemoryEntityCompanion
|
||||||
|
Function({
|
||||||
|
required String id,
|
||||||
|
i0.Value<DateTime> createdAt,
|
||||||
|
i0.Value<DateTime> updatedAt,
|
||||||
|
i0.Value<DateTime?> deletedAt,
|
||||||
|
required String ownerId,
|
||||||
|
required i2.MemoryTypeEnum type,
|
||||||
|
required String data,
|
||||||
|
i0.Value<bool> isSaved,
|
||||||
|
required DateTime memoryAt,
|
||||||
|
i0.Value<DateTime?> seenAt,
|
||||||
|
i0.Value<DateTime?> showAt,
|
||||||
|
i0.Value<DateTime?> hideAt,
|
||||||
|
});
|
||||||
|
typedef $$MemoryEntityTableUpdateCompanionBuilder = i1.MemoryEntityCompanion
|
||||||
|
Function({
|
||||||
|
i0.Value<String> id,
|
||||||
|
i0.Value<DateTime> createdAt,
|
||||||
|
i0.Value<DateTime> updatedAt,
|
||||||
|
i0.Value<DateTime?> deletedAt,
|
||||||
|
i0.Value<String> ownerId,
|
||||||
|
i0.Value<i2.MemoryTypeEnum> type,
|
||||||
|
i0.Value<String> data,
|
||||||
|
i0.Value<bool> isSaved,
|
||||||
|
i0.Value<DateTime> memoryAt,
|
||||||
|
i0.Value<DateTime?> seenAt,
|
||||||
|
i0.Value<DateTime?> showAt,
|
||||||
|
i0.Value<DateTime?> hideAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final class $$MemoryEntityTableReferences extends i0.BaseReferences<
|
||||||
|
i0.GeneratedDatabase, i1.$MemoryEntityTable, i1.MemoryEntityData> {
|
||||||
|
$$MemoryEntityTableReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||||
|
|
||||||
|
static i5.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
|
||||||
|
i6.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i5.$UserEntityTable>('user_entity')
|
||||||
|
.createAlias(i0.$_aliasNameGenerator(
|
||||||
|
i6.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i1.$MemoryEntityTable>('memory_entity')
|
||||||
|
.ownerId,
|
||||||
|
i6.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i5.$UserEntityTable>('user_entity')
|
||||||
|
.id));
|
||||||
|
|
||||||
|
i5.$$UserEntityTableProcessedTableManager get ownerId {
|
||||||
|
final $_column = $_itemColumn<String>('owner_id')!;
|
||||||
|
|
||||||
|
final manager = i5
|
||||||
|
.$$UserEntityTableTableManager(
|
||||||
|
$_db,
|
||||||
|
i6.ReadDatabaseContainer($_db)
|
||||||
|
.resultSet<i5.$UserEntityTable>('user_entity'))
|
||||||
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
|
final item = $_typedResult.readTableOrNull(_ownerIdTable($_db));
|
||||||
|
if (item == null) return manager;
|
||||||
|
return i0.ProcessedTableManager(
|
||||||
|
manager.$state.copyWith(prefetchedData: [item]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MemoryEntityTableFilterComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryEntityTable> {
|
||||||
|
$$MemoryEntityTableFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.ColumnFilters<String> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get updatedAt => $composableBuilder(
|
||||||
|
column: $table.updatedAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get deletedAt => $composableBuilder(
|
||||||
|
column: $table.deletedAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnWithTypeConverterFilters<i2.MemoryTypeEnum, i2.MemoryTypeEnum, int>
|
||||||
|
get type => $composableBuilder(
|
||||||
|
column: $table.type,
|
||||||
|
builder: (column) => i0.ColumnWithTypeConverterFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<String> get data => $composableBuilder(
|
||||||
|
column: $table.data, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<bool> get isSaved => $composableBuilder(
|
||||||
|
column: $table.isSaved, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get memoryAt => $composableBuilder(
|
||||||
|
column: $table.memoryAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get seenAt => $composableBuilder(
|
||||||
|
column: $table.seenAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get showAt => $composableBuilder(
|
||||||
|
column: $table.showAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i0.ColumnFilters<DateTime> get hideAt => $composableBuilder(
|
||||||
|
column: $table.hideAt, builder: (column) => i0.ColumnFilters(column));
|
||||||
|
|
||||||
|
i5.$$UserEntityTableFilterComposer get ownerId {
|
||||||
|
final i5.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i6.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i5.$$UserEntityTableFilterComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i6.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MemoryEntityTableOrderingComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryEntityTable> {
|
||||||
|
$$MemoryEntityTableOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.ColumnOrderings<String> get id => $composableBuilder(
|
||||||
|
column: $table.id, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get createdAt => $composableBuilder(
|
||||||
|
column: $table.createdAt,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get updatedAt => $composableBuilder(
|
||||||
|
column: $table.updatedAt,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get deletedAt => $composableBuilder(
|
||||||
|
column: $table.deletedAt,
|
||||||
|
builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<int> get type => $composableBuilder(
|
||||||
|
column: $table.type, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<String> get data => $composableBuilder(
|
||||||
|
column: $table.data, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<bool> get isSaved => $composableBuilder(
|
||||||
|
column: $table.isSaved, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get memoryAt => $composableBuilder(
|
||||||
|
column: $table.memoryAt, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get seenAt => $composableBuilder(
|
||||||
|
column: $table.seenAt, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get showAt => $composableBuilder(
|
||||||
|
column: $table.showAt, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i0.ColumnOrderings<DateTime> get hideAt => $composableBuilder(
|
||||||
|
column: $table.hideAt, builder: (column) => i0.ColumnOrderings(column));
|
||||||
|
|
||||||
|
i5.$$UserEntityTableOrderingComposer get ownerId {
|
||||||
|
final i5.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i6.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i5.$$UserEntityTableOrderingComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i6.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MemoryEntityTableAnnotationComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryEntityTable> {
|
||||||
|
$$MemoryEntityTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i0.GeneratedColumn<String> get id =>
|
||||||
|
$composableBuilder(column: $table.id, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get createdAt =>
|
||||||
|
$composableBuilder(column: $table.createdAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get updatedAt =>
|
||||||
|
$composableBuilder(column: $table.updatedAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get deletedAt =>
|
||||||
|
$composableBuilder(column: $table.deletedAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumnWithTypeConverter<i2.MemoryTypeEnum, int> get type =>
|
||||||
|
$composableBuilder(column: $table.type, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<String> get data =>
|
||||||
|
$composableBuilder(column: $table.data, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<bool> get isSaved =>
|
||||||
|
$composableBuilder(column: $table.isSaved, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get memoryAt =>
|
||||||
|
$composableBuilder(column: $table.memoryAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get seenAt =>
|
||||||
|
$composableBuilder(column: $table.seenAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get showAt =>
|
||||||
|
$composableBuilder(column: $table.showAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i0.GeneratedColumn<DateTime> get hideAt =>
|
||||||
|
$composableBuilder(column: $table.hideAt, builder: (column) => column);
|
||||||
|
|
||||||
|
i5.$$UserEntityTableAnnotationComposer get ownerId {
|
||||||
|
final i5.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i6.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i5.$$UserEntityTableAnnotationComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i6.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MemoryEntityTableTableManager extends i0.RootTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$MemoryEntityTable,
|
||||||
|
i1.MemoryEntityData,
|
||||||
|
i1.$$MemoryEntityTableFilterComposer,
|
||||||
|
i1.$$MemoryEntityTableOrderingComposer,
|
||||||
|
i1.$$MemoryEntityTableAnnotationComposer,
|
||||||
|
$$MemoryEntityTableCreateCompanionBuilder,
|
||||||
|
$$MemoryEntityTableUpdateCompanionBuilder,
|
||||||
|
(i1.MemoryEntityData, i1.$$MemoryEntityTableReferences),
|
||||||
|
i1.MemoryEntityData,
|
||||||
|
i0.PrefetchHooks Function({bool ownerId})> {
|
||||||
|
$$MemoryEntityTableTableManager(
|
||||||
|
i0.GeneratedDatabase db, i1.$MemoryEntityTable table)
|
||||||
|
: super(i0.TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
i1.$$MemoryEntityTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
i1.$$MemoryEntityTableOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
i1.$$MemoryEntityTableAnnotationComposer($db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
i0.Value<String> id = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
|
i0.Value<String> ownerId = const i0.Value.absent(),
|
||||||
|
i0.Value<i2.MemoryTypeEnum> type = const i0.Value.absent(),
|
||||||
|
i0.Value<String> data = const i0.Value.absent(),
|
||||||
|
i0.Value<bool> isSaved = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> memoryAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> seenAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> showAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> hideAt = const i0.Value.absent(),
|
||||||
|
}) =>
|
||||||
|
i1.MemoryEntityCompanion(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
type: type,
|
||||||
|
data: data,
|
||||||
|
isSaved: isSaved,
|
||||||
|
memoryAt: memoryAt,
|
||||||
|
seenAt: seenAt,
|
||||||
|
showAt: showAt,
|
||||||
|
hideAt: hideAt,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
required String id,
|
||||||
|
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
|
required String ownerId,
|
||||||
|
required i2.MemoryTypeEnum type,
|
||||||
|
required String data,
|
||||||
|
i0.Value<bool> isSaved = const i0.Value.absent(),
|
||||||
|
required DateTime memoryAt,
|
||||||
|
i0.Value<DateTime?> seenAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> showAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> hideAt = const i0.Value.absent(),
|
||||||
|
}) =>
|
||||||
|
i1.MemoryEntityCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
type: type,
|
||||||
|
data: data,
|
||||||
|
isSaved: isSaved,
|
||||||
|
memoryAt: memoryAt,
|
||||||
|
seenAt: seenAt,
|
||||||
|
showAt: showAt,
|
||||||
|
hideAt: hideAt,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (
|
||||||
|
e.readTable(table),
|
||||||
|
i1.$$MemoryEntityTableReferences(db, table, e)
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: ({ownerId = false}) {
|
||||||
|
return i0.PrefetchHooks(
|
||||||
|
db: db,
|
||||||
|
explicitlyWatchedTables: [],
|
||||||
|
addJoins: <
|
||||||
|
T extends i0.TableManagerState<
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic>>(state) {
|
||||||
|
if (ownerId) {
|
||||||
|
state = state.withJoin(
|
||||||
|
currentTable: table,
|
||||||
|
currentColumn: table.ownerId,
|
||||||
|
referencedTable:
|
||||||
|
i1.$$MemoryEntityTableReferences._ownerIdTable(db),
|
||||||
|
referencedColumn:
|
||||||
|
i1.$$MemoryEntityTableReferences._ownerIdTable(db).id,
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
getPrefetchedDataCallback: (items) async {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$MemoryEntityTableProcessedTableManager = i0.ProcessedTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$MemoryEntityTable,
|
||||||
|
i1.MemoryEntityData,
|
||||||
|
i1.$$MemoryEntityTableFilterComposer,
|
||||||
|
i1.$$MemoryEntityTableOrderingComposer,
|
||||||
|
i1.$$MemoryEntityTableAnnotationComposer,
|
||||||
|
$$MemoryEntityTableCreateCompanionBuilder,
|
||||||
|
$$MemoryEntityTableUpdateCompanionBuilder,
|
||||||
|
(i1.MemoryEntityData, i1.$$MemoryEntityTableReferences),
|
||||||
|
i1.MemoryEntityData,
|
||||||
|
i0.PrefetchHooks Function({bool ownerId})>;
|
||||||
|
|
||||||
|
class $MemoryEntityTable extends i3.MemoryEntity
|
||||||
|
with i0.TableInfo<$MemoryEntityTable, i1.MemoryEntityData> {
|
||||||
|
@override
|
||||||
|
final i0.GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$MemoryEntityTable(this.attachedDatabase, [this._alias]);
|
||||||
|
static const i0.VerificationMeta _idMeta = const i0.VerificationMeta('id');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> id = i0.GeneratedColumn<String>(
|
||||||
|
'id', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const i0.VerificationMeta _createdAtMeta =
|
||||||
|
const i0.VerificationMeta('createdAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> createdAt =
|
||||||
|
i0.GeneratedColumn<DateTime>('created_at', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: i4.currentDateAndTime);
|
||||||
|
static const i0.VerificationMeta _updatedAtMeta =
|
||||||
|
const i0.VerificationMeta('updatedAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> updatedAt =
|
||||||
|
i0.GeneratedColumn<DateTime>('updated_at', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.dateTime,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultValue: i4.currentDateAndTime);
|
||||||
|
static const i0.VerificationMeta _deletedAtMeta =
|
||||||
|
const i0.VerificationMeta('deletedAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> deletedAt =
|
||||||
|
i0.GeneratedColumn<DateTime>('deleted_at', aliasedName, true,
|
||||||
|
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
|
static const i0.VerificationMeta _ownerIdMeta =
|
||||||
|
const i0.VerificationMeta('ownerId');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> ownerId = i0.GeneratedColumn<String>(
|
||||||
|
'owner_id', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES user_entity (id) ON DELETE CASCADE'));
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumnWithTypeConverter<i2.MemoryTypeEnum, int> type =
|
||||||
|
i0.GeneratedColumn<int>('type', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.int, requiredDuringInsert: true)
|
||||||
|
.withConverter<i2.MemoryTypeEnum>(
|
||||||
|
i1.$MemoryEntityTable.$convertertype);
|
||||||
|
static const i0.VerificationMeta _dataMeta =
|
||||||
|
const i0.VerificationMeta('data');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> data = i0.GeneratedColumn<String>(
|
||||||
|
'data', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string, requiredDuringInsert: true);
|
||||||
|
static const i0.VerificationMeta _isSavedMeta =
|
||||||
|
const i0.VerificationMeta('isSaved');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<bool> isSaved = i0.GeneratedColumn<bool>(
|
||||||
|
'is_saved', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.bool,
|
||||||
|
requiredDuringInsert: false,
|
||||||
|
defaultConstraints:
|
||||||
|
i0.GeneratedColumn.constraintIsAlways('CHECK ("is_saved" IN (0, 1))'),
|
||||||
|
defaultValue: const i4.Constant(false));
|
||||||
|
static const i0.VerificationMeta _memoryAtMeta =
|
||||||
|
const i0.VerificationMeta('memoryAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> memoryAt =
|
||||||
|
i0.GeneratedColumn<DateTime>('memory_at', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.dateTime, requiredDuringInsert: true);
|
||||||
|
static const i0.VerificationMeta _seenAtMeta =
|
||||||
|
const i0.VerificationMeta('seenAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> seenAt = i0.GeneratedColumn<DateTime>(
|
||||||
|
'seen_at', aliasedName, true,
|
||||||
|
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
|
static const i0.VerificationMeta _showAtMeta =
|
||||||
|
const i0.VerificationMeta('showAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> showAt = i0.GeneratedColumn<DateTime>(
|
||||||
|
'show_at', aliasedName, true,
|
||||||
|
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
|
static const i0.VerificationMeta _hideAtMeta =
|
||||||
|
const i0.VerificationMeta('hideAt');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<DateTime> hideAt = i0.GeneratedColumn<DateTime>(
|
||||||
|
'hide_at', aliasedName, true,
|
||||||
|
type: i0.DriftSqlType.dateTime, requiredDuringInsert: false);
|
||||||
|
@override
|
||||||
|
List<i0.GeneratedColumn> get $columns => [
|
||||||
|
id,
|
||||||
|
createdAt,
|
||||||
|
updatedAt,
|
||||||
|
deletedAt,
|
||||||
|
ownerId,
|
||||||
|
type,
|
||||||
|
data,
|
||||||
|
isSaved,
|
||||||
|
memoryAt,
|
||||||
|
seenAt,
|
||||||
|
showAt,
|
||||||
|
hideAt
|
||||||
|
];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'memory_entity';
|
||||||
|
@override
|
||||||
|
i0.VerificationContext validateIntegrity(
|
||||||
|
i0.Insertable<i1.MemoryEntityData> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = i0.VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('id')) {
|
||||||
|
context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_idMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('created_at')) {
|
||||||
|
context.handle(_createdAtMeta,
|
||||||
|
createdAt.isAcceptableOrUnknown(data['created_at']!, _createdAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('updated_at')) {
|
||||||
|
context.handle(_updatedAtMeta,
|
||||||
|
updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('deleted_at')) {
|
||||||
|
context.handle(_deletedAtMeta,
|
||||||
|
deletedAt.isAcceptableOrUnknown(data['deleted_at']!, _deletedAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('owner_id')) {
|
||||||
|
context.handle(_ownerIdMeta,
|
||||||
|
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_ownerIdMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('data')) {
|
||||||
|
context.handle(
|
||||||
|
_dataMeta, this.data.isAcceptableOrUnknown(data['data']!, _dataMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_dataMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('is_saved')) {
|
||||||
|
context.handle(_isSavedMeta,
|
||||||
|
isSaved.isAcceptableOrUnknown(data['is_saved']!, _isSavedMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('memory_at')) {
|
||||||
|
context.handle(_memoryAtMeta,
|
||||||
|
memoryAt.isAcceptableOrUnknown(data['memory_at']!, _memoryAtMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_memoryAtMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('seen_at')) {
|
||||||
|
context.handle(_seenAtMeta,
|
||||||
|
seenAt.isAcceptableOrUnknown(data['seen_at']!, _seenAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('show_at')) {
|
||||||
|
context.handle(_showAtMeta,
|
||||||
|
showAt.isAcceptableOrUnknown(data['show_at']!, _showAtMeta));
|
||||||
|
}
|
||||||
|
if (data.containsKey('hide_at')) {
|
||||||
|
context.handle(_hideAtMeta,
|
||||||
|
hideAt.isAcceptableOrUnknown(data['hide_at']!, _hideAtMeta));
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
i1.MemoryEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return i1.MemoryEntityData(
|
||||||
|
id: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}id'])!,
|
||||||
|
createdAt: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!,
|
||||||
|
updatedAt: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!,
|
||||||
|
deletedAt: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}deleted_at']),
|
||||||
|
ownerId: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!,
|
||||||
|
type: i1.$MemoryEntityTable.$convertertype.fromSql(attachedDatabase
|
||||||
|
.typeMapping
|
||||||
|
.read(i0.DriftSqlType.int, data['${effectivePrefix}type'])!),
|
||||||
|
data: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}data'])!,
|
||||||
|
isSaved: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.bool, data['${effectivePrefix}is_saved'])!,
|
||||||
|
memoryAt: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}memory_at'])!,
|
||||||
|
seenAt: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}seen_at']),
|
||||||
|
showAt: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}show_at']),
|
||||||
|
hideAt: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.dateTime, data['${effectivePrefix}hide_at']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$MemoryEntityTable createAlias(String alias) {
|
||||||
|
return $MemoryEntityTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
static i0.JsonTypeConverter2<i2.MemoryTypeEnum, int, int> $convertertype =
|
||||||
|
const i0.EnumIndexConverter<i2.MemoryTypeEnum>(i2.MemoryTypeEnum.values);
|
||||||
|
@override
|
||||||
|
bool get withoutRowId => true;
|
||||||
|
@override
|
||||||
|
bool get isStrict => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemoryEntityData extends i0.DataClass
|
||||||
|
implements i0.Insertable<i1.MemoryEntityData> {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final DateTime? deletedAt;
|
||||||
|
final String ownerId;
|
||||||
|
final i2.MemoryTypeEnum type;
|
||||||
|
final String data;
|
||||||
|
final bool isSaved;
|
||||||
|
final DateTime memoryAt;
|
||||||
|
final DateTime? seenAt;
|
||||||
|
final DateTime? showAt;
|
||||||
|
final DateTime? hideAt;
|
||||||
|
const MemoryEntityData(
|
||||||
|
{required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
this.deletedAt,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.type,
|
||||||
|
required this.data,
|
||||||
|
required this.isSaved,
|
||||||
|
required this.memoryAt,
|
||||||
|
this.seenAt,
|
||||||
|
this.showAt,
|
||||||
|
this.hideAt});
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
map['id'] = i0.Variable<String>(id);
|
||||||
|
map['created_at'] = i0.Variable<DateTime>(createdAt);
|
||||||
|
map['updated_at'] = i0.Variable<DateTime>(updatedAt);
|
||||||
|
if (!nullToAbsent || deletedAt != null) {
|
||||||
|
map['deleted_at'] = i0.Variable<DateTime>(deletedAt);
|
||||||
|
}
|
||||||
|
map['owner_id'] = i0.Variable<String>(ownerId);
|
||||||
|
{
|
||||||
|
map['type'] =
|
||||||
|
i0.Variable<int>(i1.$MemoryEntityTable.$convertertype.toSql(type));
|
||||||
|
}
|
||||||
|
map['data'] = i0.Variable<String>(data);
|
||||||
|
map['is_saved'] = i0.Variable<bool>(isSaved);
|
||||||
|
map['memory_at'] = i0.Variable<DateTime>(memoryAt);
|
||||||
|
if (!nullToAbsent || seenAt != null) {
|
||||||
|
map['seen_at'] = i0.Variable<DateTime>(seenAt);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || showAt != null) {
|
||||||
|
map['show_at'] = i0.Variable<DateTime>(showAt);
|
||||||
|
}
|
||||||
|
if (!nullToAbsent || hideAt != null) {
|
||||||
|
map['hide_at'] = i0.Variable<DateTime>(hideAt);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory MemoryEntityData.fromJson(Map<String, dynamic> json,
|
||||||
|
{i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return MemoryEntityData(
|
||||||
|
id: serializer.fromJson<String>(json['id']),
|
||||||
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||||
|
deletedAt: serializer.fromJson<DateTime?>(json['deletedAt']),
|
||||||
|
ownerId: serializer.fromJson<String>(json['ownerId']),
|
||||||
|
type: i1.$MemoryEntityTable.$convertertype
|
||||||
|
.fromJson(serializer.fromJson<int>(json['type'])),
|
||||||
|
data: serializer.fromJson<String>(json['data']),
|
||||||
|
isSaved: serializer.fromJson<bool>(json['isSaved']),
|
||||||
|
memoryAt: serializer.fromJson<DateTime>(json['memoryAt']),
|
||||||
|
seenAt: serializer.fromJson<DateTime?>(json['seenAt']),
|
||||||
|
showAt: serializer.fromJson<DateTime?>(json['showAt']),
|
||||||
|
hideAt: serializer.fromJson<DateTime?>(json['hideAt']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': serializer.toJson<String>(id),
|
||||||
|
'createdAt': serializer.toJson<DateTime>(createdAt),
|
||||||
|
'updatedAt': serializer.toJson<DateTime>(updatedAt),
|
||||||
|
'deletedAt': serializer.toJson<DateTime?>(deletedAt),
|
||||||
|
'ownerId': serializer.toJson<String>(ownerId),
|
||||||
|
'type': serializer
|
||||||
|
.toJson<int>(i1.$MemoryEntityTable.$convertertype.toJson(type)),
|
||||||
|
'data': serializer.toJson<String>(data),
|
||||||
|
'isSaved': serializer.toJson<bool>(isSaved),
|
||||||
|
'memoryAt': serializer.toJson<DateTime>(memoryAt),
|
||||||
|
'seenAt': serializer.toJson<DateTime?>(seenAt),
|
||||||
|
'showAt': serializer.toJson<DateTime?>(showAt),
|
||||||
|
'hideAt': serializer.toJson<DateTime?>(hideAt),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.MemoryEntityData copyWith(
|
||||||
|
{String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
i0.Value<DateTime?> deletedAt = const i0.Value.absent(),
|
||||||
|
String? ownerId,
|
||||||
|
i2.MemoryTypeEnum? type,
|
||||||
|
String? data,
|
||||||
|
bool? isSaved,
|
||||||
|
DateTime? memoryAt,
|
||||||
|
i0.Value<DateTime?> seenAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> showAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime?> hideAt = const i0.Value.absent()}) =>
|
||||||
|
i1.MemoryEntityData(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt.present ? deletedAt.value : this.deletedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
type: type ?? this.type,
|
||||||
|
data: data ?? this.data,
|
||||||
|
isSaved: isSaved ?? this.isSaved,
|
||||||
|
memoryAt: memoryAt ?? this.memoryAt,
|
||||||
|
seenAt: seenAt.present ? seenAt.value : this.seenAt,
|
||||||
|
showAt: showAt.present ? showAt.value : this.showAt,
|
||||||
|
hideAt: hideAt.present ? hideAt.value : this.hideAt,
|
||||||
|
);
|
||||||
|
MemoryEntityData copyWithCompanion(i1.MemoryEntityCompanion data) {
|
||||||
|
return MemoryEntityData(
|
||||||
|
id: data.id.present ? data.id.value : this.id,
|
||||||
|
createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt,
|
||||||
|
updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
|
||||||
|
deletedAt: data.deletedAt.present ? data.deletedAt.value : this.deletedAt,
|
||||||
|
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
|
||||||
|
type: data.type.present ? data.type.value : this.type,
|
||||||
|
data: data.data.present ? data.data.value : this.data,
|
||||||
|
isSaved: data.isSaved.present ? data.isSaved.value : this.isSaved,
|
||||||
|
memoryAt: data.memoryAt.present ? data.memoryAt.value : this.memoryAt,
|
||||||
|
seenAt: data.seenAt.present ? data.seenAt.value : this.seenAt,
|
||||||
|
showAt: data.showAt.present ? data.showAt.value : this.showAt,
|
||||||
|
hideAt: data.hideAt.present ? data.hideAt.value : this.hideAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('MemoryEntityData(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('updatedAt: $updatedAt, ')
|
||||||
|
..write('deletedAt: $deletedAt, ')
|
||||||
|
..write('ownerId: $ownerId, ')
|
||||||
|
..write('type: $type, ')
|
||||||
|
..write('data: $data, ')
|
||||||
|
..write('isSaved: $isSaved, ')
|
||||||
|
..write('memoryAt: $memoryAt, ')
|
||||||
|
..write('seenAt: $seenAt, ')
|
||||||
|
..write('showAt: $showAt, ')
|
||||||
|
..write('hideAt: $hideAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(id, createdAt, updatedAt, deletedAt, ownerId,
|
||||||
|
type, data, isSaved, memoryAt, seenAt, showAt, hideAt);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is i1.MemoryEntityData &&
|
||||||
|
other.id == this.id &&
|
||||||
|
other.createdAt == this.createdAt &&
|
||||||
|
other.updatedAt == this.updatedAt &&
|
||||||
|
other.deletedAt == this.deletedAt &&
|
||||||
|
other.ownerId == this.ownerId &&
|
||||||
|
other.type == this.type &&
|
||||||
|
other.data == this.data &&
|
||||||
|
other.isSaved == this.isSaved &&
|
||||||
|
other.memoryAt == this.memoryAt &&
|
||||||
|
other.seenAt == this.seenAt &&
|
||||||
|
other.showAt == this.showAt &&
|
||||||
|
other.hideAt == this.hideAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemoryEntityCompanion extends i0.UpdateCompanion<i1.MemoryEntityData> {
|
||||||
|
final i0.Value<String> id;
|
||||||
|
final i0.Value<DateTime> createdAt;
|
||||||
|
final i0.Value<DateTime> updatedAt;
|
||||||
|
final i0.Value<DateTime?> deletedAt;
|
||||||
|
final i0.Value<String> ownerId;
|
||||||
|
final i0.Value<i2.MemoryTypeEnum> type;
|
||||||
|
final i0.Value<String> data;
|
||||||
|
final i0.Value<bool> isSaved;
|
||||||
|
final i0.Value<DateTime> memoryAt;
|
||||||
|
final i0.Value<DateTime?> seenAt;
|
||||||
|
final i0.Value<DateTime?> showAt;
|
||||||
|
final i0.Value<DateTime?> hideAt;
|
||||||
|
const MemoryEntityCompanion({
|
||||||
|
this.id = const i0.Value.absent(),
|
||||||
|
this.createdAt = const i0.Value.absent(),
|
||||||
|
this.updatedAt = const i0.Value.absent(),
|
||||||
|
this.deletedAt = const i0.Value.absent(),
|
||||||
|
this.ownerId = const i0.Value.absent(),
|
||||||
|
this.type = const i0.Value.absent(),
|
||||||
|
this.data = const i0.Value.absent(),
|
||||||
|
this.isSaved = const i0.Value.absent(),
|
||||||
|
this.memoryAt = const i0.Value.absent(),
|
||||||
|
this.seenAt = const i0.Value.absent(),
|
||||||
|
this.showAt = const i0.Value.absent(),
|
||||||
|
this.hideAt = const i0.Value.absent(),
|
||||||
|
});
|
||||||
|
MemoryEntityCompanion.insert({
|
||||||
|
required String id,
|
||||||
|
this.createdAt = const i0.Value.absent(),
|
||||||
|
this.updatedAt = const i0.Value.absent(),
|
||||||
|
this.deletedAt = const i0.Value.absent(),
|
||||||
|
required String ownerId,
|
||||||
|
required i2.MemoryTypeEnum type,
|
||||||
|
required String data,
|
||||||
|
this.isSaved = const i0.Value.absent(),
|
||||||
|
required DateTime memoryAt,
|
||||||
|
this.seenAt = const i0.Value.absent(),
|
||||||
|
this.showAt = const i0.Value.absent(),
|
||||||
|
this.hideAt = const i0.Value.absent(),
|
||||||
|
}) : id = i0.Value(id),
|
||||||
|
ownerId = i0.Value(ownerId),
|
||||||
|
type = i0.Value(type),
|
||||||
|
data = i0.Value(data),
|
||||||
|
memoryAt = i0.Value(memoryAt);
|
||||||
|
static i0.Insertable<i1.MemoryEntityData> custom({
|
||||||
|
i0.Expression<String>? id,
|
||||||
|
i0.Expression<DateTime>? createdAt,
|
||||||
|
i0.Expression<DateTime>? updatedAt,
|
||||||
|
i0.Expression<DateTime>? deletedAt,
|
||||||
|
i0.Expression<String>? ownerId,
|
||||||
|
i0.Expression<int>? type,
|
||||||
|
i0.Expression<String>? data,
|
||||||
|
i0.Expression<bool>? isSaved,
|
||||||
|
i0.Expression<DateTime>? memoryAt,
|
||||||
|
i0.Expression<DateTime>? seenAt,
|
||||||
|
i0.Expression<DateTime>? showAt,
|
||||||
|
i0.Expression<DateTime>? hideAt,
|
||||||
|
}) {
|
||||||
|
return i0.RawValuesInsertable({
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
if (updatedAt != null) 'updated_at': updatedAt,
|
||||||
|
if (deletedAt != null) 'deleted_at': deletedAt,
|
||||||
|
if (ownerId != null) 'owner_id': ownerId,
|
||||||
|
if (type != null) 'type': type,
|
||||||
|
if (data != null) 'data': data,
|
||||||
|
if (isSaved != null) 'is_saved': isSaved,
|
||||||
|
if (memoryAt != null) 'memory_at': memoryAt,
|
||||||
|
if (seenAt != null) 'seen_at': seenAt,
|
||||||
|
if (showAt != null) 'show_at': showAt,
|
||||||
|
if (hideAt != null) 'hide_at': hideAt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.MemoryEntityCompanion copyWith(
|
||||||
|
{i0.Value<String>? id,
|
||||||
|
i0.Value<DateTime>? createdAt,
|
||||||
|
i0.Value<DateTime>? updatedAt,
|
||||||
|
i0.Value<DateTime?>? deletedAt,
|
||||||
|
i0.Value<String>? ownerId,
|
||||||
|
i0.Value<i2.MemoryTypeEnum>? type,
|
||||||
|
i0.Value<String>? data,
|
||||||
|
i0.Value<bool>? isSaved,
|
||||||
|
i0.Value<DateTime>? memoryAt,
|
||||||
|
i0.Value<DateTime?>? seenAt,
|
||||||
|
i0.Value<DateTime?>? showAt,
|
||||||
|
i0.Value<DateTime?>? hideAt}) {
|
||||||
|
return i1.MemoryEntityCompanion(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
deletedAt: deletedAt ?? this.deletedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
type: type ?? this.type,
|
||||||
|
data: data ?? this.data,
|
||||||
|
isSaved: isSaved ?? this.isSaved,
|
||||||
|
memoryAt: memoryAt ?? this.memoryAt,
|
||||||
|
seenAt: seenAt ?? this.seenAt,
|
||||||
|
showAt: showAt ?? this.showAt,
|
||||||
|
hideAt: hideAt ?? this.hideAt,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
if (id.present) {
|
||||||
|
map['id'] = i0.Variable<String>(id.value);
|
||||||
|
}
|
||||||
|
if (createdAt.present) {
|
||||||
|
map['created_at'] = i0.Variable<DateTime>(createdAt.value);
|
||||||
|
}
|
||||||
|
if (updatedAt.present) {
|
||||||
|
map['updated_at'] = i0.Variable<DateTime>(updatedAt.value);
|
||||||
|
}
|
||||||
|
if (deletedAt.present) {
|
||||||
|
map['deleted_at'] = i0.Variable<DateTime>(deletedAt.value);
|
||||||
|
}
|
||||||
|
if (ownerId.present) {
|
||||||
|
map['owner_id'] = i0.Variable<String>(ownerId.value);
|
||||||
|
}
|
||||||
|
if (type.present) {
|
||||||
|
map['type'] = i0.Variable<int>(
|
||||||
|
i1.$MemoryEntityTable.$convertertype.toSql(type.value));
|
||||||
|
}
|
||||||
|
if (data.present) {
|
||||||
|
map['data'] = i0.Variable<String>(data.value);
|
||||||
|
}
|
||||||
|
if (isSaved.present) {
|
||||||
|
map['is_saved'] = i0.Variable<bool>(isSaved.value);
|
||||||
|
}
|
||||||
|
if (memoryAt.present) {
|
||||||
|
map['memory_at'] = i0.Variable<DateTime>(memoryAt.value);
|
||||||
|
}
|
||||||
|
if (seenAt.present) {
|
||||||
|
map['seen_at'] = i0.Variable<DateTime>(seenAt.value);
|
||||||
|
}
|
||||||
|
if (showAt.present) {
|
||||||
|
map['show_at'] = i0.Variable<DateTime>(showAt.value);
|
||||||
|
}
|
||||||
|
if (hideAt.present) {
|
||||||
|
map['hide_at'] = i0.Variable<DateTime>(hideAt.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('MemoryEntityCompanion(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('updatedAt: $updatedAt, ')
|
||||||
|
..write('deletedAt: $deletedAt, ')
|
||||||
|
..write('ownerId: $ownerId, ')
|
||||||
|
..write('type: $type, ')
|
||||||
|
..write('data: $data, ')
|
||||||
|
..write('isSaved: $isSaved, ')
|
||||||
|
..write('memoryAt: $memoryAt, ')
|
||||||
|
..write('seenAt: $seenAt, ')
|
||||||
|
..write('showAt: $showAt, ')
|
||||||
|
..write('hideAt: $hideAt')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
17
mobile/lib/infrastructure/entities/memory_asset.entity.dart
Normal file
17
mobile/lib/infrastructure/entities/memory_asset.entity.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
|
class MemoryAssetEntity extends Table with DriftDefaultsMixin {
|
||||||
|
const MemoryAssetEntity();
|
||||||
|
|
||||||
|
TextColumn get assetId =>
|
||||||
|
text().references(RemoteAssetEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
TextColumn get memoryId =>
|
||||||
|
text().references(MemoryEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {assetId, memoryId};
|
||||||
|
}
|
550
mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart
generated
Normal file
550
mobile/lib/infrastructure/entities/memory_asset.entity.drift.dart
generated
Normal file
@ -0,0 +1,550 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart' as i0;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||||
|
as i1;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart'
|
||||||
|
as i2;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||||
|
as i3;
|
||||||
|
import 'package:drift/internal/modular.dart' as i4;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||||
|
as i5;
|
||||||
|
|
||||||
|
typedef $$MemoryAssetEntityTableCreateCompanionBuilder
|
||||||
|
= i1.MemoryAssetEntityCompanion Function({
|
||||||
|
required String assetId,
|
||||||
|
required String memoryId,
|
||||||
|
});
|
||||||
|
typedef $$MemoryAssetEntityTableUpdateCompanionBuilder
|
||||||
|
= i1.MemoryAssetEntityCompanion Function({
|
||||||
|
i0.Value<String> assetId,
|
||||||
|
i0.Value<String> memoryId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final class $$MemoryAssetEntityTableReferences extends i0.BaseReferences<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$MemoryAssetEntityTable,
|
||||||
|
i1.MemoryAssetEntityData> {
|
||||||
|
$$MemoryAssetEntityTableReferences(
|
||||||
|
super.$_db, super.$_table, super.$_typedResult);
|
||||||
|
|
||||||
|
static i3.$RemoteAssetEntityTable _assetIdTable(i0.GeneratedDatabase db) =>
|
||||||
|
i4.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||||
|
.createAlias(i0.$_aliasNameGenerator(
|
||||||
|
i4.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i1.$MemoryAssetEntityTable>('memory_asset_entity')
|
||||||
|
.assetId,
|
||||||
|
i4.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||||
|
.id));
|
||||||
|
|
||||||
|
i3.$$RemoteAssetEntityTableProcessedTableManager get assetId {
|
||||||
|
final $_column = $_itemColumn<String>('asset_id')!;
|
||||||
|
|
||||||
|
final manager = i3
|
||||||
|
.$$RemoteAssetEntityTableTableManager(
|
||||||
|
$_db,
|
||||||
|
i4.ReadDatabaseContainer($_db)
|
||||||
|
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'))
|
||||||
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
|
final item = $_typedResult.readTableOrNull(_assetIdTable($_db));
|
||||||
|
if (item == null) return manager;
|
||||||
|
return i0.ProcessedTableManager(
|
||||||
|
manager.$state.copyWith(prefetchedData: [item]));
|
||||||
|
}
|
||||||
|
|
||||||
|
static i5.$MemoryEntityTable _memoryIdTable(i0.GeneratedDatabase db) =>
|
||||||
|
i4.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i5.$MemoryEntityTable>('memory_entity')
|
||||||
|
.createAlias(i0.$_aliasNameGenerator(
|
||||||
|
i4.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i1.$MemoryAssetEntityTable>('memory_asset_entity')
|
||||||
|
.memoryId,
|
||||||
|
i4.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i5.$MemoryEntityTable>('memory_entity')
|
||||||
|
.id));
|
||||||
|
|
||||||
|
i5.$$MemoryEntityTableProcessedTableManager get memoryId {
|
||||||
|
final $_column = $_itemColumn<String>('memory_id')!;
|
||||||
|
|
||||||
|
final manager = i5
|
||||||
|
.$$MemoryEntityTableTableManager(
|
||||||
|
$_db,
|
||||||
|
i4.ReadDatabaseContainer($_db)
|
||||||
|
.resultSet<i5.$MemoryEntityTable>('memory_entity'))
|
||||||
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
|
final item = $_typedResult.readTableOrNull(_memoryIdTable($_db));
|
||||||
|
if (item == null) return manager;
|
||||||
|
return i0.ProcessedTableManager(
|
||||||
|
manager.$state.copyWith(prefetchedData: [item]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MemoryAssetEntityTableFilterComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryAssetEntityTable> {
|
||||||
|
$$MemoryAssetEntityTableFilterComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i3.$$RemoteAssetEntityTableFilterComposer get assetId {
|
||||||
|
final i3.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.assetId,
|
||||||
|
referencedTable: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i3.$$RemoteAssetEntityTableFilterComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
|
||||||
|
i5.$$MemoryEntityTableFilterComposer get memoryId {
|
||||||
|
final i5.$$MemoryEntityTableFilterComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.memoryId,
|
||||||
|
referencedTable: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i5.$$MemoryEntityTableFilterComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MemoryAssetEntityTableOrderingComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryAssetEntityTable> {
|
||||||
|
$$MemoryAssetEntityTableOrderingComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i3.$$RemoteAssetEntityTableOrderingComposer get assetId {
|
||||||
|
final i3.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||||
|
$composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.assetId,
|
||||||
|
referencedTable: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i3.$$RemoteAssetEntityTableOrderingComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i3.$RemoteAssetEntityTable>(
|
||||||
|
'remote_asset_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
|
||||||
|
i5.$$MemoryEntityTableOrderingComposer get memoryId {
|
||||||
|
final i5.$$MemoryEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.memoryId,
|
||||||
|
referencedTable: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i5.$$MemoryEntityTableOrderingComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MemoryAssetEntityTableAnnotationComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$MemoryAssetEntityTable> {
|
||||||
|
$$MemoryAssetEntityTableAnnotationComposer({
|
||||||
|
required super.$db,
|
||||||
|
required super.$table,
|
||||||
|
super.joinBuilder,
|
||||||
|
super.$addJoinBuilderToRootComposer,
|
||||||
|
super.$removeJoinBuilderFromRootComposer,
|
||||||
|
});
|
||||||
|
i3.$$RemoteAssetEntityTableAnnotationComposer get assetId {
|
||||||
|
final i3.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||||
|
$composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.assetId,
|
||||||
|
referencedTable: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i3.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i3.$$RemoteAssetEntityTableAnnotationComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i3.$RemoteAssetEntityTable>(
|
||||||
|
'remote_asset_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
|
||||||
|
i5.$$MemoryEntityTableAnnotationComposer get memoryId {
|
||||||
|
final i5.$$MemoryEntityTableAnnotationComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.memoryId,
|
||||||
|
referencedTable: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i5.$$MemoryEntityTableAnnotationComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i4.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i5.$MemoryEntityTable>('memory_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$MemoryAssetEntityTableTableManager extends i0.RootTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$MemoryAssetEntityTable,
|
||||||
|
i1.MemoryAssetEntityData,
|
||||||
|
i1.$$MemoryAssetEntityTableFilterComposer,
|
||||||
|
i1.$$MemoryAssetEntityTableOrderingComposer,
|
||||||
|
i1.$$MemoryAssetEntityTableAnnotationComposer,
|
||||||
|
$$MemoryAssetEntityTableCreateCompanionBuilder,
|
||||||
|
$$MemoryAssetEntityTableUpdateCompanionBuilder,
|
||||||
|
(i1.MemoryAssetEntityData, i1.$$MemoryAssetEntityTableReferences),
|
||||||
|
i1.MemoryAssetEntityData,
|
||||||
|
i0.PrefetchHooks Function({bool assetId, bool memoryId})> {
|
||||||
|
$$MemoryAssetEntityTableTableManager(
|
||||||
|
i0.GeneratedDatabase db, i1.$MemoryAssetEntityTable table)
|
||||||
|
: super(i0.TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
i1.$$MemoryAssetEntityTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () => i1
|
||||||
|
.$$MemoryAssetEntityTableOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
i1.$$MemoryAssetEntityTableAnnotationComposer(
|
||||||
|
$db: db, $table: table),
|
||||||
|
updateCompanionCallback: ({
|
||||||
|
i0.Value<String> assetId = const i0.Value.absent(),
|
||||||
|
i0.Value<String> memoryId = const i0.Value.absent(),
|
||||||
|
}) =>
|
||||||
|
i1.MemoryAssetEntityCompanion(
|
||||||
|
assetId: assetId,
|
||||||
|
memoryId: memoryId,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
required String assetId,
|
||||||
|
required String memoryId,
|
||||||
|
}) =>
|
||||||
|
i1.MemoryAssetEntityCompanion.insert(
|
||||||
|
assetId: assetId,
|
||||||
|
memoryId: memoryId,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (
|
||||||
|
e.readTable(table),
|
||||||
|
i1.$$MemoryAssetEntityTableReferences(db, table, e)
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: ({assetId = false, memoryId = false}) {
|
||||||
|
return i0.PrefetchHooks(
|
||||||
|
db: db,
|
||||||
|
explicitlyWatchedTables: [],
|
||||||
|
addJoins: <
|
||||||
|
T extends i0.TableManagerState<
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic,
|
||||||
|
dynamic>>(state) {
|
||||||
|
if (assetId) {
|
||||||
|
state = state.withJoin(
|
||||||
|
currentTable: table,
|
||||||
|
currentColumn: table.assetId,
|
||||||
|
referencedTable:
|
||||||
|
i1.$$MemoryAssetEntityTableReferences._assetIdTable(db),
|
||||||
|
referencedColumn: i1.$$MemoryAssetEntityTableReferences
|
||||||
|
._assetIdTable(db)
|
||||||
|
.id,
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
if (memoryId) {
|
||||||
|
state = state.withJoin(
|
||||||
|
currentTable: table,
|
||||||
|
currentColumn: table.memoryId,
|
||||||
|
referencedTable: i1.$$MemoryAssetEntityTableReferences
|
||||||
|
._memoryIdTable(db),
|
||||||
|
referencedColumn: i1.$$MemoryAssetEntityTableReferences
|
||||||
|
._memoryIdTable(db)
|
||||||
|
.id,
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
getPrefetchedDataCallback: (items) async {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$MemoryAssetEntityTableProcessedTableManager
|
||||||
|
= i0.ProcessedTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$MemoryAssetEntityTable,
|
||||||
|
i1.MemoryAssetEntityData,
|
||||||
|
i1.$$MemoryAssetEntityTableFilterComposer,
|
||||||
|
i1.$$MemoryAssetEntityTableOrderingComposer,
|
||||||
|
i1.$$MemoryAssetEntityTableAnnotationComposer,
|
||||||
|
$$MemoryAssetEntityTableCreateCompanionBuilder,
|
||||||
|
$$MemoryAssetEntityTableUpdateCompanionBuilder,
|
||||||
|
(i1.MemoryAssetEntityData, i1.$$MemoryAssetEntityTableReferences),
|
||||||
|
i1.MemoryAssetEntityData,
|
||||||
|
i0.PrefetchHooks Function({bool assetId, bool memoryId})>;
|
||||||
|
|
||||||
|
class $MemoryAssetEntityTable extends i2.MemoryAssetEntity
|
||||||
|
with i0.TableInfo<$MemoryAssetEntityTable, i1.MemoryAssetEntityData> {
|
||||||
|
@override
|
||||||
|
final i0.GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$MemoryAssetEntityTable(this.attachedDatabase, [this._alias]);
|
||||||
|
static const i0.VerificationMeta _assetIdMeta =
|
||||||
|
const i0.VerificationMeta('assetId');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> assetId = i0.GeneratedColumn<String>(
|
||||||
|
'asset_id', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_asset_entity (id) ON DELETE CASCADE'));
|
||||||
|
static const i0.VerificationMeta _memoryIdMeta =
|
||||||
|
const i0.VerificationMeta('memoryId');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> memoryId = i0.GeneratedColumn<String>(
|
||||||
|
'memory_id', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES memory_entity (id) ON DELETE CASCADE'));
|
||||||
|
@override
|
||||||
|
List<i0.GeneratedColumn> get $columns => [assetId, memoryId];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'memory_asset_entity';
|
||||||
|
@override
|
||||||
|
i0.VerificationContext validateIntegrity(
|
||||||
|
i0.Insertable<i1.MemoryAssetEntityData> instance,
|
||||||
|
{bool isInserting = false}) {
|
||||||
|
final context = i0.VerificationContext();
|
||||||
|
final data = instance.toColumns(true);
|
||||||
|
if (data.containsKey('asset_id')) {
|
||||||
|
context.handle(_assetIdMeta,
|
||||||
|
assetId.isAcceptableOrUnknown(data['asset_id']!, _assetIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_assetIdMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('memory_id')) {
|
||||||
|
context.handle(_memoryIdMeta,
|
||||||
|
memoryId.isAcceptableOrUnknown(data['memory_id']!, _memoryIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_memoryIdMeta);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<i0.GeneratedColumn> get $primaryKey => {assetId, memoryId};
|
||||||
|
@override
|
||||||
|
i1.MemoryAssetEntityData map(Map<String, dynamic> data,
|
||||||
|
{String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return i1.MemoryAssetEntityData(
|
||||||
|
assetId: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}asset_id'])!,
|
||||||
|
memoryId: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}memory_id'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$MemoryAssetEntityTable createAlias(String alias) {
|
||||||
|
return $MemoryAssetEntityTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get withoutRowId => true;
|
||||||
|
@override
|
||||||
|
bool get isStrict => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemoryAssetEntityData extends i0.DataClass
|
||||||
|
implements i0.Insertable<i1.MemoryAssetEntityData> {
|
||||||
|
final String assetId;
|
||||||
|
final String memoryId;
|
||||||
|
const MemoryAssetEntityData({required this.assetId, required this.memoryId});
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
map['asset_id'] = i0.Variable<String>(assetId);
|
||||||
|
map['memory_id'] = i0.Variable<String>(memoryId);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory MemoryAssetEntityData.fromJson(Map<String, dynamic> json,
|
||||||
|
{i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return MemoryAssetEntityData(
|
||||||
|
assetId: serializer.fromJson<String>(json['assetId']),
|
||||||
|
memoryId: serializer.fromJson<String>(json['memoryId']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@override
|
||||||
|
Map<String, dynamic> toJson({i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return <String, dynamic>{
|
||||||
|
'assetId': serializer.toJson<String>(assetId),
|
||||||
|
'memoryId': serializer.toJson<String>(memoryId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.MemoryAssetEntityData copyWith({String? assetId, String? memoryId}) =>
|
||||||
|
i1.MemoryAssetEntityData(
|
||||||
|
assetId: assetId ?? this.assetId,
|
||||||
|
memoryId: memoryId ?? this.memoryId,
|
||||||
|
);
|
||||||
|
MemoryAssetEntityData copyWithCompanion(i1.MemoryAssetEntityCompanion data) {
|
||||||
|
return MemoryAssetEntityData(
|
||||||
|
assetId: data.assetId.present ? data.assetId.value : this.assetId,
|
||||||
|
memoryId: data.memoryId.present ? data.memoryId.value : this.memoryId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('MemoryAssetEntityData(')
|
||||||
|
..write('assetId: $assetId, ')
|
||||||
|
..write('memoryId: $memoryId')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(assetId, memoryId);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is i1.MemoryAssetEntityData &&
|
||||||
|
other.assetId == this.assetId &&
|
||||||
|
other.memoryId == this.memoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
class MemoryAssetEntityCompanion
|
||||||
|
extends i0.UpdateCompanion<i1.MemoryAssetEntityData> {
|
||||||
|
final i0.Value<String> assetId;
|
||||||
|
final i0.Value<String> memoryId;
|
||||||
|
const MemoryAssetEntityCompanion({
|
||||||
|
this.assetId = const i0.Value.absent(),
|
||||||
|
this.memoryId = const i0.Value.absent(),
|
||||||
|
});
|
||||||
|
MemoryAssetEntityCompanion.insert({
|
||||||
|
required String assetId,
|
||||||
|
required String memoryId,
|
||||||
|
}) : assetId = i0.Value(assetId),
|
||||||
|
memoryId = i0.Value(memoryId);
|
||||||
|
static i0.Insertable<i1.MemoryAssetEntityData> custom({
|
||||||
|
i0.Expression<String>? assetId,
|
||||||
|
i0.Expression<String>? memoryId,
|
||||||
|
}) {
|
||||||
|
return i0.RawValuesInsertable({
|
||||||
|
if (assetId != null) 'asset_id': assetId,
|
||||||
|
if (memoryId != null) 'memory_id': memoryId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.MemoryAssetEntityCompanion copyWith(
|
||||||
|
{i0.Value<String>? assetId, i0.Value<String>? memoryId}) {
|
||||||
|
return i1.MemoryAssetEntityCompanion(
|
||||||
|
assetId: assetId ?? this.assetId,
|
||||||
|
memoryId: memoryId ?? this.memoryId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Map<String, i0.Expression> toColumns(bool nullToAbsent) {
|
||||||
|
final map = <String, i0.Expression>{};
|
||||||
|
if (assetId.present) {
|
||||||
|
map['asset_id'] = i0.Variable<String>(assetId.value);
|
||||||
|
}
|
||||||
|
if (memoryId.present) {
|
||||||
|
map['memory_id'] = i0.Variable<String>(memoryId.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('MemoryAssetEntityCompanion(')
|
||||||
|
..write('assetId: $assetId, ')
|
||||||
|
..write('memoryId: $memoryId')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,7 @@ mergedAsset: SELECT * FROM
|
|||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
local_asset_entity lae ON rae.checksum = lae.checksum
|
local_asset_entity lae ON rae.checksum = lae.checksum
|
||||||
WHERE
|
WHERE
|
||||||
rae.visibility = 0 AND rae.owner_id in ?
|
rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
NULL as remote_id,
|
NULL as remote_id,
|
||||||
@ -52,8 +52,8 @@ mergedBucket(:group_by AS INTEGER):
|
|||||||
SELECT
|
SELECT
|
||||||
COUNT(*) as asset_count,
|
COUNT(*) as asset_count,
|
||||||
CASE
|
CASE
|
||||||
WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', created_at) -- day
|
WHEN :group_by = 0 THEN STRFTIME('%Y-%m-%d', created_at, 'localtime') -- day
|
||||||
WHEN :group_by = 1 THEN STRFTIME('%Y-%m', created_at) -- month
|
WHEN :group_by = 1 THEN STRFTIME('%Y-%m', created_at, 'localtime') -- month
|
||||||
END AS bucket_date
|
END AS bucket_date
|
||||||
FROM
|
FROM
|
||||||
(
|
(
|
||||||
@ -65,7 +65,7 @@ FROM
|
|||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
local_asset_entity lae ON rae.checksum = lae.checksum
|
local_asset_entity lae ON rae.checksum = lae.checksum
|
||||||
WHERE
|
WHERE
|
||||||
rae.visibility = 0 AND rae.owner_id in ?
|
rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id in ?
|
||||||
UNION ALL
|
UNION ALL
|
||||||
SELECT
|
SELECT
|
||||||
lae.name,
|
lae.name,
|
||||||
|
@ -18,7 +18,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
|
final generatedlimit = $write(limit, startIndex: $arrayStartIndex);
|
||||||
$arrayStartIndex += generatedlimit.amountOfVariables;
|
$arrayStartIndex += generatedlimit.amountOfVariables;
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
|
'SELECT * FROM (SELECT rae.id AS remote_id, lae.id AS local_id, rae.name, rae.type, rae.created_at, rae.updated_at, rae.width, rae.height, rae.duration_in_seconds, rae.is_favorite, rae.thumb_hash, rae.checksum, rae.owner_id FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar1) UNION ALL SELECT NULL AS remote_id, lae.id AS local_id, lae.name, lae.type, lae.created_at, lae.updated_at, lae.width, lae.height, lae.duration_in_seconds, lae.is_favorite, NULL AS thumb_hash, lae.checksum, NULL AS owner_id FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) ORDER BY created_at DESC ${generatedlimit.sql}',
|
||||||
variables: [
|
variables: [
|
||||||
for (var $ in var1) i0.Variable<String>($),
|
for (var $ in var1) i0.Variable<String>($),
|
||||||
...generatedlimit.introducedVariables
|
...generatedlimit.introducedVariables
|
||||||
@ -51,7 +51,7 @@ class MergedAssetDrift extends i1.ModularAccessor {
|
|||||||
final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
|
final expandedvar2 = $expandVar($arrayStartIndex, var2.length);
|
||||||
$arrayStartIndex += var2.length;
|
$arrayStartIndex += var2.length;
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at) WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at) END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
|
'SELECT COUNT(*) AS asset_count, CASE WHEN ?1 = 0 THEN STRFTIME(\'%Y-%m-%d\', created_at, \'localtime\') WHEN ?1 = 1 THEN STRFTIME(\'%Y-%m\', created_at, \'localtime\') END AS bucket_date FROM (SELECT rae.name, rae.created_at FROM remote_asset_entity AS rae LEFT JOIN local_asset_entity AS lae ON rae.checksum = lae.checksum WHERE rae.deleted_at IS NULL AND rae.visibility = 0 AND rae.owner_id IN ($expandedvar2) UNION ALL SELECT lae.name, lae.created_at FROM local_asset_entity AS lae LEFT JOIN remote_asset_entity AS rae ON rae.checksum = lae.checksum WHERE rae.id IS NULL) GROUP BY bucket_date ORDER BY bucket_date DESC',
|
||||||
variables: [
|
variables: [
|
||||||
i0.Variable<int>(groupBy),
|
i0.Variable<int>(groupBy),
|
||||||
for (var $ in var2) i0.Variable<String>($)
|
for (var $ in var2) i0.Variable<String>($)
|
||||||
|
@ -7,6 +7,8 @@ import 'package:immich_mobile/infrastructure/entities/exif.entity.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.dart';
|
||||||
@ -46,6 +48,8 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
|||||||
RemoteAlbumEntity,
|
RemoteAlbumEntity,
|
||||||
RemoteAlbumAssetEntity,
|
RemoteAlbumAssetEntity,
|
||||||
RemoteAlbumUserEntity,
|
RemoteAlbumUserEntity,
|
||||||
|
MemoryEntity,
|
||||||
|
MemoryAssetEntity,
|
||||||
],
|
],
|
||||||
include: {
|
include: {
|
||||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||||
|
@ -23,9 +23,13 @@ import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.
|
|||||||
as i10;
|
as i10;
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart'
|
||||||
as i11;
|
as i11;
|
||||||
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart'
|
||||||
as i12;
|
as i12;
|
||||||
import 'package:drift/internal/modular.dart' as i13;
|
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart'
|
||||||
|
as i13;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||||
|
as i14;
|
||||||
|
import 'package:drift/internal/modular.dart' as i15;
|
||||||
|
|
||||||
abstract class $Drift extends i0.GeneratedDatabase {
|
abstract class $Drift extends i0.GeneratedDatabase {
|
||||||
$Drift(i0.QueryExecutor e) : super(e);
|
$Drift(i0.QueryExecutor e) : super(e);
|
||||||
@ -51,8 +55,11 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
i10.$RemoteAlbumAssetEntityTable(this);
|
i10.$RemoteAlbumAssetEntityTable(this);
|
||||||
late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
|
late final i11.$RemoteAlbumUserEntityTable remoteAlbumUserEntity =
|
||||||
i11.$RemoteAlbumUserEntityTable(this);
|
i11.$RemoteAlbumUserEntityTable(this);
|
||||||
i12.MergedAssetDrift get mergedAssetDrift => i13.ReadDatabaseContainer(this)
|
late final i12.$MemoryEntityTable memoryEntity = i12.$MemoryEntityTable(this);
|
||||||
.accessor<i12.MergedAssetDrift>(i12.MergedAssetDrift.new);
|
late final i13.$MemoryAssetEntityTable memoryAssetEntity =
|
||||||
|
i13.$MemoryAssetEntityTable(this);
|
||||||
|
i14.MergedAssetDrift get mergedAssetDrift => i15.ReadDatabaseContainer(this)
|
||||||
|
.accessor<i14.MergedAssetDrift>(i14.MergedAssetDrift.new);
|
||||||
@override
|
@override
|
||||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||||
@ -71,7 +78,9 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
remoteExifEntity,
|
remoteExifEntity,
|
||||||
remoteAlbumEntity,
|
remoteAlbumEntity,
|
||||||
remoteAlbumAssetEntity,
|
remoteAlbumAssetEntity,
|
||||||
remoteAlbumUserEntity
|
remoteAlbumUserEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
||||||
@ -175,6 +184,27 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
kind: i0.UpdateKind.delete),
|
kind: i0.UpdateKind.delete),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
i0.WritePropagation(
|
||||||
|
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||||
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
|
result: [
|
||||||
|
i0.TableUpdate('memory_entity', kind: i0.UpdateKind.delete),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
i0.WritePropagation(
|
||||||
|
on: i0.TableUpdateQuery.onTableName('remote_asset_entity',
|
||||||
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
|
result: [
|
||||||
|
i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
i0.WritePropagation(
|
||||||
|
on: i0.TableUpdateQuery.onTableName('memory_entity',
|
||||||
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
|
result: [
|
||||||
|
i0.TableUpdate('memory_asset_entity', kind: i0.UpdateKind.delete),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
@ -208,4 +238,8 @@ class $DriftManager {
|
|||||||
_db, _db.remoteAlbumAssetEntity);
|
_db, _db.remoteAlbumAssetEntity);
|
||||||
i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11
|
i11.$$RemoteAlbumUserEntityTableTableManager get remoteAlbumUserEntity => i11
|
||||||
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
|
.$$RemoteAlbumUserEntityTableTableManager(_db, _db.remoteAlbumUserEntity);
|
||||||
|
i12.$$MemoryEntityTableTableManager get memoryEntity =>
|
||||||
|
i12.$$MemoryEntityTableTableManager(_db, _db.memoryEntity);
|
||||||
|
i13.$$MemoryAssetEntityTableTableManager get memoryAssetEntity =>
|
||||||
|
i13.$$MemoryAssetEntityTableTableManager(_db, _db.memoryAssetEntity);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
||||||
as entity;
|
as entity;
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
|
|
||||||
@ -43,36 +41,3 @@ class IsarExifRepository extends IsarDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class DriftRemoteExifRepository extends DriftDatabaseRepository {
|
|
||||||
final Drift _db;
|
|
||||||
const DriftRemoteExifRepository(this._db) : super(_db);
|
|
||||||
|
|
||||||
Future<ExifInfo?> get(String assetId) {
|
|
||||||
final query = _db.remoteExifEntity.select()
|
|
||||||
..where((exif) => exif.assetId.equals(assetId));
|
|
||||||
|
|
||||||
return query.map((asset) => asset.toDto()).getSingleOrNull();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension on RemoteExifEntityData {
|
|
||||||
ExifInfo toDto() {
|
|
||||||
return ExifInfo(
|
|
||||||
fileSize: fileSize,
|
|
||||||
description: description,
|
|
||||||
orientation: orientation,
|
|
||||||
timeZone: timeZone,
|
|
||||||
dateTimeOriginal: dateTimeOriginal,
|
|
||||||
latitude: latitude,
|
|
||||||
longitude: longitude,
|
|
||||||
city: city,
|
|
||||||
state: state,
|
|
||||||
country: country,
|
|
||||||
make: make,
|
|
||||||
model: model,
|
|
||||||
f: fNumber,
|
|
||||||
iso: iso,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
@ -7,6 +8,26 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
|
|||||||
final Drift _db;
|
final Drift _db;
|
||||||
const DriftLocalAssetRepository(this._db) : super(_db);
|
const DriftLocalAssetRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Stream<LocalAsset?> watchAsset(String id) {
|
||||||
|
final query = _db.localAssetEntity
|
||||||
|
.select()
|
||||||
|
.addColumns([_db.localAssetEntity.id]).join([
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.localAssetEntity.checksum.equalsExp(_db.remoteAssetEntity.checksum),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(_db.localAssetEntity.id.equals(id));
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final asset = row.readTable(_db.localAssetEntity).toDto();
|
||||||
|
return asset.copyWith(
|
||||||
|
remoteId: row.read(_db.remoteAssetEntity.id),
|
||||||
|
);
|
||||||
|
}).watchSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateHashes(Iterable<LocalAsset> hashes) {
|
Future<void> updateHashes(Iterable<LocalAsset> hashes) {
|
||||||
if (hashes.isEmpty) {
|
if (hashes.isEmpty) {
|
||||||
return Future.value();
|
return Future.value();
|
||||||
|
@ -0,0 +1,81 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
|
class DriftMemoryRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftMemoryRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Future<List<DriftMemory>> getAll(String ownerId) async {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final localUtc = DateTime.utc(now.year, now.month, now.day, 0, 0, 0);
|
||||||
|
|
||||||
|
final query = _db.select(_db.memoryEntity).join([
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.memoryAssetEntity,
|
||||||
|
_db.memoryAssetEntity.memoryId.equalsExp(_db.memoryEntity.id),
|
||||||
|
),
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
_db.remoteAssetEntity.id.equalsExp(_db.memoryAssetEntity.assetId) &
|
||||||
|
_db.remoteAssetEntity.deletedAt.isNull() &
|
||||||
|
_db.remoteAssetEntity.visibility
|
||||||
|
.equalsValue(AssetVisibility.timeline),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(_db.memoryEntity.ownerId.equals(ownerId))
|
||||||
|
..where(_db.memoryEntity.deletedAt.isNull())
|
||||||
|
..where(
|
||||||
|
_db.memoryEntity.showAt.isSmallerOrEqualValue(localUtc),
|
||||||
|
)
|
||||||
|
..where(
|
||||||
|
_db.memoryEntity.hideAt.isBiggerOrEqualValue(localUtc),
|
||||||
|
)
|
||||||
|
..orderBy([
|
||||||
|
OrderingTerm.desc(_db.memoryEntity.memoryAt),
|
||||||
|
OrderingTerm.asc(_db.remoteAssetEntity.createdAt),
|
||||||
|
]);
|
||||||
|
|
||||||
|
final rows = await query.get();
|
||||||
|
|
||||||
|
final Map<String, DriftMemory> memoriesMap = {};
|
||||||
|
|
||||||
|
for (final row in rows) {
|
||||||
|
final memory = row.readTable(_db.memoryEntity);
|
||||||
|
final asset = row.readTable(_db.remoteAssetEntity);
|
||||||
|
|
||||||
|
final existingMemory = memoriesMap[memory.id];
|
||||||
|
if (existingMemory != null) {
|
||||||
|
existingMemory.assets.add(asset.toDto());
|
||||||
|
} else {
|
||||||
|
final assets = [asset.toDto()];
|
||||||
|
memoriesMap[memory.id] = memory.toDto().copyWith(assets: assets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return memoriesMap.values.toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on MemoryEntityData {
|
||||||
|
DriftMemory toDto() {
|
||||||
|
return DriftMemory(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
deletedAt: deletedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
type: type,
|
||||||
|
data: MemoryData.fromJson(data),
|
||||||
|
isSaved: isSaved,
|
||||||
|
memoryAt: memoryAt,
|
||||||
|
seenAt: seenAt,
|
||||||
|
showAt: showAt,
|
||||||
|
hideAt: hideAt,
|
||||||
|
assets: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,44 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.dart'
|
||||||
|
hide ExifInfo;
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
class DriftRemoteAssetRepository extends DriftDatabaseRepository {
|
class RemoteAssetRepository extends DriftDatabaseRepository {
|
||||||
final Drift _db;
|
final Drift _db;
|
||||||
const DriftRemoteAssetRepository(this._db) : super(_db);
|
const RemoteAssetRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Stream<RemoteAsset?> watchAsset(String id) {
|
||||||
|
final query = _db.remoteAssetEntity
|
||||||
|
.select()
|
||||||
|
.addColumns([_db.localAssetEntity.id]).join([
|
||||||
|
leftOuterJoin(
|
||||||
|
_db.localAssetEntity,
|
||||||
|
_db.remoteAssetEntity.checksum.equalsExp(_db.localAssetEntity.checksum),
|
||||||
|
useColumns: false,
|
||||||
|
),
|
||||||
|
])
|
||||||
|
..where(_db.remoteAssetEntity.id.equals(id));
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final asset = row.readTable(_db.remoteAssetEntity).toDto();
|
||||||
|
return asset.copyWith(
|
||||||
|
localId: row.read(_db.localAssetEntity.id),
|
||||||
|
);
|
||||||
|
}).watchSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ExifInfo?> getExif(String id) {
|
||||||
|
return _db.managers.remoteExifEntity
|
||||||
|
.filter((row) => row.assetId.id.equals(id))
|
||||||
|
.map((row) => row.toDto())
|
||||||
|
.getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateFavorite(List<String> ids, bool isFavorite) {
|
Future<void> updateFavorite(List<String> ids, bool isFavorite) {
|
||||||
return _db.batch((batch) async {
|
return _db.batch((batch) async {
|
||||||
@ -33,6 +64,22 @@ class DriftRemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> trash(List<String> ids) {
|
||||||
|
return _db.batch((batch) async {
|
||||||
|
for (final id in ids) {
|
||||||
|
batch.update(
|
||||||
|
_db.remoteAssetEntity,
|
||||||
|
RemoteAssetEntityCompanion(deletedAt: Value(DateTime.now())),
|
||||||
|
where: (e) => e.id.equals(id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(List<String> ids) {
|
||||||
|
return _db.remoteAssetEntity.deleteWhere((row) => row.id.isIn(ids));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateLocation(List<String> ids, LatLng location) {
|
Future<void> updateLocation(List<String> ids, LatLng location) {
|
||||||
return _db.batch((batch) async {
|
return _db.batch((batch) async {
|
||||||
for (final id in ids) {
|
for (final id in ids) {
|
||||||
|
@ -5,20 +5,21 @@ import 'package:logging/logging.dart';
|
|||||||
import 'package:photo_manager/photo_manager.dart';
|
import 'package:photo_manager/photo_manager.dart';
|
||||||
|
|
||||||
class StorageRepository {
|
class StorageRepository {
|
||||||
final _log = Logger('StorageRepository');
|
const StorageRepository();
|
||||||
|
|
||||||
Future<File?> getFileForAsset(LocalAsset asset) async {
|
Future<File?> getFileForAsset(LocalAsset asset) async {
|
||||||
|
final log = Logger('StorageRepository');
|
||||||
File? file;
|
File? file;
|
||||||
try {
|
try {
|
||||||
final entity = await AssetEntity.fromId(asset.id);
|
final entity = await AssetEntity.fromId(asset.id);
|
||||||
file = await entity?.originFile;
|
file = await entity?.originFile;
|
||||||
if (file == null) {
|
if (file == null) {
|
||||||
_log.warning(
|
log.warning(
|
||||||
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
"Cannot get file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error, stackTrace) {
|
} catch (error, stackTrace) {
|
||||||
_log.warning(
|
log.warning(
|
||||||
"Error getting file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
"Error getting file for asset ${asset.id}, name: ${asset.name}, created on: ${asset.createdAt}",
|
||||||
error,
|
error,
|
||||||
stackTrace,
|
stackTrace,
|
||||||
|
@ -52,6 +52,8 @@ class SyncApiRepository {
|
|||||||
SyncRequestType.albumAssetsV1,
|
SyncRequestType.albumAssetsV1,
|
||||||
SyncRequestType.albumAssetExifsV1,
|
SyncRequestType.albumAssetExifsV1,
|
||||||
SyncRequestType.albumToAssetsV1,
|
SyncRequestType.albumToAssetsV1,
|
||||||
|
SyncRequestType.memoriesV1,
|
||||||
|
SyncRequestType.memoryToAssetsV1,
|
||||||
],
|
],
|
||||||
).toJson(),
|
).toJson(),
|
||||||
);
|
);
|
||||||
@ -157,6 +159,10 @@ const _kResponseMap = <SyncEntityType, Function(Object)>{
|
|||||||
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
SyncEntityType.albumToAssetBackfillV1: SyncAlbumToAssetV1.fromJson,
|
||||||
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
|
SyncEntityType.albumToAssetDeleteV1: SyncAlbumToAssetDeleteV1.fromJson,
|
||||||
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
|
SyncEntityType.syncAckV1: _SyncAckV1.fromJson,
|
||||||
|
SyncEntityType.memoryV1: SyncMemoryV1.fromJson,
|
||||||
|
SyncEntityType.memoryDeleteV1: SyncMemoryDeleteV1.fromJson,
|
||||||
|
SyncEntityType.memoryToAssetV1: SyncMemoryAssetV1.fromJson,
|
||||||
|
SyncEntityType.memoryToAssetDeleteV1: SyncMemoryAssetDeleteV1.fromJson,
|
||||||
};
|
};
|
||||||
|
|
||||||
class _SyncAckV1 {
|
class _SyncAckV1 {
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/exif.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/partner.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_asset.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/memory.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
@ -64,8 +69,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: SyncPartnerDeleteV1', e, s);
|
_logger.severe('Error: SyncPartnerDeleteV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,8 +92,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: SyncPartnerV1', e, s);
|
_logger.severe('Error: SyncPartnerV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,10 +103,11 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
String debugLabel = 'user',
|
String debugLabel = 'user',
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
await _db.remoteAssetEntity
|
await _db.remoteAssetEntity.deleteWhere(
|
||||||
.deleteWhere((row) => row.id.isIn(data.map((e) => e.assetId)));
|
(row) => row.id.isIn(data.map((error) => error.assetId)),
|
||||||
} catch (e, s) {
|
);
|
||||||
_logger.severe('Error: deleteAssetsV1 - $debugLabel', e, s);
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,8 +142,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: updateAssetsV1 - $debugLabel', e, s);
|
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,8 +167,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
fNumber: Value(exif.fNumber),
|
fNumber: Value(exif.fNumber),
|
||||||
fileSize: Value(exif.fileSizeInByte),
|
fileSize: Value(exif.fileSizeInByte),
|
||||||
focalLength: Value(exif.focalLength),
|
focalLength: Value(exif.focalLength),
|
||||||
latitude: Value(exif.latitude),
|
latitude: Value(exif.latitude?.toDouble()),
|
||||||
longitude: Value(exif.longitude),
|
longitude: Value(exif.longitude?.toDouble()),
|
||||||
iso: Value(exif.iso),
|
iso: Value(exif.iso),
|
||||||
make: Value(exif.make),
|
make: Value(exif.make),
|
||||||
model: Value(exif.model),
|
model: Value(exif.model),
|
||||||
@ -170,6 +176,7 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
timeZone: Value(exif.timeZone),
|
timeZone: Value(exif.timeZone),
|
||||||
rating: Value(exif.rating),
|
rating: Value(exif.rating),
|
||||||
projectionType: Value(exif.projectionType),
|
projectionType: Value(exif.projectionType),
|
||||||
|
lens: Value(exif.lensModel),
|
||||||
);
|
);
|
||||||
|
|
||||||
batch.insert(
|
batch.insert(
|
||||||
@ -179,18 +186,23 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: updateAssetsExifV1 - $debugLabel', e, s);
|
_logger.severe(
|
||||||
|
'Error: updateAssetsExifV1 - $debugLabel',
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
|
Future<void> deleteAlbumsV1(Iterable<SyncAlbumDeleteV1> data) async {
|
||||||
try {
|
try {
|
||||||
await _db.remoteAlbumEntity
|
await _db.remoteAlbumEntity.deleteWhere(
|
||||||
.deleteWhere((row) => row.id.isIn(data.map((e) => e.albumId)));
|
(row) => row.id.isIn(data.map((e) => e.albumId)),
|
||||||
} catch (e, s) {
|
);
|
||||||
_logger.severe('Error: deleteAlbumsV1', e, s);
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe('Error: deleteAlbumsV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,8 +229,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: updateAlbumsV1', e, s);
|
_logger.severe('Error: updateAlbumsV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -236,8 +248,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: deleteAlbumUsersV1', e, s);
|
_logger.severe('Error: deleteAlbumUsersV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -263,8 +275,12 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: updateAlbumUsersV1 - $debugLabel', e, s);
|
_logger.severe(
|
||||||
|
'Error: updateAlbumUsersV1 - $debugLabel',
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -284,8 +300,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: deleteAlbumToAssetsV1', e, s);
|
_logger.severe('Error: deleteAlbumToAssetsV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -309,8 +325,96 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stackTrace) {
|
||||||
_logger.severe('Error: updateAlbumToAssetsV1 - $debugLabel', e, s);
|
_logger.severe(
|
||||||
|
'Error: updateAlbumToAssetsV1 - $debugLabel',
|
||||||
|
error,
|
||||||
|
stackTrace,
|
||||||
|
);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateMemoriesV1(Iterable<SyncMemoryV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final memory in data) {
|
||||||
|
final companion = MemoryEntityCompanion(
|
||||||
|
createdAt: Value(memory.createdAt),
|
||||||
|
deletedAt: Value(memory.deletedAt),
|
||||||
|
ownerId: Value(memory.ownerId),
|
||||||
|
type: Value(memory.type.toMemoryType()),
|
||||||
|
data: Value(jsonEncode(memory.data)),
|
||||||
|
isSaved: Value(memory.isSaved),
|
||||||
|
memoryAt: Value(memory.memoryAt),
|
||||||
|
seenAt: Value.absentIfNull(memory.seenAt),
|
||||||
|
showAt: Value.absentIfNull(memory.showAt),
|
||||||
|
hideAt: Value.absentIfNull(memory.hideAt),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.memoryEntity,
|
||||||
|
companion.copyWith(id: Value(memory.id)),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe('Error: updateMemoriesV1', error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteMemoriesV1(Iterable<SyncMemoryDeleteV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.memoryEntity.deleteWhere(
|
||||||
|
(row) => row.id.isIn(data.map((e) => e.memoryId)),
|
||||||
|
);
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe('Error: deleteMemoriesV1', error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateMemoryAssetsV1(Iterable<SyncMemoryAssetV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final asset in data) {
|
||||||
|
final companion = MemoryAssetEntityCompanion(
|
||||||
|
memoryId: Value(asset.memoryId),
|
||||||
|
assetId: Value(asset.assetId),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.memoryAssetEntity,
|
||||||
|
companion,
|
||||||
|
onConflict: DoNothing(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe('Error: updateMemoryAssetsV1', error, stackTrace);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteMemoryAssetsV1(
|
||||||
|
Iterable<SyncMemoryAssetDeleteV1> data,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final asset in data) {
|
||||||
|
batch.delete(
|
||||||
|
_db.memoryAssetEntity,
|
||||||
|
MemoryAssetEntityCompanion(
|
||||||
|
memoryId: Value(asset.memoryId),
|
||||||
|
assetId: Value(asset.assetId),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stackTrace) {
|
||||||
|
_logger.severe('Error: deleteMemoryAssetsV1', error, stackTrace);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -334,6 +438,13 @@ extension on AssetOrder {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on MemoryType {
|
||||||
|
MemoryTypeEnum toMemoryType() => switch (this) {
|
||||||
|
MemoryType.onThisDay => MemoryTypeEnum.onThisDay,
|
||||||
|
_ => throw Exception('Unknown MemoryType value: $this'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
extension on api.AlbumUserRole {
|
extension on api.AlbumUserRole {
|
||||||
AlbumUserRole toAlbumUserRole() => switch (this) {
|
AlbumUserRole toAlbumUserRole() => switch (this) {
|
||||||
api.AlbumUserRole.editor => AlbumUserRole.editor,
|
api.AlbumUserRole.editor => AlbumUserRole.editor,
|
||||||
@ -356,7 +467,7 @@ extension on String {
|
|||||||
Duration? toDuration() {
|
Duration? toDuration() {
|
||||||
try {
|
try {
|
||||||
final parts = split(':')
|
final parts = split(':')
|
||||||
.map((e) => double.parse(e).toInt())
|
.map((error) => double.parse(error).toInt())
|
||||||
.toList(growable: false);
|
.toList(growable: false);
|
||||||
|
|
||||||
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
return Duration(hours: parts[0], minutes: parts[1], seconds: parts[2]);
|
||||||
|
@ -238,7 +238,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
PhotoViewGalleryPageOptions buildImage(Asset asset) {
|
PhotoViewGalleryPageOptions buildImage(Asset asset) {
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
onDragStart: (_, details, __) {
|
onDragStart: (_, details, __, ___) {
|
||||||
localPosition.value = details.localPosition;
|
localPosition.value = details.localPosition;
|
||||||
},
|
},
|
||||||
onDragUpdate: (_, details, __) {
|
onDragUpdate: (_, details, __) {
|
||||||
@ -267,7 +267,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) {
|
PhotoViewGalleryPageOptions buildVideo(BuildContext context, Asset asset) {
|
||||||
return PhotoViewGalleryPageOptions.customChild(
|
return PhotoViewGalleryPageOptions.customChild(
|
||||||
onDragStart: (_, details, __) =>
|
onDragStart: (_, details, __, ___) =>
|
||||||
localPosition.value = details.localPosition,
|
localPosition.value = details.localPosition,
|
||||||
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
onDragUpdate: (_, details, __) => handleSwipeUpDown(details),
|
||||||
heroAttributes: _getHeroAttributes(asset),
|
heroAttributes: _getHeroAttributes(asset),
|
||||||
@ -370,7 +370,7 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
itemCount: totalAssets.value,
|
itemCount: totalAssets.value,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
onPageChanged: (value) {
|
onPageChanged: (value, _) {
|
||||||
final next = currentIndex.value < value ? value + 1 : value - 1;
|
final next = currentIndex.value < value ? value + 1 : value - 1;
|
||||||
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
|
@ -42,22 +42,6 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void onNavigationSelected(TabsRouter router, int index) {
|
|
||||||
// On Photos page menu tapped
|
|
||||||
if (router.activeIndex == 0 && index == 0) {
|
|
||||||
scrollToTopNotifierProvider.scrollToTop();
|
|
||||||
}
|
|
||||||
|
|
||||||
// On Search page tapped
|
|
||||||
if (router.activeIndex == 1 && index == 1) {
|
|
||||||
ref.read(searchInputFocusProvider).requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
|
||||||
router.setActiveIndex(index);
|
|
||||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
final navigationDestinations = [
|
final navigationDestinations = [
|
||||||
NavigationDestination(
|
NavigationDestination(
|
||||||
label: 'photos'.tr(),
|
label: 'photos'.tr(),
|
||||||
@ -110,15 +94,6 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
Widget bottomNavigationBar(TabsRouter tabsRouter) {
|
|
||||||
return NavigationBar(
|
|
||||||
selectedIndex: tabsRouter.activeIndex,
|
|
||||||
onDestinationSelected: (index) =>
|
|
||||||
onNavigationSelected(tabsRouter, index),
|
|
||||||
destinations: navigationDestinations,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget navigationRail(TabsRouter tabsRouter) {
|
Widget navigationRail(TabsRouter tabsRouter) {
|
||||||
return NavigationRail(
|
return NavigationRail(
|
||||||
destinations: navigationDestinations
|
destinations: navigationDestinations
|
||||||
@ -131,15 +106,13 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
onDestinationSelected: (index) =>
|
onDestinationSelected: (index) =>
|
||||||
onNavigationSelected(tabsRouter, index),
|
_onNavigationSelected(tabsRouter, index, ref),
|
||||||
selectedIndex: tabsRouter.activeIndex,
|
selectedIndex: tabsRouter.activeIndex,
|
||||||
labelType: NavigationRailLabelType.all,
|
labelType: NavigationRailLabelType.all,
|
||||||
groupAlignment: 0.0,
|
groupAlignment: 0.0,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final multiselectEnabled =
|
|
||||||
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
|
||||||
return AutoTabsRouter(
|
return AutoTabsRouter(
|
||||||
routes: [
|
routes: [
|
||||||
const MainTimelineRoute(),
|
const MainTimelineRoute(),
|
||||||
@ -173,12 +146,57 @@ class TabShellPage extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
: heroedChild,
|
: heroedChild,
|
||||||
bottomNavigationBar: multiselectEnabled || isScreenLandscape
|
bottomNavigationBar: _BottomNavigationBar(
|
||||||
? null
|
tabsRouter: tabsRouter,
|
||||||
: bottomNavigationBar(tabsRouter),
|
destinations: navigationDestinations,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
||||||
|
// On Photos page menu tapped
|
||||||
|
if (router.activeIndex == 0 && index == 0) {
|
||||||
|
scrollToTopNotifierProvider.scrollToTop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Search page tapped
|
||||||
|
if (router.activeIndex == 1 && index == 1) {
|
||||||
|
ref.read(searchInputFocusProvider).requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
|
router.setActiveIndex(index);
|
||||||
|
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomNavigationBar extends ConsumerWidget {
|
||||||
|
const _BottomNavigationBar({
|
||||||
|
required this.tabsRouter,
|
||||||
|
required this.destinations,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Widget> destinations;
|
||||||
|
final TabsRouter tabsRouter;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||||
|
final isMultiselectEnabled =
|
||||||
|
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
|
|
||||||
|
if (isScreenLandscape || isMultiselectEnabled) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
return NavigationBar(
|
||||||
|
selectedIndex: tabsRouter.activeIndex,
|
||||||
|
onDestinationSelected: (index) =>
|
||||||
|
_onNavigationSelected(tabsRouter, index, ref),
|
||||||
|
destinations: destinations,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MainTimelinePage extends ConsumerWidget {
|
class MainTimelinePage extends ConsumerWidget {
|
||||||
@ -9,6 +11,22 @@ class MainTimelinePage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return const Timeline();
|
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
||||||
|
|
||||||
|
return memoryLaneProvider.when(
|
||||||
|
data: (memories) {
|
||||||
|
return memories.isEmpty
|
||||||
|
? const Timeline()
|
||||||
|
: Timeline(
|
||||||
|
topSliverWidget: SliverToBoxAdapter(
|
||||||
|
key: Key('memory-lane-${memories.first.assets.first.id}'),
|
||||||
|
child: DriftMemoryLane(memories: memories),
|
||||||
|
),
|
||||||
|
topSliverWidgetHeight: 200,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
loading: () => const Timeline(),
|
||||||
|
error: (error, stackTrace) => const Timeline(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -154,6 +154,14 @@ final _remoteStats = [
|
|||||||
name: 'Remote Albums',
|
name: 'Remote Albums',
|
||||||
load: (db) => db.managers.remoteAlbumEntity.count(),
|
load: (db) => db.managers.remoteAlbumEntity.count(),
|
||||||
),
|
),
|
||||||
|
_Stat(
|
||||||
|
name: 'Memories',
|
||||||
|
load: (db) => db.managers.memoryEntity.count(),
|
||||||
|
),
|
||||||
|
_Stat(
|
||||||
|
name: 'Memories Assets',
|
||||||
|
load: (db) => db.managers.memoryAssetEntity.count(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
394
mobile/lib/presentation/pages/drift_memory.page.dart
Normal file
394
mobile/lib/presentation/pages/drift_memory.page.dart
Normal file
@ -0,0 +1,394 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/memory/memory_bottom_info.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/memory/memory_card.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/memories/memory_epilogue.dart';
|
||||||
|
import 'package:immich_mobile/widgets/memories/memory_progress_indicator.dart';
|
||||||
|
|
||||||
|
/// Expects [currentAssetNotifier] to be set before navigating to this page
|
||||||
|
@RoutePage()
|
||||||
|
class DriftMemoryPage extends HookConsumerWidget {
|
||||||
|
final List<DriftMemory> memories;
|
||||||
|
final int memoryIndex;
|
||||||
|
|
||||||
|
const DriftMemoryPage({
|
||||||
|
required this.memories,
|
||||||
|
required this.memoryIndex,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final currentMemory = useState(memories[memoryIndex]);
|
||||||
|
final currentAssetPage = useState(0);
|
||||||
|
final currentMemoryIndex = useState(memoryIndex);
|
||||||
|
final assetProgress = useState(
|
||||||
|
"${currentAssetPage.value + 1}|${currentMemory.value.assets.length}",
|
||||||
|
);
|
||||||
|
const bgColor = Colors.black;
|
||||||
|
final currentAsset = useState<RemoteAsset?>(null);
|
||||||
|
|
||||||
|
/// The list of all of the asset page controllers
|
||||||
|
final memoryAssetPageControllers =
|
||||||
|
List.generate(memories.length, (i) => usePageController());
|
||||||
|
|
||||||
|
/// The main vertically scrolling page controller with each list of memories
|
||||||
|
final memoryPageController = usePageController(initialPage: memoryIndex);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
// Memories is an immersive activity
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
|
||||||
|
return () {
|
||||||
|
// Clean up to normal edge to edge when we are done
|
||||||
|
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
toNextMemory() {
|
||||||
|
memoryPageController.nextPage(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void toPreviousMemory() {
|
||||||
|
if (currentMemoryIndex.value > 0) {
|
||||||
|
// Move to the previous memory page
|
||||||
|
memoryPageController.previousPage(
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
curve: Curves.easeIn,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wait for the next frame to ensure the page is built
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final previousIndex = currentMemoryIndex.value - 1;
|
||||||
|
final previousMemoryController =
|
||||||
|
memoryAssetPageControllers[previousIndex];
|
||||||
|
|
||||||
|
// Ensure the controller is attached
|
||||||
|
if (previousMemoryController.hasClients) {
|
||||||
|
previousMemoryController
|
||||||
|
.jumpToPage(memories[previousIndex].assets.length - 1);
|
||||||
|
} else {
|
||||||
|
// Wait for the next frame until it is attached
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (previousMemoryController.hasClients) {
|
||||||
|
previousMemoryController
|
||||||
|
.jumpToPage(memories[previousIndex].assets.length - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toNextAsset(int currentAssetIndex) {
|
||||||
|
if (currentAssetIndex + 1 < currentMemory.value.assets.length) {
|
||||||
|
// Go to the next asset
|
||||||
|
PageController controller =
|
||||||
|
memoryAssetPageControllers[currentMemoryIndex.value];
|
||||||
|
|
||||||
|
controller.nextPage(
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Go to the next memory since we are at the end of our assets
|
||||||
|
toNextMemory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toPreviousAsset(int currentAssetIndex) {
|
||||||
|
if (currentAssetIndex > 0) {
|
||||||
|
// Go to the previous asset
|
||||||
|
PageController controller =
|
||||||
|
memoryAssetPageControllers[currentMemoryIndex.value];
|
||||||
|
|
||||||
|
controller.previousPage(
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
duration: const Duration(milliseconds: 500),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Go to the previous memory since we are at the end of our assets
|
||||||
|
toPreviousMemory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateProgressText() {
|
||||||
|
assetProgress.value =
|
||||||
|
"${currentAssetPage.value + 1}|${currentMemory.value.assets.length}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Downloads and caches the image for the asset at this [currentMemory]'s index
|
||||||
|
precacheAsset(int index) async {
|
||||||
|
// Guard index out of range
|
||||||
|
if (index < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context might be removed due to popping out of Memory Lane during Scroll handling
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
late RemoteAsset asset;
|
||||||
|
if (index < currentMemory.value.assets.length) {
|
||||||
|
// Uses the next asset in this current memory
|
||||||
|
asset = currentMemory.value.assets[index];
|
||||||
|
} else {
|
||||||
|
// Precache the first asset in the next memory if available
|
||||||
|
final currentMemoryIndex = memories.indexOf(currentMemory.value);
|
||||||
|
|
||||||
|
// Guard no memory found
|
||||||
|
if (currentMemoryIndex == -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final nextMemoryIndex = currentMemoryIndex + 1;
|
||||||
|
// Guard no next memory
|
||||||
|
if (nextMemoryIndex >= memories.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the first asset from the next memory
|
||||||
|
asset = memories[nextMemoryIndex].assets.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precache the asset
|
||||||
|
final size = MediaQuery.sizeOf(context);
|
||||||
|
await precacheImage(
|
||||||
|
getFullImageProvider(
|
||||||
|
asset,
|
||||||
|
size: Size(size.width, size.height),
|
||||||
|
),
|
||||||
|
context,
|
||||||
|
size: size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precache the next page right away if we are on the first page
|
||||||
|
if (currentAssetPage.value == 0) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 200))
|
||||||
|
.then((_) => precacheAsset(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> onAssetChanged(int otherIndex) async {
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||||
|
currentAssetPage.value = otherIndex;
|
||||||
|
updateProgressText();
|
||||||
|
|
||||||
|
// Wait for page change animation to finish
|
||||||
|
await Future.delayed(const Duration(milliseconds: 400));
|
||||||
|
// And then precache the next asset
|
||||||
|
await precacheAsset(otherIndex + 1);
|
||||||
|
|
||||||
|
final asset = currentMemory.value.assets[otherIndex];
|
||||||
|
currentAsset.value = asset;
|
||||||
|
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
||||||
|
// if (asset.isVideo || asset.isMotionPhoto) {
|
||||||
|
if (asset.isVideo) {
|
||||||
|
ref.read(videoPlaybackValueProvider.notifier).reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Notification listener is used instead of OnPageChanged callback since OnPageChanged is called
|
||||||
|
* when the page in the **center** of the viewer changes. We want to reset currentAssetPage only when the final
|
||||||
|
* page during the end of scroll is different than the current page
|
||||||
|
*/
|
||||||
|
return NotificationListener<ScrollNotification>(
|
||||||
|
onNotification: (ScrollNotification notification) {
|
||||||
|
// Calculate OverScroll manually using the number of pixels away from maxScrollExtent
|
||||||
|
// maxScrollExtend contains the sum of horizontal pixels of all assets for depth = 1
|
||||||
|
// or sum of vertical pixels of all memories for depth = 0
|
||||||
|
if (notification is ScrollUpdateNotification) {
|
||||||
|
final isEpiloguePage =
|
||||||
|
(memoryPageController.page?.floor() ?? 0) >= memories.length;
|
||||||
|
|
||||||
|
final offset = notification.metrics.pixels;
|
||||||
|
if (isEpiloguePage &&
|
||||||
|
(offset > notification.metrics.maxScrollExtent + 150)) {
|
||||||
|
context.maybePop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
body: SafeArea(
|
||||||
|
child: PageView.builder(
|
||||||
|
physics: const BouncingScrollPhysics(
|
||||||
|
parent: AlwaysScrollableScrollPhysics(),
|
||||||
|
),
|
||||||
|
scrollDirection: Axis.vertical,
|
||||||
|
controller: memoryPageController,
|
||||||
|
onPageChanged: (pageNumber) {
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).mediumImpact();
|
||||||
|
if (pageNumber < memories.length) {
|
||||||
|
currentMemoryIndex.value = pageNumber;
|
||||||
|
currentMemory.value = memories[pageNumber];
|
||||||
|
}
|
||||||
|
|
||||||
|
currentAssetPage.value = 0;
|
||||||
|
|
||||||
|
updateProgressText();
|
||||||
|
},
|
||||||
|
itemCount: memories.length + 1,
|
||||||
|
itemBuilder: (context, mIndex) {
|
||||||
|
// Build last page
|
||||||
|
if (mIndex == memories.length) {
|
||||||
|
return MemoryEpilogue(
|
||||||
|
onStartOver: () => memoryPageController.animateToPage(
|
||||||
|
0,
|
||||||
|
duration: const Duration(seconds: 1),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final yearsAgo = DateTime.now().year - memories[mIndex].data.year;
|
||||||
|
final title = 'years_ago'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'years': yearsAgo.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
// Build horizontal page
|
||||||
|
final assetController = memoryAssetPageControllers[mIndex];
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
left: 24.0,
|
||||||
|
right: 24.0,
|
||||||
|
top: 8.0,
|
||||||
|
bottom: 2.0,
|
||||||
|
),
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: assetController,
|
||||||
|
builder: (context, child) {
|
||||||
|
double value = 0.0;
|
||||||
|
if (assetController.hasClients) {
|
||||||
|
// We can only access [page] if this has clients
|
||||||
|
value = assetController.page ?? 0;
|
||||||
|
}
|
||||||
|
return MemoryProgressIndicator(
|
||||||
|
ticks: memories[mIndex].assets.length,
|
||||||
|
value: (value + 1) / memories[mIndex].assets.length,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
PageView.builder(
|
||||||
|
physics: const BouncingScrollPhysics(
|
||||||
|
parent: AlwaysScrollableScrollPhysics(),
|
||||||
|
),
|
||||||
|
controller: assetController,
|
||||||
|
onPageChanged: onAssetChanged,
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: memories[mIndex].assets.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final asset = memories[mIndex].assets[index];
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
color: Colors.black,
|
||||||
|
child: DriftMemoryCard(
|
||||||
|
asset: asset,
|
||||||
|
title: title,
|
||||||
|
showTitle: index == 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned.fill(
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Left side of the screen
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: () {
|
||||||
|
toPreviousAsset(index);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Right side of the screen
|
||||||
|
Expanded(
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.translucent,
|
||||||
|
onTap: () {
|
||||||
|
toNextAsset(index);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
top: 8,
|
||||||
|
left: 8,
|
||||||
|
child: MaterialButton(
|
||||||
|
minWidth: 0,
|
||||||
|
onPressed: () {
|
||||||
|
// auto_route doesn't invoke pop scope, so
|
||||||
|
// turn off full screen mode here
|
||||||
|
// https://github.com/Milad-Akarie/auto_route_library/issues/1799
|
||||||
|
context.maybePop();
|
||||||
|
SystemChrome.setEnabledSystemUIMode(
|
||||||
|
SystemUiMode.edgeToEdge,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
|
elevation: 0,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.close_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (currentAsset.value != null &&
|
||||||
|
currentAsset.value!.isVideo)
|
||||||
|
Positioned(
|
||||||
|
bottom: 24,
|
||||||
|
right: 32,
|
||||||
|
child: Icon(
|
||||||
|
Icons.videocam_outlined,
|
||||||
|
color: Colors.grey[200],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DriftMemoryBottomInfo(
|
||||||
|
memory: memories[mIndex],
|
||||||
|
title: title,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ class ArchiveActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).archive(source);
|
final result = await ref.read(actionProvider.notifier).archive(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'archive_action_prompt'.t(
|
final successMessage = 'archive_action_prompt'.t(
|
||||||
|
@ -9,17 +9,33 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
this.onPressed,
|
this.onPressed,
|
||||||
this.onLongPressed,
|
this.onLongPressed,
|
||||||
this.maxWidth = 90.0,
|
this.maxWidth = 90.0,
|
||||||
|
this.minWidth,
|
||||||
|
this.menuItem = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final IconData iconData;
|
final IconData iconData;
|
||||||
final double maxWidth;
|
final double maxWidth;
|
||||||
|
final double? minWidth;
|
||||||
|
final bool menuItem;
|
||||||
final void Function()? onPressed;
|
final void Function()? onPressed;
|
||||||
final void Function()? onLongPressed;
|
final void Function()? onLongPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final minWidth = context.isMobile ? context.width / 4.5 : 75.0;
|
final miniWidth =
|
||||||
|
minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
|
||||||
|
final iconTheme = IconTheme.of(context);
|
||||||
|
final iconSize = iconTheme.size ?? 24.0;
|
||||||
|
final iconColor = iconTheme.color ?? context.themeData.iconTheme.color;
|
||||||
|
final textColor = context.themeData.textTheme.labelLarge?.color;
|
||||||
|
|
||||||
|
if (menuItem) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: Icon(iconData, size: iconSize, color: iconColor),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return ConstrainedBox(
|
return ConstrainedBox(
|
||||||
constraints: BoxConstraints(
|
constraints: BoxConstraints(
|
||||||
@ -30,19 +46,22 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||||
),
|
),
|
||||||
|
textColor: textColor,
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
onLongPress: onLongPressed,
|
onLongPress: onLongPressed,
|
||||||
minWidth: minWidth,
|
minWidth: miniWidth,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Icon(iconData, size: 24),
|
Icon(iconData, size: iconSize, color: iconColor),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style:
|
style: const TextStyle(
|
||||||
const TextStyle(fontSize: 14.0, fontWeight: FontWeight.w400),
|
fontSize: 14.0,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
),
|
||||||
maxLines: 3,
|
maxLines: 3,
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
|
@ -1,10 +1,42 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class DeletePermanentActionButton extends ConsumerWidget {
|
class DeletePermanentActionButton extends ConsumerWidget {
|
||||||
const DeletePermanentActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const DeletePermanentActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).delete(source);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'delete_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@ -12,6 +44,7 @@ class DeletePermanentActionButton extends ConsumerWidget {
|
|||||||
maxWidth: 110.0,
|
maxWidth: 110.0,
|
||||||
iconData: Icons.delete_forever,
|
iconData: Icons.delete_forever,
|
||||||
label: "delete_dialog_title".t(context: context),
|
label: "delete_dialog_title".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,18 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class FavoriteActionButton extends ConsumerWidget {
|
class FavoriteActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
final bool menuItem;
|
||||||
|
|
||||||
const FavoriteActionButton({super.key, required this.source});
|
const FavoriteActionButton({
|
||||||
|
super.key,
|
||||||
|
required this.source,
|
||||||
|
this.menuItem = false,
|
||||||
|
});
|
||||||
|
|
||||||
void _onTap(BuildContext context, WidgetRef ref) async {
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
if (!context.mounted) {
|
if (!context.mounted) {
|
||||||
@ -20,7 +24,11 @@ class FavoriteActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).favorite(source);
|
final result = await ref.read(actionProvider.notifier).favorite(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
|
if (source == ActionSource.viewer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'favorite_action_prompt'.t(
|
final successMessage = 'favorite_action_prompt'.t(
|
||||||
@ -45,6 +53,7 @@ class FavoriteActionButton extends ConsumerWidget {
|
|||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.favorite_border_rounded,
|
iconData: Icons.favorite_border_rounded,
|
||||||
label: "favorite".t(context: context),
|
label: "favorite".t(context: context),
|
||||||
|
menuItem: menuItem,
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -21,7 +20,6 @@ class MoveToLockFolderActionButton extends ConsumerWidget {
|
|||||||
|
|
||||||
final result =
|
final result =
|
||||||
await ref.read(actionProvider.notifier).moveToLockFolder(source);
|
await ref.read(actionProvider.notifier).moveToLockFolder(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'move_to_lock_folder_action_prompt'.t(
|
final successMessage = 'move_to_lock_folder_action_prompt'.t(
|
||||||
|
@ -5,7 +5,6 @@ import 'package:immich_mobile/constants/enums.dart';
|
|||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
@ -21,7 +20,6 @@ class RemoveFromLockFolderActionButton extends ConsumerWidget {
|
|||||||
|
|
||||||
final result =
|
final result =
|
||||||
await ref.read(actionProvider.notifier).removeFromLockFolder(source);
|
await ref.read(actionProvider.notifier).removeFromLockFolder(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'remove_from_lock_folder_action_prompt'.t(
|
final successMessage = 'remove_from_lock_folder_action_prompt'.t(
|
||||||
|
@ -1,17 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
|
||||||
|
|
||||||
class TrashActionButton extends ConsumerWidget {
|
|
||||||
const TrashActionButton({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return BaseActionButton(
|
|
||||||
maxWidth: 85.0,
|
|
||||||
iconData: Icons.delete_outline_rounded,
|
|
||||||
label: "control_bottom_app_bar_trash_from_immich".t(context: context),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,50 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
|
class TrashActionButton extends ConsumerWidget {
|
||||||
|
final ActionSource source;
|
||||||
|
|
||||||
|
const TrashActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).trash(source);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'trash_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return BaseActionButton(
|
||||||
|
maxWidth: 85.0,
|
||||||
|
iconData: Icons.delete_outline_rounded,
|
||||||
|
label: "control_bottom_app_bar_trash_from_immich".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,49 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class UnarchiveActionButton extends ConsumerWidget {
|
class UnarchiveActionButton extends ConsumerWidget {
|
||||||
const UnarchiveActionButton({super.key});
|
final ActionSource source;
|
||||||
|
|
||||||
|
const UnarchiveActionButton({super.key, required this.source});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).unArchive(source);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'unarchive_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.unarchive_outlined,
|
iconData: Icons.unarchive_outlined,
|
||||||
label: "unarchive".t(context: context),
|
label: "unarchive".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,60 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/action.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
|
|
||||||
class UnFavoriteActionButton extends ConsumerWidget {
|
class UnFavoriteActionButton extends ConsumerWidget {
|
||||||
const UnFavoriteActionButton({super.key});
|
final ActionSource source;
|
||||||
|
final bool menuItem;
|
||||||
|
|
||||||
|
const UnFavoriteActionButton({
|
||||||
|
super.key,
|
||||||
|
required this.source,
|
||||||
|
this.menuItem = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
void _onTap(BuildContext context, WidgetRef ref) async {
|
||||||
|
if (!context.mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = await ref.read(actionProvider.notifier).unFavorite(source);
|
||||||
|
|
||||||
|
if (source == ActionSource.viewer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
|
final successMessage = 'unfavorite_action_prompt'.t(
|
||||||
|
context: context,
|
||||||
|
args: {'count': result.count.toString()},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ImmichToast.show(
|
||||||
|
context: context,
|
||||||
|
msg: result.success
|
||||||
|
? successMessage
|
||||||
|
: 'scaffold_body_error_occurred'.t(context: context),
|
||||||
|
gravity: ToastGravity.BOTTOM,
|
||||||
|
toastType: result.success ? ToastType.success : ToastType.error,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.favorite_rounded,
|
iconData: Icons.favorite_rounded,
|
||||||
label: "unfavorite".t(context: context),
|
label: "unfavorite".t(context: context),
|
||||||
|
onPressed: () => _onTap(context, ref),
|
||||||
|
menuItem: menuItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,529 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_bar.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/top_app_bar.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/photo_view/photo_view.dart';
|
||||||
|
import 'package:immich_mobile/widgets/photo_view/photo_view_gallery.dart';
|
||||||
|
import 'package:platform/platform.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class AssetViewerPage extends StatelessWidget {
|
||||||
|
final int initialIndex;
|
||||||
|
final TimelineService timelineService;
|
||||||
|
|
||||||
|
const AssetViewerPage({
|
||||||
|
super.key,
|
||||||
|
required this.initialIndex,
|
||||||
|
required this.timelineService,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// This is necessary to ensure that the timeline service is available
|
||||||
|
// since the Timeline and AssetViewer are on different routes / Widget subtrees.
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [timelineServiceProvider.overrideWithValue(timelineService)],
|
||||||
|
child: AssetViewer(initialIndex: initialIndex),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetViewer extends ConsumerStatefulWidget {
|
||||||
|
final int initialIndex;
|
||||||
|
final Platform? platform;
|
||||||
|
|
||||||
|
const AssetViewer({
|
||||||
|
super.key,
|
||||||
|
required this.initialIndex,
|
||||||
|
this.platform,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState createState() => _AssetViewerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
const double _kBottomSheetMinimumExtent = 0.4;
|
||||||
|
const double _kBottomSheetSnapExtent = 0.7;
|
||||||
|
|
||||||
|
class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
|
late PageController pageController;
|
||||||
|
late DraggableScrollableController bottomSheetController;
|
||||||
|
PersistentBottomSheetController? sheetCloseController;
|
||||||
|
// PhotoViewGallery takes care of disposing it's controllers
|
||||||
|
PhotoViewControllerBase? viewController;
|
||||||
|
StreamSubscription? reloadSubscription;
|
||||||
|
|
||||||
|
late Platform platform;
|
||||||
|
late PhotoViewControllerValue initialPhotoViewState;
|
||||||
|
bool? hasDraggedDown;
|
||||||
|
bool isSnapping = false;
|
||||||
|
bool blockGestures = false;
|
||||||
|
bool dragInProgress = false;
|
||||||
|
bool shouldPopOnDrag = false;
|
||||||
|
double? initialScale;
|
||||||
|
double previousExtent = _kBottomSheetMinimumExtent;
|
||||||
|
Offset dragDownPosition = Offset.zero;
|
||||||
|
int totalAssets = 0;
|
||||||
|
BuildContext? scaffoldContext;
|
||||||
|
|
||||||
|
// Delayed operations that should be cancelled on disposal
|
||||||
|
final List<Timer> _delayedOperations = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
pageController = PageController(initialPage: widget.initialIndex);
|
||||||
|
platform = widget.platform ?? const LocalPlatform();
|
||||||
|
totalAssets = ref.read(timelineServiceProvider).totalAssets;
|
||||||
|
bottomSheetController = DraggableScrollableController();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_onAssetChanged(widget.initialIndex);
|
||||||
|
});
|
||||||
|
reloadSubscription = EventStream.shared.listen(_onEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
pageController.dispose();
|
||||||
|
bottomSheetController.dispose();
|
||||||
|
_cancelTimers();
|
||||||
|
reloadSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get showingBottomSheet =>
|
||||||
|
ref.read(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||||
|
|
||||||
|
Color get backgroundColor {
|
||||||
|
final opacity =
|
||||||
|
ref.read(assetViewerProvider.select((s) => s.backgroundOpacity));
|
||||||
|
return Colors.black.withAlpha(opacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelTimers() {
|
||||||
|
for (final timer in _delayedOperations) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
_delayedOperations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is used to calculate the scale of the asset when the bottom sheet is showing.
|
||||||
|
// It is a small increment to ensure that the asset is slightly zoomed in when the
|
||||||
|
// bottom sheet is showing, which emulates the zoom effect.
|
||||||
|
double get _getScaleForBottomSheet =>
|
||||||
|
(viewController?.prevValue.scale ?? viewController?.value.scale ?? 1.0) +
|
||||||
|
0.01;
|
||||||
|
|
||||||
|
double _getVerticalOffsetForBottomSheet(double extent) =>
|
||||||
|
(context.height * extent) - (context.height * _kBottomSheetMinimumExtent);
|
||||||
|
|
||||||
|
Future<void> _precacheImage(int index) async {
|
||||||
|
if (!mounted || index < 0 || index >= totalAssets) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
|
final screenSize = Size(context.width, context.height);
|
||||||
|
|
||||||
|
// Precache both thumbnail and full image for smooth transitions
|
||||||
|
unawaited(
|
||||||
|
Future.wait([
|
||||||
|
precacheImage(
|
||||||
|
getThumbnailImageProvider(asset: asset, size: screenSize),
|
||||||
|
context,
|
||||||
|
onError: (_, __) {},
|
||||||
|
),
|
||||||
|
precacheImage(
|
||||||
|
getFullImageProvider(asset, size: screenSize),
|
||||||
|
context,
|
||||||
|
onError: (_, __) {},
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onAssetChanged(int index) {
|
||||||
|
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
|
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
||||||
|
unawaited(ref.read(timelineServiceProvider).preCacheAssets(index));
|
||||||
|
_cancelTimers();
|
||||||
|
// This will trigger the pre-caching of adjacent assets ensuring
|
||||||
|
// that they are ready when the user navigates to them.
|
||||||
|
final timer = Timer(Durations.medium4, () {
|
||||||
|
// Check if widget is still mounted before proceeding
|
||||||
|
if (!mounted) return;
|
||||||
|
|
||||||
|
for (final offset in [-1, 1]) {
|
||||||
|
unawaited(_precacheImage(index + offset));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_delayedOperations.add(timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPageBuild(PhotoViewControllerBase controller) {
|
||||||
|
viewController ??= controller;
|
||||||
|
if (showingBottomSheet) {
|
||||||
|
final verticalOffset = (context.height * bottomSheetController.size) -
|
||||||
|
(context.height * _kBottomSheetMinimumExtent);
|
||||||
|
controller.position = Offset(0, -verticalOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPageChanged(int index, PhotoViewControllerBase? controller) {
|
||||||
|
_onAssetChanged(index);
|
||||||
|
viewController = controller;
|
||||||
|
|
||||||
|
// If the bottom sheet is showing, we need to adjust scale the asset to
|
||||||
|
// emulate the zoom effect
|
||||||
|
if (showingBottomSheet) {
|
||||||
|
initialScale = controller?.scale;
|
||||||
|
controller?.scale = _getScaleForBottomSheet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragStart(
|
||||||
|
_,
|
||||||
|
DragStartDetails details,
|
||||||
|
PhotoViewControllerBase controller,
|
||||||
|
PhotoViewScaleStateController scaleStateController,
|
||||||
|
) {
|
||||||
|
viewController = controller;
|
||||||
|
dragDownPosition = details.localPosition;
|
||||||
|
initialPhotoViewState = controller.value;
|
||||||
|
final isZoomed =
|
||||||
|
scaleStateController.scaleState == PhotoViewScaleState.zoomedIn ||
|
||||||
|
scaleStateController.scaleState == PhotoViewScaleState.covering;
|
||||||
|
if (!showingBottomSheet && isZoomed) {
|
||||||
|
blockGestures = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragEnd(BuildContext ctx, _, __) {
|
||||||
|
dragInProgress = false;
|
||||||
|
|
||||||
|
if (shouldPopOnDrag) {
|
||||||
|
// Dismiss immediately without state updates to avoid rebuilds
|
||||||
|
ctx.maybePop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not reset the state if the bottom sheet is showing
|
||||||
|
if (showingBottomSheet) {
|
||||||
|
_snapBottomSheet();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the gestures are blocked, do not reset the state
|
||||||
|
if (blockGestures) {
|
||||||
|
blockGestures = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldPopOnDrag = false;
|
||||||
|
hasDraggedDown = null;
|
||||||
|
viewController?.animateMultiple(
|
||||||
|
position: initialPhotoViewState.position,
|
||||||
|
scale: initialPhotoViewState.scale,
|
||||||
|
rotation: initialPhotoViewState.rotation,
|
||||||
|
);
|
||||||
|
ref.read(assetViewerProvider.notifier).setOpacity(255);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDragUpdate(BuildContext ctx, DragUpdateDetails details, _) {
|
||||||
|
if (blockGestures) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dragInProgress = true;
|
||||||
|
final delta = details.localPosition - dragDownPosition;
|
||||||
|
hasDraggedDown ??= delta.dy > 0;
|
||||||
|
if (!hasDraggedDown! || showingBottomSheet) {
|
||||||
|
_handleDragUp(ctx, delta);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleDragDown(ctx, delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragUp(BuildContext ctx, Offset delta) {
|
||||||
|
const double openThreshold = 50;
|
||||||
|
|
||||||
|
final position = initialPhotoViewState.position + Offset(0, delta.dy);
|
||||||
|
final distanceToOrigin = position.distance;
|
||||||
|
|
||||||
|
viewController?.updateMultiple(position: position);
|
||||||
|
// Moves the bottom sheet when the asset is being dragged up
|
||||||
|
if (showingBottomSheet && bottomSheetController.isAttached) {
|
||||||
|
final centre = (ctx.height * _kBottomSheetMinimumExtent);
|
||||||
|
bottomSheetController.jumpTo((centre + distanceToOrigin) / ctx.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (distanceToOrigin > openThreshold && !showingBottomSheet) {
|
||||||
|
_openBottomSheet(ctx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragDown(BuildContext ctx, Offset delta) {
|
||||||
|
const double dragRatio = 0.2;
|
||||||
|
const double popThreshold = 75;
|
||||||
|
|
||||||
|
final distance = delta.distance;
|
||||||
|
shouldPopOnDrag = delta.dy > 0 && distance > popThreshold;
|
||||||
|
|
||||||
|
final maxScaleDistance = ctx.height * 0.5;
|
||||||
|
final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio);
|
||||||
|
double? updatedScale;
|
||||||
|
if (initialPhotoViewState.scale != null) {
|
||||||
|
updatedScale = initialPhotoViewState.scale! * (1.0 - scaleReduction);
|
||||||
|
}
|
||||||
|
|
||||||
|
final backgroundOpacity =
|
||||||
|
(255 * (1.0 - (scaleReduction / dragRatio))).round();
|
||||||
|
|
||||||
|
viewController?.updateMultiple(
|
||||||
|
position: initialPhotoViewState.position + delta,
|
||||||
|
scale: updatedScale,
|
||||||
|
);
|
||||||
|
ref.read(assetViewerProvider.notifier).setOpacity(backgroundOpacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTapDown(_, __, ___) {
|
||||||
|
if (!showingBottomSheet) {
|
||||||
|
ref.read(assetViewerProvider.notifier).toggleControls();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _onNotification(Notification delta) {
|
||||||
|
if (delta is DraggableScrollableNotification) {
|
||||||
|
_handleDraggableNotification(delta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle sheet snap manually so that the it snaps only at _kBottomSheetSnapExtent but not after
|
||||||
|
// the isSnapping guard is to prevent the notification from recursively handling the
|
||||||
|
// notification, eventually resulting in a heap overflow
|
||||||
|
if (!isSnapping && delta is ScrollEndNotification) {
|
||||||
|
_snapBottomSheet();
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDraggableNotification(DraggableScrollableNotification delta) {
|
||||||
|
final currentExtent = delta.extent;
|
||||||
|
final isDraggingDown = currentExtent < previousExtent;
|
||||||
|
previousExtent = currentExtent;
|
||||||
|
// Closes the bottom sheet if the user is dragging down
|
||||||
|
if (isDraggingDown && delta.extent < 0.5) {
|
||||||
|
if (dragInProgress) {
|
||||||
|
blockGestures = true;
|
||||||
|
}
|
||||||
|
sheetCloseController?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the asset is being dragged down, we do not want to update the asset position again
|
||||||
|
if (dragInProgress) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final verticalOffset = _getVerticalOffsetForBottomSheet(delta.extent);
|
||||||
|
// Moves the asset when the bottom sheet is being dragged
|
||||||
|
if (verticalOffset > 0) {
|
||||||
|
viewController?.position = Offset(0, -verticalOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEvent(Event event) {
|
||||||
|
if (event is TimelineReloadEvent) {
|
||||||
|
_onTimelineReload(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event is ViewerOpenBottomSheetEvent) {
|
||||||
|
final extent = _kBottomSheetMinimumExtent + 0.3;
|
||||||
|
_openBottomSheet(scaffoldContext!, extent: extent);
|
||||||
|
final offset = _getVerticalOffsetForBottomSheet(extent);
|
||||||
|
viewController?.position = Offset(0, -offset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTimelineReload(_) {
|
||||||
|
setState(() {
|
||||||
|
totalAssets = ref.read(timelineServiceProvider).totalAssets;
|
||||||
|
if (totalAssets == 0) {
|
||||||
|
context.maybePop();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final index = pageController.page?.round() ?? 0;
|
||||||
|
final newAsset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
|
final currentAsset = ref.read(currentAssetNotifier);
|
||||||
|
// Do not reload / close the bottom sheet if the asset has not changed
|
||||||
|
if (newAsset.heroTag == currentAsset?.heroTag) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_onAssetChanged(pageController.page!.round());
|
||||||
|
sheetCloseController?.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openBottomSheet(
|
||||||
|
BuildContext ctx, {
|
||||||
|
double extent = _kBottomSheetMinimumExtent,
|
||||||
|
}) {
|
||||||
|
ref.read(assetViewerProvider.notifier).setBottomSheet(true);
|
||||||
|
initialScale = viewController?.scale;
|
||||||
|
viewController?.updateMultiple(scale: _getScaleForBottomSheet);
|
||||||
|
previousExtent = _kBottomSheetMinimumExtent;
|
||||||
|
sheetCloseController = showBottomSheet(
|
||||||
|
context: ctx,
|
||||||
|
sheetAnimationStyle: AnimationStyle(
|
||||||
|
duration: Durations.short4,
|
||||||
|
reverseDuration: Durations.short2,
|
||||||
|
),
|
||||||
|
constraints: const BoxConstraints(maxWidth: double.infinity),
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20.0)),
|
||||||
|
),
|
||||||
|
backgroundColor: ctx.colorScheme.surfaceContainerLowest,
|
||||||
|
builder: (_) {
|
||||||
|
return NotificationListener<Notification>(
|
||||||
|
onNotification: _onNotification,
|
||||||
|
child: AssetDetailBottomSheet(
|
||||||
|
controller: bottomSheetController,
|
||||||
|
initialChildSize: extent,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
sheetCloseController?.closed.then((_) => _handleSheetClose());
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleSheetClose() {
|
||||||
|
viewController?.animateMultiple(position: Offset.zero);
|
||||||
|
viewController?.updateMultiple(scale: initialScale);
|
||||||
|
ref.read(assetViewerProvider.notifier).setBottomSheet(false);
|
||||||
|
sheetCloseController = null;
|
||||||
|
shouldPopOnDrag = false;
|
||||||
|
hasDraggedDown = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _snapBottomSheet() {
|
||||||
|
if (bottomSheetController.size > _kBottomSheetSnapExtent ||
|
||||||
|
bottomSheetController.size < 0.4) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isSnapping = true;
|
||||||
|
bottomSheetController.animateTo(
|
||||||
|
_kBottomSheetSnapExtent,
|
||||||
|
duration: Durations.short3,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _placeholderBuilder(
|
||||||
|
BuildContext ctx,
|
||||||
|
ImageChunkEvent? progress,
|
||||||
|
int index,
|
||||||
|
) {
|
||||||
|
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
color: backgroundColor,
|
||||||
|
child: Thumbnail(
|
||||||
|
asset: asset,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
size: Size(
|
||||||
|
ctx.width,
|
||||||
|
ctx.height,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
|
||||||
|
scaffoldContext ??= ctx;
|
||||||
|
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
|
final size = Size(ctx.width, ctx.height);
|
||||||
|
|
||||||
|
return PhotoViewGalleryPageOptions(
|
||||||
|
key: ValueKey(asset.heroTag),
|
||||||
|
imageProvider: getFullImageProvider(asset, size: size),
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag),
|
||||||
|
filterQuality: FilterQuality.high,
|
||||||
|
tightMode: true,
|
||||||
|
initialScale: PhotoViewComputedScale.contained * 0.999,
|
||||||
|
minScale: PhotoViewComputedScale.contained * 0.999,
|
||||||
|
disableScaleGestures: showingBottomSheet,
|
||||||
|
onDragStart: _onDragStart,
|
||||||
|
onDragUpdate: _onDragUpdate,
|
||||||
|
onDragEnd: _onDragEnd,
|
||||||
|
onTapDown: _onTapDown,
|
||||||
|
errorBuilder: (_, __, ___) => Container(
|
||||||
|
width: ctx.width,
|
||||||
|
height: ctx.height,
|
||||||
|
color: backgroundColor,
|
||||||
|
child: Thumbnail(
|
||||||
|
asset: asset,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
size: size,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPop<T>(bool didPop, T? result) {
|
||||||
|
ref.read(currentAssetNotifier.notifier).dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Rebuild the widget when the asset viewer state changes
|
||||||
|
// Using multiple selectors to avoid unnecessary rebuilds for other state changes
|
||||||
|
ref.watch(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||||
|
ref.watch(assetViewerProvider.select((s) => s.backgroundOpacity));
|
||||||
|
|
||||||
|
// Currently it is not possible to scroll the asset when the bottom sheet is open all the way.
|
||||||
|
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||||
|
// TODO: Add a custom scrum builder once the fix lands on stable
|
||||||
|
return PopScope(
|
||||||
|
onPopInvokedWithResult: _onPop,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
appBar: const ViewerTopAppBar(),
|
||||||
|
extendBody: true,
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
|
body: PhotoViewGallery.builder(
|
||||||
|
gaplessPlayback: true,
|
||||||
|
loadingBuilder: _placeholderBuilder,
|
||||||
|
pageController: pageController,
|
||||||
|
scrollPhysics: platform.isIOS
|
||||||
|
? const FastScrollPhysics() // Use bouncing physics for iOS
|
||||||
|
: const FastClampingScrollPhysics() // Use heavy physics for Android
|
||||||
|
,
|
||||||
|
itemCount: totalAssets,
|
||||||
|
onPageChanged: _onPageChanged,
|
||||||
|
onPageBuild: _onPageBuild,
|
||||||
|
builder: _assetBuilder,
|
||||||
|
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||||
|
enablePanAlways: true,
|
||||||
|
),
|
||||||
|
bottomNavigationBar: const ViewerBottomBar(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
class AssetViewerState {
|
||||||
|
final int backgroundOpacity;
|
||||||
|
final bool showingBottomSheet;
|
||||||
|
final bool showingControls;
|
||||||
|
|
||||||
|
const AssetViewerState({
|
||||||
|
this.backgroundOpacity = 255,
|
||||||
|
this.showingBottomSheet = false,
|
||||||
|
this.showingControls = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
AssetViewerState copyWith({
|
||||||
|
int? backgroundOpacity,
|
||||||
|
bool? showingBottomSheet,
|
||||||
|
bool? showingControls,
|
||||||
|
}) {
|
||||||
|
return AssetViewerState(
|
||||||
|
backgroundOpacity: backgroundOpacity ?? this.backgroundOpacity,
|
||||||
|
showingBottomSheet: showingBottomSheet ?? this.showingBottomSheet,
|
||||||
|
showingControls: showingControls ?? this.showingControls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AssetViewerState(opacity: $backgroundOpacity, bottomSheet: $showingBottomSheet, controls: $showingControls)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other.runtimeType != runtimeType) return false;
|
||||||
|
return other is AssetViewerState &&
|
||||||
|
other.backgroundOpacity == backgroundOpacity &&
|
||||||
|
other.showingBottomSheet == showingBottomSheet &&
|
||||||
|
other.showingControls == showingControls;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
backgroundOpacity.hashCode ^
|
||||||
|
showingBottomSheet.hashCode ^
|
||||||
|
showingControls.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetViewerStateNotifier extends AutoDisposeNotifier<AssetViewerState> {
|
||||||
|
@override
|
||||||
|
AssetViewerState build() {
|
||||||
|
return const AssetViewerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setOpacity(int opacity) {
|
||||||
|
state = state.copyWith(
|
||||||
|
backgroundOpacity: opacity,
|
||||||
|
showingControls: opacity == 255 ? true : state.showingControls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setBottomSheet(bool showing) {
|
||||||
|
state = state.copyWith(
|
||||||
|
showingBottomSheet: showing,
|
||||||
|
showingControls: showing ? true : state.showingControls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggleControls() {
|
||||||
|
state = state.copyWith(showingControls: !state.showingControls);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetViewerProvider =
|
||||||
|
AutoDisposeNotifierProvider<AssetViewerStateNotifier, AssetViewerState>(
|
||||||
|
AssetViewerStateNotifier.new,
|
||||||
|
);
|
@ -0,0 +1,93 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
class ViewerBottomBar extends ConsumerWidget {
|
||||||
|
const ViewerBottomBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asset = ref.watch(currentAssetNotifier);
|
||||||
|
if (asset == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||||
|
final isSheetOpen = ref.watch(
|
||||||
|
assetViewerProvider.select((s) => s.showingBottomSheet),
|
||||||
|
);
|
||||||
|
int opacity = ref.watch(
|
||||||
|
assetViewerProvider.select((state) => state.backgroundOpacity),
|
||||||
|
);
|
||||||
|
final showControls =
|
||||||
|
ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||||
|
|
||||||
|
if (!showControls) {
|
||||||
|
opacity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final actions = <Widget>[
|
||||||
|
const ShareActionButton(),
|
||||||
|
const _EditActionButton(),
|
||||||
|
if (asset.hasRemote && isOwner)
|
||||||
|
const ArchiveActionButton(source: ActionSource.viewer),
|
||||||
|
];
|
||||||
|
|
||||||
|
return IgnorePointer(
|
||||||
|
ignoring: opacity < 255,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: opacity / 255,
|
||||||
|
duration: Durations.short2,
|
||||||
|
child: AnimatedSwitcher(
|
||||||
|
duration: Durations.short4,
|
||||||
|
child: isSheetOpen
|
||||||
|
? const SizedBox.shrink()
|
||||||
|
: SafeArea(
|
||||||
|
child: Theme(
|
||||||
|
data: context.themeData.copyWith(
|
||||||
|
iconTheme:
|
||||||
|
const IconThemeData(size: 22, color: Colors.white),
|
||||||
|
textTheme: context.themeData.textTheme.copyWith(
|
||||||
|
labelLarge:
|
||||||
|
context.themeData.textTheme.labelLarge?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
height: 80,
|
||||||
|
color: Colors.black.withAlpha(125),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: actions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditActionButton extends ConsumerWidget {
|
||||||
|
const _EditActionButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return BaseActionButton(
|
||||||
|
iconData: Icons.tune_outlined,
|
||||||
|
label: 'edit'.t(context: context),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,253 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/location_details.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
|
|
||||||
|
const _kSeparator = ' • ';
|
||||||
|
|
||||||
|
class AssetDetailBottomSheet extends ConsumerWidget {
|
||||||
|
final DraggableScrollableController? controller;
|
||||||
|
final double initialChildSize;
|
||||||
|
|
||||||
|
const AssetDetailBottomSheet({
|
||||||
|
this.controller,
|
||||||
|
this.initialChildSize = 0.35,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asset = ref.watch(currentAssetNotifier);
|
||||||
|
if (asset == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final isTrashEnable = ref.watch(
|
||||||
|
serverInfoProvider.select((state) => state.serverFeatures.trash),
|
||||||
|
);
|
||||||
|
|
||||||
|
final actions = <Widget>[
|
||||||
|
const ShareActionButton(),
|
||||||
|
if (asset.hasRemote) ...[
|
||||||
|
const ShareLinkActionButton(source: ActionSource.viewer),
|
||||||
|
const ArchiveActionButton(source: ActionSource.viewer),
|
||||||
|
if (!asset.hasLocal) const DownloadActionButton(),
|
||||||
|
isTrashEnable
|
||||||
|
? const TrashActionButton(source: ActionSource.viewer)
|
||||||
|
: const DeletePermanentActionButton(source: ActionSource.viewer),
|
||||||
|
const MoveToLockFolderActionButton(
|
||||||
|
source: ActionSource.viewer,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (asset.storage == AssetState.local) ...[
|
||||||
|
const DeleteLocalActionButton(),
|
||||||
|
const UploadActionButton(),
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return BaseBottomSheet(
|
||||||
|
actions: actions,
|
||||||
|
slivers: const [_AssetDetailBottomSheet()],
|
||||||
|
controller: controller,
|
||||||
|
initialChildSize: initialChildSize,
|
||||||
|
minChildSize: 0.1,
|
||||||
|
maxChildSize: 0.88,
|
||||||
|
expand: false,
|
||||||
|
shouldCloseOnMinExtent: false,
|
||||||
|
resizeOnScroll: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||||
|
const _AssetDetailBottomSheet();
|
||||||
|
|
||||||
|
String _getDateTime(BuildContext ctx, BaseAsset asset) {
|
||||||
|
final dateTime = asset.createdAt.toLocal();
|
||||||
|
final date = DateFormat.yMMMEd(ctx.locale.toLanguageTag()).format(dateTime);
|
||||||
|
final time = DateFormat.jm(ctx.locale.toLanguageTag()).format(dateTime);
|
||||||
|
return '$date$_kSeparator$time';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getFileInfo(BaseAsset asset, ExifInfo? exifInfo) {
|
||||||
|
final height = asset.height ?? exifInfo?.height;
|
||||||
|
final width = asset.width ?? exifInfo?.width;
|
||||||
|
final resolution =
|
||||||
|
(width != null && height != null) ? "$width x $height" : null;
|
||||||
|
final fileSize =
|
||||||
|
exifInfo?.fileSize != null ? formatBytes(exifInfo!.fileSize!) : null;
|
||||||
|
|
||||||
|
return switch ((fileSize, resolution)) {
|
||||||
|
(null, null) => '',
|
||||||
|
(String fileSize, null) => fileSize,
|
||||||
|
(null, String resolution) => resolution,
|
||||||
|
(String fileSize, String resolution) =>
|
||||||
|
'$fileSize$_kSeparator$resolution',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _getCameraInfoTitle(ExifInfo? exifInfo) {
|
||||||
|
if (exifInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch ((exifInfo.make, exifInfo.model)) {
|
||||||
|
(null, null) => null,
|
||||||
|
(String make, null) => make,
|
||||||
|
(null, String model) => model,
|
||||||
|
(String make, String model) => '$make $model',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _getCameraInfoSubtitle(ExifInfo? exifInfo) {
|
||||||
|
if (exifInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final fNumber =
|
||||||
|
exifInfo.fNumber.isNotEmpty ? 'ƒ/${exifInfo.fNumber}' : null;
|
||||||
|
final exposureTime =
|
||||||
|
exifInfo.exposureTime.isNotEmpty ? exifInfo.exposureTime : null;
|
||||||
|
final focalLength =
|
||||||
|
exifInfo.focalLength.isNotEmpty ? '${exifInfo.focalLength} mm' : null;
|
||||||
|
final iso = exifInfo.iso != null ? 'ISO ${exifInfo.iso}' : null;
|
||||||
|
|
||||||
|
return [fNumber, exposureTime, focalLength, iso]
|
||||||
|
.where((spec) => spec != null && spec.isNotEmpty)
|
||||||
|
.join(_kSeparator);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asset = ref.watch(currentAssetNotifier);
|
||||||
|
if (asset == null) {
|
||||||
|
return const SliverToBoxAdapter(child: SizedBox.shrink());
|
||||||
|
}
|
||||||
|
|
||||||
|
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
||||||
|
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
||||||
|
|
||||||
|
return SliverList.list(
|
||||||
|
children: [
|
||||||
|
// Asset Date and Time
|
||||||
|
_SheetTile(
|
||||||
|
title: _getDateTime(context, asset),
|
||||||
|
titleStyle: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SheetLocationDetails(),
|
||||||
|
// Details header
|
||||||
|
_SheetTile(
|
||||||
|
title: 'exif_bottom_sheet_details'.t(context: context),
|
||||||
|
titleStyle: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
// File info
|
||||||
|
_SheetTile(
|
||||||
|
title: asset.name,
|
||||||
|
titleStyle: context.textTheme.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
leading: Icon(
|
||||||
|
asset.isImage ? Icons.image_outlined : Icons.videocam_outlined,
|
||||||
|
size: 30,
|
||||||
|
color: context.textTheme.labelLarge?.color,
|
||||||
|
),
|
||||||
|
subtitle: _getFileInfo(asset, exifInfo),
|
||||||
|
subtitleStyle: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.textTheme.labelLarge?.color?.withAlpha(200),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Camera info
|
||||||
|
if (cameraTitle != null)
|
||||||
|
_SheetTile(
|
||||||
|
title: cameraTitle,
|
||||||
|
titleStyle: context.textTheme.labelLarge
|
||||||
|
?.copyWith(fontWeight: FontWeight.w600),
|
||||||
|
leading: Icon(
|
||||||
|
Icons.camera_outlined,
|
||||||
|
size: 30,
|
||||||
|
color: context.textTheme.labelLarge?.color,
|
||||||
|
),
|
||||||
|
subtitle: _getCameraInfoSubtitle(exifInfo),
|
||||||
|
subtitleStyle: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.textTheme.labelLarge?.color?.withAlpha(200),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SheetTile extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final Widget? leading;
|
||||||
|
final String? subtitle;
|
||||||
|
final TextStyle? titleStyle;
|
||||||
|
final TextStyle? subtitleStyle;
|
||||||
|
|
||||||
|
const _SheetTile({
|
||||||
|
required this.title,
|
||||||
|
this.titleStyle,
|
||||||
|
this.leading,
|
||||||
|
this.subtitle,
|
||||||
|
this.subtitleStyle,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Widget titleWidget;
|
||||||
|
if (leading == null) {
|
||||||
|
titleWidget = LimitedBox(
|
||||||
|
maxWidth: double.infinity,
|
||||||
|
child: Text(title, style: titleStyle),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
titleWidget = Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.only(left: 15),
|
||||||
|
child: Text(title, style: titleStyle),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Widget? subtitleWidget;
|
||||||
|
if (leading == null && subtitle != null) {
|
||||||
|
subtitleWidget = Text(subtitle!, style: subtitleStyle);
|
||||||
|
} else if (leading != null && subtitle != null) {
|
||||||
|
subtitleWidget = Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 15),
|
||||||
|
child: Text(subtitle!, style: subtitleStyle),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
subtitleWidget = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
title: titleWidget,
|
||||||
|
titleAlignment: ListTileTitleAlignment.center,
|
||||||
|
leading: leading,
|
||||||
|
contentPadding: leading == null ? null : const EdgeInsets.only(left: 25),
|
||||||
|
subtitle: subtitleWidget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart';
|
||||||
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
|
class SheetLocationDetails extends ConsumerStatefulWidget {
|
||||||
|
const SheetLocationDetails({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState createState() => _SheetLocationDetailsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SheetLocationDetailsState extends ConsumerState<SheetLocationDetails> {
|
||||||
|
BaseAsset? asset;
|
||||||
|
ExifInfo? exifInfo;
|
||||||
|
MapLibreMapController? _mapController;
|
||||||
|
|
||||||
|
String? _getLocationName(ExifInfo? exifInfo) {
|
||||||
|
if (exifInfo == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final cityName = exifInfo.city;
|
||||||
|
final stateName = exifInfo.state;
|
||||||
|
|
||||||
|
if (cityName != null && stateName != null) {
|
||||||
|
return "$cityName, $stateName";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onMapCreated(MapLibreMapController controller) {
|
||||||
|
_mapController = controller;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onExifChanged(
|
||||||
|
AsyncValue<ExifInfo?>? previous,
|
||||||
|
AsyncValue<ExifInfo?> current,
|
||||||
|
) {
|
||||||
|
asset = ref.read(currentAssetNotifier);
|
||||||
|
setState(() {
|
||||||
|
exifInfo = current.valueOrNull;
|
||||||
|
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
|
||||||
|
if (exifInfo != null && hasCoordinates) {
|
||||||
|
_mapController?.moveCamera(
|
||||||
|
CameraUpdate.newLatLng(
|
||||||
|
LatLng(exifInfo!.latitude!, exifInfo!.longitude!),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
ref.listenManual(
|
||||||
|
currentAssetExifProvider,
|
||||||
|
_onExifChanged,
|
||||||
|
fireImmediately: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final hasCoordinates = exifInfo?.hasCoordinates ?? false;
|
||||||
|
|
||||||
|
// Guard no lat/lng
|
||||||
|
if (!hasCoordinates ||
|
||||||
|
(asset is LocalAsset && !(asset as LocalAsset).hasRemote)) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final remoteId = asset is LocalAsset
|
||||||
|
? (asset as LocalAsset).remoteId
|
||||||
|
: (asset as RemoteAsset).id;
|
||||||
|
final locationName = _getLocationName(exifInfo);
|
||||||
|
final coordinates =
|
||||||
|
"${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}";
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: 16.0,
|
||||||
|
horizontal: context.isMobile ? 16.0 : 56.0,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Text(
|
||||||
|
"exif_bottom_sheet_location".t(context: context),
|
||||||
|
style: context.textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ExifMap(
|
||||||
|
exifInfo: exifInfo!,
|
||||||
|
markerId: remoteId,
|
||||||
|
onMapCreated: _onMapCreated,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
if (locationName != null)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4.0),
|
||||||
|
child: Text(
|
||||||
|
locationName,
|
||||||
|
style: context.textTheme.bodyLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
coordinates,
|
||||||
|
style: context.textTheme.labelLarge?.copyWith(
|
||||||
|
color: context.textTheme.labelLarge?.color?.withAlpha(150),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
|
const ViewerTopAppBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final asset = ref.watch(currentAssetNotifier);
|
||||||
|
if (asset == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||||
|
|
||||||
|
final isShowingSheet = ref
|
||||||
|
.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
||||||
|
int opacity = ref.watch(
|
||||||
|
assetViewerProvider.select((state) => state.backgroundOpacity),
|
||||||
|
);
|
||||||
|
final showControls =
|
||||||
|
ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||||
|
|
||||||
|
if (!showControls) {
|
||||||
|
opacity = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
final actions = <Widget>[
|
||||||
|
if (asset.hasRemote && isOwner && !asset.isFavorite)
|
||||||
|
const FavoriteActionButton(source: ActionSource.viewer, menuItem: true),
|
||||||
|
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||||
|
const UnFavoriteActionButton(
|
||||||
|
source: ActionSource.viewer,
|
||||||
|
menuItem: true,
|
||||||
|
),
|
||||||
|
const _KebabMenu(),
|
||||||
|
];
|
||||||
|
|
||||||
|
return IgnorePointer(
|
||||||
|
ignoring: opacity < 255,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: opacity / 255,
|
||||||
|
duration: Durations.short2,
|
||||||
|
child: AppBar(
|
||||||
|
backgroundColor:
|
||||||
|
isShowingSheet ? Colors.transparent : Colors.black.withAlpha(125),
|
||||||
|
leading: const _AppBarBackButton(),
|
||||||
|
iconTheme: const IconThemeData(size: 22, color: Colors.white),
|
||||||
|
actionsIconTheme: const IconThemeData(size: 22, color: Colors.white),
|
||||||
|
shape: const Border(),
|
||||||
|
actions: isShowingSheet ? null : actions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => const Size.fromHeight(60.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _KebabMenu extends ConsumerWidget {
|
||||||
|
const _KebabMenu();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
EventStream.shared.emit(const ViewerOpenBottomSheetEvent());
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.more_vert_rounded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppBarBackButton extends ConsumerWidget {
|
||||||
|
const _AppBarBackButton();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isShowingSheet = ref
|
||||||
|
.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
||||||
|
final backgroundColor =
|
||||||
|
isShowingSheet && !context.isDarkTheme ? Colors.white : Colors.black;
|
||||||
|
final foregroundColor =
|
||||||
|
isShowingSheet && !context.isDarkTheme ? Colors.black : Colors.white;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 12.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
iconSize: 22,
|
||||||
|
iconColor: foregroundColor,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
elevation: isShowingSheet ? 4 : 0,
|
||||||
|
),
|
||||||
|
onPressed: context.maybePop,
|
||||||
|
child: const Icon(Icons.arrow_back_rounded),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
|
|
||||||
class BaseBottomSheet extends ConsumerStatefulWidget {
|
class BaseBottomSheet extends ConsumerStatefulWidget {
|
||||||
@ -74,10 +75,7 @@ class _BaseDraggableScrollableSheetState
|
|||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
elevation: 6.0,
|
elevation: 6.0,
|
||||||
shape: const RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.only(
|
borderRadius: BorderRadius.vertical(top: Radius.circular(18)),
|
||||||
topLeft: Radius.circular(6),
|
|
||||||
topRight: Radius.circular(6),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 0),
|
margin: const EdgeInsets.symmetric(horizontal: 0),
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
@ -86,17 +84,22 @@ class _BaseDraggableScrollableSheetState
|
|||||||
SliverToBoxAdapter(
|
SliverToBoxAdapter(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 10),
|
||||||
const _DragHandle(),
|
const _DragHandle(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 14),
|
||||||
|
if (widget.actions.isNotEmpty)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 120,
|
height: 115,
|
||||||
child: ListView(
|
child: ListView(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
children: widget.actions,
|
children: widget.actions,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (widget.actions.isNotEmpty) ...[
|
||||||
|
const Divider(indent: 16, endIndent: 16),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -118,7 +121,7 @@ class _DragHandle extends StatelessWidget {
|
|||||||
height: 6,
|
height: 6,
|
||||||
width: 32,
|
width: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.themeData.dividerColor,
|
color: context.themeData.dividerColor.lighten(amount: 0.6),
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
borderRadius: const BorderRadius.all(Radius.circular(20)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -12,7 +12,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_f
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_link_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/stack_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_buton.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/base_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
@ -39,8 +39,10 @@ class HomeBottomAppBar extends ConsumerWidget {
|
|||||||
const FavoriteActionButton(source: ActionSource.timeline),
|
const FavoriteActionButton(source: ActionSource.timeline),
|
||||||
const DownloadActionButton(),
|
const DownloadActionButton(),
|
||||||
isTrashEnable
|
isTrashEnable
|
||||||
? const TrashActionButton()
|
? const TrashActionButton(source: ActionSource.timeline)
|
||||||
: const DeletePermanentActionButton(),
|
: const DeletePermanentActionButton(
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
),
|
||||||
const EditDateTimeActionButton(),
|
const EditDateTimeActionButton(),
|
||||||
const EditLocationActionButton(source: ActionSource.timeline),
|
const EditLocationActionButton(source: ActionSource.timeline),
|
||||||
const MoveToLockFolderActionButton(
|
const MoveToLockFolderActionButton(
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
||||||
|
import 'package:octo_image/octo_image.dart';
|
||||||
|
|
||||||
|
class FullImage extends StatelessWidget {
|
||||||
|
const FullImage(
|
||||||
|
this.asset, {
|
||||||
|
required this.size,
|
||||||
|
this.fit = BoxFit.cover,
|
||||||
|
this.placeholder = const ThumbnailPlaceholder(),
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BaseAsset asset;
|
||||||
|
final Size size;
|
||||||
|
final Widget? placeholder;
|
||||||
|
final BoxFit fit;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final provider = getFullImageProvider(asset, size: size);
|
||||||
|
return OctoImage(
|
||||||
|
fadeInDuration: const Duration(milliseconds: 0),
|
||||||
|
fadeOutDuration: const Duration(milliseconds: 100),
|
||||||
|
placeholderBuilder: placeholder != null ? (_) => placeholder! : null,
|
||||||
|
image: provider,
|
||||||
|
width: size.width,
|
||||||
|
height: size.height,
|
||||||
|
fit: fit,
|
||||||
|
errorBuilder: (context, error, stackTrace) {
|
||||||
|
provider.evict();
|
||||||
|
return const Icon(Icons.image_not_supported_outlined, size: 32);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
63
mobile/lib/presentation/widgets/images/image_provider.dart
Normal file
63
mobile/lib/presentation/widgets/images/image_provider.dart
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||||
|
|
||||||
|
ImageProvider getFullImageProvider(
|
||||||
|
BaseAsset asset, {
|
||||||
|
Size size = const Size(1080, 1920),
|
||||||
|
}) {
|
||||||
|
// Create new provider and cache it
|
||||||
|
final ImageProvider provider;
|
||||||
|
if (_shouldUseLocalAsset(asset)) {
|
||||||
|
provider = LocalFullImageProvider(asset: asset as LocalAsset, size: size);
|
||||||
|
} else {
|
||||||
|
final String assetId;
|
||||||
|
if (asset is LocalAsset && asset.hasRemote) {
|
||||||
|
assetId = asset.remoteId!;
|
||||||
|
} else if (asset is RemoteAsset) {
|
||||||
|
assetId = asset.id;
|
||||||
|
} else {
|
||||||
|
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
||||||
|
}
|
||||||
|
provider = RemoteFullImageProvider(assetId: assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImageProvider getThumbnailImageProvider({
|
||||||
|
BaseAsset? asset,
|
||||||
|
String? remoteId,
|
||||||
|
Size size = const Size.square(256),
|
||||||
|
}) {
|
||||||
|
assert(
|
||||||
|
asset != null || remoteId != null,
|
||||||
|
'Either asset or remoteId must be provided',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (remoteId != null) {
|
||||||
|
return RemoteThumbProvider(assetId: remoteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_shouldUseLocalAsset(asset!)) {
|
||||||
|
return LocalThumbProvider(asset: asset as LocalAsset, size: size);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String assetId;
|
||||||
|
if (asset is LocalAsset && asset.hasRemote) {
|
||||||
|
assetId = asset.remoteId!;
|
||||||
|
} else if (asset is RemoteAsset) {
|
||||||
|
assetId = asset.id;
|
||||||
|
} else {
|
||||||
|
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return RemoteThumbProvider(assetId: assetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _shouldUseLocalAsset(BaseAsset asset) =>
|
||||||
|
asset is LocalAsset &&
|
||||||
|
(!asset.hasRemote || !AppSetting.get(Setting.preferRemoteImage));
|
241
mobile/lib/presentation/widgets/images/local_image_provider.dart
Normal file
241
mobile/lib/presentation/widgets/images/local_image_provider.dart
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/exceptions/image_loading_exception.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
|
||||||
|
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
||||||
|
final AssetMediaRepository _assetMediaRepository =
|
||||||
|
const AssetMediaRepository();
|
||||||
|
final CacheManager? cacheManager;
|
||||||
|
|
||||||
|
final LocalAsset asset;
|
||||||
|
final Size size;
|
||||||
|
|
||||||
|
const LocalThumbProvider({
|
||||||
|
required this.asset,
|
||||||
|
this.size = const Size.square(kTimelineFixedTileExtent),
|
||||||
|
this.cacheManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LocalThumbProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter loadImage(
|
||||||
|
LocalThumbProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) {
|
||||||
|
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
||||||
|
return MultiFrameImageStreamCompleter(
|
||||||
|
codec: _codec(key, cache, decode),
|
||||||
|
scale: 1.0,
|
||||||
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
|
DiagnosticsProperty<LocalAsset>('Asset', key.asset),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Codec> _codec(
|
||||||
|
LocalThumbProvider key,
|
||||||
|
CacheManager cache,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) async {
|
||||||
|
final cacheKey =
|
||||||
|
'${key.asset.id}-${key.asset.updatedAt}-${key.size.width}x${key.size.height}';
|
||||||
|
|
||||||
|
final fileFromCache = await cache.getFileFromCache(cacheKey);
|
||||||
|
if (fileFromCache != null) {
|
||||||
|
try {
|
||||||
|
final buffer =
|
||||||
|
await ImmutableBuffer.fromFilePath(fileFromCache.file.path);
|
||||||
|
return decode(buffer);
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
final thumbnailBytes =
|
||||||
|
await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size);
|
||||||
|
if (thumbnailBytes == null) {
|
||||||
|
PaintingBinding.instance.imageCache.evict(key);
|
||||||
|
throw StateError(
|
||||||
|
"Loading thumb for local photo ${key.asset.name} failed",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
||||||
|
unawaited(cache.putFile(cacheKey, thumbnailBytes));
|
||||||
|
return decode(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other is LocalThumbProvider) {
|
||||||
|
return asset.id == other.asset.id &&
|
||||||
|
asset.updatedAt == other.asset.updatedAt;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class LocalFullImageProvider extends ImageProvider<LocalFullImageProvider> {
|
||||||
|
final AssetMediaRepository _assetMediaRepository =
|
||||||
|
const AssetMediaRepository();
|
||||||
|
final StorageRepository _storageRepository = const StorageRepository();
|
||||||
|
|
||||||
|
final LocalAsset asset;
|
||||||
|
final Size size;
|
||||||
|
|
||||||
|
const LocalFullImageProvider({
|
||||||
|
required this.asset,
|
||||||
|
required this.size,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<LocalFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter loadImage(
|
||||||
|
LocalFullImageProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) {
|
||||||
|
return MultiImageStreamCompleter(
|
||||||
|
codec: _codec(key, decode),
|
||||||
|
scale: 1.0,
|
||||||
|
informationCollector: () sync* {
|
||||||
|
yield ErrorDescription(asset.name);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Streams in each stage of the image as we ask for it
|
||||||
|
Stream<Codec> _codec(
|
||||||
|
LocalFullImageProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) async* {
|
||||||
|
try {
|
||||||
|
switch (key.asset.type) {
|
||||||
|
case AssetType.image:
|
||||||
|
yield* _decodeProgressive(key, decode);
|
||||||
|
break;
|
||||||
|
case AssetType.video:
|
||||||
|
final codec = await _getThumbnailCodec(key, decode);
|
||||||
|
if (codec == null) {
|
||||||
|
throw StateError("Failed to load preview for ${key.asset.name}");
|
||||||
|
}
|
||||||
|
yield codec;
|
||||||
|
break;
|
||||||
|
case AssetType.other:
|
||||||
|
case AssetType.audio:
|
||||||
|
throw StateError('Unsupported asset type ${key.asset.type}');
|
||||||
|
}
|
||||||
|
} catch (error, stack) {
|
||||||
|
Logger('ImmichLocalImageProvider')
|
||||||
|
.severe('Error loading local image ${key.asset.name}', error, stack);
|
||||||
|
throw const ImageLoadingException(
|
||||||
|
'Could not load image from local storage',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Codec?> _getThumbnailCodec(
|
||||||
|
LocalFullImageProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) async {
|
||||||
|
final thumbBytes =
|
||||||
|
await _assetMediaRepository.getThumbnail(key.asset.id, size: key.size);
|
||||||
|
if (thumbBytes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final buffer = await ImmutableBuffer.fromUint8List(thumbBytes);
|
||||||
|
return decode(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<Codec> _decodeProgressive(
|
||||||
|
LocalFullImageProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) async* {
|
||||||
|
final file = await _storageRepository.getFileForAsset(key.asset);
|
||||||
|
if (file == null) {
|
||||||
|
throw StateError("Opening file for asset ${key.asset.name} failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
final fileSize = await file.length();
|
||||||
|
final devicePixelRatio =
|
||||||
|
PlatformDispatcher.instance.views.first.devicePixelRatio;
|
||||||
|
final isLargeFile = fileSize > 20 * 1024 * 1024; // 20MB
|
||||||
|
final isHEIC = file.path.toLowerCase().contains(RegExp(r'\.(heic|heif)$'));
|
||||||
|
final isProgressive = isLargeFile || (isHEIC && !Platform.isIOS);
|
||||||
|
|
||||||
|
if (isProgressive) {
|
||||||
|
try {
|
||||||
|
final progressiveMultiplier = devicePixelRatio >= 3.0 ? 1.3 : 1.2;
|
||||||
|
final size = Size(
|
||||||
|
(key.size.width * progressiveMultiplier).clamp(256, 1024),
|
||||||
|
(key.size.height * progressiveMultiplier).clamp(256, 1024),
|
||||||
|
);
|
||||||
|
final mediumThumb =
|
||||||
|
await _assetMediaRepository.getThumbnail(key.asset.id, size: size);
|
||||||
|
if (mediumThumb != null) {
|
||||||
|
final mediumBuffer = await ImmutableBuffer.fromUint8List(mediumThumb);
|
||||||
|
yield await decode(mediumBuffer);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load original only when the file is smaller or if the user wants to load original images
|
||||||
|
// Or load a slightly larger image for progressive loading
|
||||||
|
if (isProgressive && !(AppSetting.get(Setting.loadOriginal))) {
|
||||||
|
final progressiveMultiplier = devicePixelRatio >= 3.0 ? 2.0 : 1.6;
|
||||||
|
final size = Size(
|
||||||
|
(key.size.width * progressiveMultiplier).clamp(512, 2048),
|
||||||
|
(key.size.height * progressiveMultiplier).clamp(512, 2048),
|
||||||
|
);
|
||||||
|
final highThumb =
|
||||||
|
await _assetMediaRepository.getThumbnail(key.asset.id, size: size);
|
||||||
|
if (highThumb != null) {
|
||||||
|
final highBuffer = await ImmutableBuffer.fromUint8List(highThumb);
|
||||||
|
yield await decode(highBuffer);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final buffer = await ImmutableBuffer.fromFilePath(file.path);
|
||||||
|
yield await decode(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other is LocalFullImageProvider) {
|
||||||
|
return asset.id == other.asset.id &&
|
||||||
|
asset.updatedAt == other.asset.updatedAt &&
|
||||||
|
size == other.size;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
asset.id.hashCode ^ asset.updatedAt.hashCode ^ size.hashCode;
|
||||||
|
}
|
@ -1,95 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/asset_media.repository.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
|
||||||
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
|
||||||
|
|
||||||
class LocalThumbProvider extends ImageProvider<LocalThumbProvider> {
|
|
||||||
final AssetMediaRepository _assetMediaRepository =
|
|
||||||
const AssetMediaRepository();
|
|
||||||
final CacheManager? cacheManager;
|
|
||||||
|
|
||||||
final LocalAsset asset;
|
|
||||||
final double height;
|
|
||||||
final double width;
|
|
||||||
|
|
||||||
LocalThumbProvider({
|
|
||||||
required this.asset,
|
|
||||||
this.height = kTimelineFixedTileExtent,
|
|
||||||
this.width = kTimelineFixedTileExtent,
|
|
||||||
this.cacheManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<LocalThumbProvider> obtainKey(
|
|
||||||
ImageConfiguration configuration,
|
|
||||||
) {
|
|
||||||
return SynchronousFuture(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ImageStreamCompleter loadImage(
|
|
||||||
LocalThumbProvider key,
|
|
||||||
ImageDecoderCallback decode,
|
|
||||||
) {
|
|
||||||
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
|
||||||
return MultiFrameImageStreamCompleter(
|
|
||||||
codec: _codec(key, cache, decode),
|
|
||||||
scale: 1.0,
|
|
||||||
informationCollector: () => <DiagnosticsNode>[
|
|
||||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
|
||||||
DiagnosticsProperty<LocalAsset>('Asset', key.asset),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Codec> _codec(
|
|
||||||
LocalThumbProvider key,
|
|
||||||
CacheManager cache,
|
|
||||||
ImageDecoderCallback decode,
|
|
||||||
) async {
|
|
||||||
final cacheKey = '${key.asset.id}-${key.asset.updatedAt}-${width}x$height';
|
|
||||||
|
|
||||||
final fileFromCache = await cache.getFileFromCache(cacheKey);
|
|
||||||
if (fileFromCache != null) {
|
|
||||||
try {
|
|
||||||
final buffer =
|
|
||||||
await ImmutableBuffer.fromFilePath(fileFromCache.file.path);
|
|
||||||
return await decode(buffer);
|
|
||||||
} catch (_) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
final thumbnailBytes = await _assetMediaRepository.getThumbnail(
|
|
||||||
key.asset.id,
|
|
||||||
size: Size(key.width, key.height),
|
|
||||||
);
|
|
||||||
if (thumbnailBytes == null) {
|
|
||||||
PaintingBinding.instance.imageCache.evict(key);
|
|
||||||
throw StateError(
|
|
||||||
"Loading thumb for local photo ${key.asset.name} failed",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final buffer = await ImmutableBuffer.fromUint8List(thumbnailBytes);
|
|
||||||
unawaited(cache.putFile(cacheKey, thumbnailBytes));
|
|
||||||
return decode(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
if (other is LocalThumbProvider) {
|
|
||||||
return asset.id == other.asset.id &&
|
|
||||||
asset.updatedAt == other.asset.updatedAt;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => asset.id.hashCode ^ asset.updatedAt.hashCode;
|
|
||||||
}
|
|
@ -0,0 +1,142 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/painting.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/setting.service.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
|
||||||
|
import 'package:immich_mobile/providers/image/cache/remote_image_cache_manager.dart';
|
||||||
|
import 'package:immich_mobile/utils/image_url_builder.dart';
|
||||||
|
|
||||||
|
class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
||||||
|
final String assetId;
|
||||||
|
final CacheManager? cacheManager;
|
||||||
|
|
||||||
|
const RemoteThumbProvider({
|
||||||
|
required this.assetId,
|
||||||
|
this.cacheManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RemoteThumbProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter loadImage(
|
||||||
|
RemoteThumbProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) {
|
||||||
|
final cache = cacheManager ?? RemoteImageCacheManager();
|
||||||
|
final chunkController = StreamController<ImageChunkEvent>();
|
||||||
|
return MultiFrameImageStreamCompleter(
|
||||||
|
codec: _codec(key, cache, decode, chunkController),
|
||||||
|
scale: 1.0,
|
||||||
|
chunkEvents: chunkController.stream,
|
||||||
|
informationCollector: () => <DiagnosticsNode>[
|
||||||
|
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
||||||
|
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Codec> _codec(
|
||||||
|
RemoteThumbProvider key,
|
||||||
|
CacheManager cache,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
StreamController<ImageChunkEvent> chunkController,
|
||||||
|
) async {
|
||||||
|
final preview = getThumbnailUrlForRemoteId(
|
||||||
|
key.assetId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ImageLoader.loadImageFromCache(
|
||||||
|
preview,
|
||||||
|
cache: cache,
|
||||||
|
decode: decode,
|
||||||
|
chunkEvents: chunkController,
|
||||||
|
).whenComplete(chunkController.close);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other is RemoteThumbProvider) {
|
||||||
|
return assetId == other.assetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => assetId.hashCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
class RemoteFullImageProvider extends ImageProvider<RemoteFullImageProvider> {
|
||||||
|
final String assetId;
|
||||||
|
final CacheManager? cacheManager;
|
||||||
|
|
||||||
|
const RemoteFullImageProvider({
|
||||||
|
required this.assetId,
|
||||||
|
this.cacheManager,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<RemoteFullImageProvider> obtainKey(ImageConfiguration configuration) {
|
||||||
|
return SynchronousFuture(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
ImageStreamCompleter loadImage(
|
||||||
|
RemoteFullImageProvider key,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
) {
|
||||||
|
final cache = cacheManager ?? RemoteImageCacheManager();
|
||||||
|
final chunkEvents = StreamController<ImageChunkEvent>();
|
||||||
|
return MultiImageStreamCompleter(
|
||||||
|
codec: _codec(key, cache, decode, chunkEvents),
|
||||||
|
scale: 1.0,
|
||||||
|
chunkEvents: chunkEvents.stream,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<Codec> _codec(
|
||||||
|
RemoteFullImageProvider key,
|
||||||
|
CacheManager cache,
|
||||||
|
ImageDecoderCallback decode,
|
||||||
|
StreamController<ImageChunkEvent> chunkController,
|
||||||
|
) async* {
|
||||||
|
yield await ImageLoader.loadImageFromCache(
|
||||||
|
getPreviewUrlForRemoteId(key.assetId),
|
||||||
|
cache: cache,
|
||||||
|
decode: decode,
|
||||||
|
chunkEvents: chunkController,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (AppSetting.get(Setting.loadOriginal)) {
|
||||||
|
yield await ImageLoader.loadImageFromCache(
|
||||||
|
getOriginalUrlForRemoteId(key.assetId),
|
||||||
|
cache: cache,
|
||||||
|
decode: decode,
|
||||||
|
chunkEvents: chunkController,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await chunkController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
if (other is RemoteFullImageProvider) {
|
||||||
|
return assetId == other.assetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => assetId.hashCode;
|
||||||
|
}
|
@ -1,80 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/painting.dart';
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/constants.dart';
|
|
||||||
import 'package:immich_mobile/providers/image/cache/image_loader.dart';
|
|
||||||
import 'package:immich_mobile/providers/image/cache/thumbnail_image_cache_manager.dart';
|
|
||||||
import 'package:immich_mobile/utils/image_url_builder.dart';
|
|
||||||
|
|
||||||
class RemoteThumbProvider extends ImageProvider<RemoteThumbProvider> {
|
|
||||||
final String assetId;
|
|
||||||
final double height;
|
|
||||||
final double width;
|
|
||||||
final CacheManager? cacheManager;
|
|
||||||
|
|
||||||
const RemoteThumbProvider({
|
|
||||||
required this.assetId,
|
|
||||||
this.height = kTimelineFixedTileExtent,
|
|
||||||
this.width = kTimelineFixedTileExtent,
|
|
||||||
this.cacheManager,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<RemoteThumbProvider> obtainKey(
|
|
||||||
ImageConfiguration configuration,
|
|
||||||
) {
|
|
||||||
return SynchronousFuture(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
ImageStreamCompleter loadImage(
|
|
||||||
RemoteThumbProvider key,
|
|
||||||
ImageDecoderCallback decode,
|
|
||||||
) {
|
|
||||||
final cache = cacheManager ?? ThumbnailImageCacheManager();
|
|
||||||
final chunkController = StreamController<ImageChunkEvent>();
|
|
||||||
return MultiFrameImageStreamCompleter(
|
|
||||||
codec: _codec(key, cache, decode, chunkController),
|
|
||||||
scale: 1.0,
|
|
||||||
chunkEvents: chunkController.stream,
|
|
||||||
informationCollector: () => <DiagnosticsNode>[
|
|
||||||
DiagnosticsProperty<ImageProvider>('Image provider', this),
|
|
||||||
DiagnosticsProperty<String>('Asset Id', key.assetId),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Codec> _codec(
|
|
||||||
RemoteThumbProvider key,
|
|
||||||
CacheManager cache,
|
|
||||||
ImageDecoderCallback decode,
|
|
||||||
StreamController<ImageChunkEvent> chunkController,
|
|
||||||
) async {
|
|
||||||
final preview = getThumbnailUrlForRemoteId(
|
|
||||||
key.assetId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return ImageLoader.loadImageFromCache(
|
|
||||||
preview,
|
|
||||||
cache: cache,
|
|
||||||
decode: decode,
|
|
||||||
chunkEvents: chunkController,
|
|
||||||
).whenComplete(chunkController.close);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) {
|
|
||||||
if (identical(this, other)) return true;
|
|
||||||
if (other is RemoteThumbProvider) {
|
|
||||||
return assetId == other.assetId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => assetId.hashCode;
|
|
||||||
}
|
|
@ -1,7 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/local_thumb_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/remote_thumb_provider.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart';
|
import 'package:immich_mobile/presentation/widgets/images/thumb_hash_provider.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart';
|
||||||
import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart';
|
import 'package:immich_mobile/widgets/common/fade_in_placeholder_image.dart';
|
||||||
@ -25,49 +24,12 @@ class Thumbnail extends StatelessWidget {
|
|||||||
final Size size;
|
final Size size;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
|
|
||||||
static ImageProvider imageProvider({
|
|
||||||
BaseAsset? asset,
|
|
||||||
String? remoteId,
|
|
||||||
Size size = const Size.square(256),
|
|
||||||
}) {
|
|
||||||
assert(
|
|
||||||
asset != null || remoteId != null,
|
|
||||||
'Either asset or remoteId must be provided',
|
|
||||||
);
|
|
||||||
|
|
||||||
if (remoteId != null) {
|
|
||||||
return RemoteThumbProvider(
|
|
||||||
assetId: remoteId,
|
|
||||||
height: size.height,
|
|
||||||
width: size.width,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (asset is LocalAsset) {
|
|
||||||
return LocalThumbProvider(
|
|
||||||
asset: asset,
|
|
||||||
height: size.height,
|
|
||||||
width: size.width,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (asset is RemoteAsset) {
|
|
||||||
return RemoteThumbProvider(
|
|
||||||
assetId: asset.id,
|
|
||||||
height: size.height,
|
|
||||||
width: size.width,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw ArgumentError("Unsupported asset type: ${asset.runtimeType}");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final thumbHash =
|
final thumbHash =
|
||||||
asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
asset is RemoteAsset ? (asset as RemoteAsset).thumbHash : null;
|
||||||
final provider =
|
final provider =
|
||||||
imageProvider(asset: asset, remoteId: remoteId, size: size);
|
getThumbnailImageProvider(asset: asset, remoteId: remoteId, size: size);
|
||||||
|
|
||||||
return OctoImage.fromSet(
|
return OctoImage.fromSet(
|
||||||
image: provider,
|
image: provider,
|
||||||
|
@ -59,12 +59,15 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
|
child: Hero(
|
||||||
|
tag: asset.heroTag,
|
||||||
child: Thumbnail(
|
child: Thumbnail(
|
||||||
asset: asset,
|
asset: asset,
|
||||||
fit: fit,
|
fit: fit,
|
||||||
size: size,
|
size: size,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
if (asset.isVideo)
|
if (asset.isVideo)
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.topRight,
|
alignment: Alignment.topRight,
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
// ignore_for_file: require_trailing_commas
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
|
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/scroll_to_date_notifier.provider.dart';
|
||||||
|
|
||||||
|
class DriftMemoryBottomInfo extends StatelessWidget {
|
||||||
|
final DriftMemory memory;
|
||||||
|
final String title;
|
||||||
|
const DriftMemoryBottomInfo({
|
||||||
|
super.key,
|
||||||
|
required this.memory,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final df = DateFormat.yMMMMd();
|
||||||
|
final fileCreatedDate = memory.assets.first.createdAt;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[400],
|
||||||
|
fontSize: 13.0,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
df.format(fileCreatedDate),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 15.0,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MaterialButton(
|
||||||
|
minWidth: 0,
|
||||||
|
onPressed: () {
|
||||||
|
context.maybePop();
|
||||||
|
scrollToDateNotifierProvider.scrollToDate(fileCreatedDate);
|
||||||
|
},
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
color: Colors.white.withValues(alpha: 0.2),
|
||||||
|
elevation: 0,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.open_in_new,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
159
mobile/lib/presentation/widgets/memory/memory_card.widget.dart
Normal file
159
mobile/lib/presentation/widgets/memory/memory_card.widget.dart
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/full_image.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/image_provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/hooks/blurhash_hook.dart';
|
||||||
|
|
||||||
|
class DriftMemoryCard extends StatelessWidget {
|
||||||
|
final RemoteAsset asset;
|
||||||
|
final String title;
|
||||||
|
final bool showTitle;
|
||||||
|
final Function()? onVideoEnded;
|
||||||
|
|
||||||
|
const DriftMemoryCard({
|
||||||
|
required this.asset,
|
||||||
|
required this.title,
|
||||||
|
required this.showTitle,
|
||||||
|
this.onVideoEnded,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
color: Colors.black,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.all(Radius.circular(25.0)),
|
||||||
|
side: BorderSide(
|
||||||
|
color: Colors.black,
|
||||||
|
width: 1.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.hardEdge,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
SizedBox.expand(
|
||||||
|
child: _BlurredBackdrop(asset: asset),
|
||||||
|
),
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// Determine the fit using the aspect ratio
|
||||||
|
BoxFit fit = BoxFit.contain;
|
||||||
|
if (asset.width != null && asset.height != null) {
|
||||||
|
final aspectRatio = asset.width! / asset.height!;
|
||||||
|
final phoneAspectRatio =
|
||||||
|
constraints.maxWidth / constraints.maxHeight;
|
||||||
|
// Look for a 25% difference in either direction
|
||||||
|
if (phoneAspectRatio * .75 < aspectRatio &&
|
||||||
|
phoneAspectRatio * 1.25 > aspectRatio) {
|
||||||
|
// Cover to look nice if we have nearly the same aspect ratio
|
||||||
|
fit = BoxFit.cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asset.isImage) {
|
||||||
|
return Hero(
|
||||||
|
tag: 'memory-${asset.id}',
|
||||||
|
child: FullImage(
|
||||||
|
asset,
|
||||||
|
fit: fit,
|
||||||
|
size: const Size(double.infinity, double.infinity),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return Hero(
|
||||||
|
tag: 'memory-${asset.id}',
|
||||||
|
// child: SizedBox(
|
||||||
|
// width: context.width,
|
||||||
|
// height: context.height,
|
||||||
|
// child: NativeVideoViewerPage(
|
||||||
|
// key: ValueKey(asset.id),
|
||||||
|
// asset: asset,
|
||||||
|
// showControls: false,
|
||||||
|
// playbackDelayFactor: 2,
|
||||||
|
// image: ImmichImage(
|
||||||
|
// asset,
|
||||||
|
// width: context.width,
|
||||||
|
// height: context.height,
|
||||||
|
// fit: BoxFit.contain,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
child: FullImage(
|
||||||
|
asset,
|
||||||
|
fit: fit,
|
||||||
|
size: const Size(double.infinity, double.infinity),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (showTitle)
|
||||||
|
Positioned(
|
||||||
|
left: 18.0,
|
||||||
|
bottom: 18.0,
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: context.textTheme.headlineMedium?.copyWith(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BlurredBackdrop extends HookWidget {
|
||||||
|
final RemoteAsset asset;
|
||||||
|
|
||||||
|
const _BlurredBackdrop({required this.asset});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final blurhash = useDriftBlurHashRef(asset).value;
|
||||||
|
if (blurhash != null) {
|
||||||
|
// Use a nice cheap blur hash image decoration
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: MemoryImage(
|
||||||
|
blurhash,
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Fall back to using a more expensive image filtered
|
||||||
|
// Since the ImmichImage is already precached, we can
|
||||||
|
// safely use that as the image provider
|
||||||
|
return ImageFiltered(
|
||||||
|
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
image: DecorationImage(
|
||||||
|
image: getFullImageProvider(
|
||||||
|
asset,
|
||||||
|
size: Size(context.width, context.height),
|
||||||
|
),
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Container(
|
||||||
|
color: Colors.black.withValues(alpha: 0.2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
115
mobile/lib/presentation/widgets/memory/memory_lane.widget.dart
Normal file
115
mobile/lib/presentation/widgets/memory/memory_lane.widget.dart
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
|
class DriftMemoryLane extends ConsumerWidget {
|
||||||
|
final List<DriftMemory> memories;
|
||||||
|
|
||||||
|
const DriftMemoryLane({super.key, required this.memories});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxHeight: 200,
|
||||||
|
),
|
||||||
|
child: CarouselView(
|
||||||
|
itemExtent: 145.0,
|
||||||
|
shrinkExtent: 1.0,
|
||||||
|
elevation: 2,
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
overlayColor: WidgetStateProperty.all(
|
||||||
|
Colors.white.withValues(alpha: 0.1),
|
||||||
|
),
|
||||||
|
onTap: (index) {
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||||
|
|
||||||
|
if (memories[index].assets.isNotEmpty) {
|
||||||
|
final asset = memories[index].assets[0];
|
||||||
|
ref.read(currentAssetNotifier.notifier).setAsset(asset);
|
||||||
|
|
||||||
|
if (asset.isVideo) {
|
||||||
|
ref.read(videoPlaybackValueProvider.notifier).reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.pushRoute(
|
||||||
|
DriftMemoryRoute(
|
||||||
|
memories: memories,
|
||||||
|
memoryIndex: index,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
children:
|
||||||
|
memories.map((memory) => DriftMemoryCard(memory: memory)).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DriftMemoryCard extends ConsumerWidget {
|
||||||
|
const DriftMemoryCard({
|
||||||
|
super.key,
|
||||||
|
required this.memory,
|
||||||
|
});
|
||||||
|
|
||||||
|
final DriftMemory memory;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final yearsAgo = DateTime.now().year - memory.data.year;
|
||||||
|
final title = 'years_ago'.t(
|
||||||
|
context: context,
|
||||||
|
args: {
|
||||||
|
'years': yearsAgo.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return Center(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
ColorFiltered(
|
||||||
|
colorFilter: ColorFilter.mode(
|
||||||
|
Colors.black.withValues(alpha: 0.2),
|
||||||
|
BlendMode.darken,
|
||||||
|
),
|
||||||
|
child: Hero(
|
||||||
|
tag: 'memory-${memory.assets[0].id}',
|
||||||
|
child: SizedBox(
|
||||||
|
width: 205,
|
||||||
|
height: 200,
|
||||||
|
child: Thumbnail(
|
||||||
|
remoteId: memory.assets[0].id,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 16,
|
||||||
|
left: 16,
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: 114,
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
@ -12,6 +13,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart'
|
|||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
class FixedSegment extends Segment {
|
class FixedSegment extends Segment {
|
||||||
final double tileHeight;
|
final double tileHeight;
|
||||||
@ -35,50 +37,24 @@ class FixedSegment extends Segment {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
double indexToLayoutOffset(int index) {
|
double indexToLayoutOffset(int index) {
|
||||||
index -= gridIndex;
|
final relativeIndex = index - gridIndex;
|
||||||
if (index < 0) {
|
return relativeIndex < 0
|
||||||
return startOffset;
|
? startOffset
|
||||||
}
|
: gridOffset + (mainAxisExtend * relativeIndex);
|
||||||
return gridOffset + (mainAxisExtend * index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int getMinChildIndexForScrollOffset(double scrollOffset) {
|
int getMinChildIndexForScrollOffset(double scrollOffset) {
|
||||||
scrollOffset -= gridOffset;
|
final adjustedOffset = scrollOffset - gridOffset;
|
||||||
if (!scrollOffset.isFinite || scrollOffset < 0) {
|
if (!adjustedOffset.isFinite || adjustedOffset < 0) return firstIndex;
|
||||||
return firstIndex;
|
return gridIndex + (adjustedOffset / mainAxisExtend).floor();
|
||||||
}
|
|
||||||
final rowsAbove = (scrollOffset / mainAxisExtend).floor();
|
|
||||||
return gridIndex + rowsAbove;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int getMaxChildIndexForScrollOffset(double scrollOffset) {
|
int getMaxChildIndexForScrollOffset(double scrollOffset) {
|
||||||
scrollOffset -= gridOffset;
|
final adjustedOffset = scrollOffset - gridOffset;
|
||||||
if (!scrollOffset.isFinite || scrollOffset < 0) {
|
if (!adjustedOffset.isFinite || adjustedOffset < 0) return firstIndex;
|
||||||
return firstIndex;
|
return gridIndex + (adjustedOffset / mainAxisExtend).ceil() - 1;
|
||||||
}
|
|
||||||
final firstRowBelow = (scrollOffset / mainAxisExtend).ceil();
|
|
||||||
return gridIndex + firstRowBelow - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleOnTap(WidgetRef ref, BaseAsset asset) {
|
|
||||||
final multiSelectState = ref.read(multiSelectProvider);
|
|
||||||
if (!multiSelectState.isEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleOnLongPress(WidgetRef ref, BaseAsset asset) {
|
|
||||||
final multiSelectState = ref.read(multiSelectProvider);
|
|
||||||
if (multiSelectState.isEnabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
|
||||||
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -97,132 +73,128 @@ class FixedSegment extends Segment {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return _buildRow(firstAssetIndex + assetIndex, numberOfAssets);
|
return _FixedSegmentRow(
|
||||||
|
assetIndex: firstAssetIndex + assetIndex,
|
||||||
|
assetCount: numberOfAssets,
|
||||||
|
tileHeight: tileHeight,
|
||||||
|
spacing: spacing,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildRow(int assetIndex, int count) => RepaintBoundary(
|
class _FixedSegmentRow extends ConsumerWidget {
|
||||||
child: Consumer(
|
final int assetIndex;
|
||||||
builder: (ctx, ref, _) {
|
final int assetCount;
|
||||||
|
final double tileHeight;
|
||||||
|
final double spacing;
|
||||||
|
|
||||||
|
const _FixedSegmentRow({
|
||||||
|
required this.assetIndex,
|
||||||
|
required this.assetCount,
|
||||||
|
required this.tileHeight,
|
||||||
|
required this.spacing,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isScrubbing =
|
final isScrubbing =
|
||||||
ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
ref.watch(timelineStateProvider.select((s) => s.isScrubbing));
|
||||||
final timelineService = ref.read(timelineServiceProvider);
|
final timelineService = ref.read(timelineServiceProvider);
|
||||||
|
|
||||||
// Create stable callback references to prevent unnecessary rebuilds
|
|
||||||
onTap(BaseAsset asset) => _handleOnTap(ref, asset);
|
|
||||||
onLongPress(BaseAsset asset) => _handleOnLongPress(ref, asset);
|
|
||||||
|
|
||||||
// Timeline is being scrubbed, show placeholders
|
|
||||||
if (isScrubbing) {
|
if (isScrubbing) {
|
||||||
|
return _buildPlaceholder(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timelineService.hasRange(assetIndex, assetCount)) {
|
||||||
|
return _buildAssetRow(
|
||||||
|
context,
|
||||||
|
timelineService.getAssets(assetIndex, assetCount),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return FutureBuilder<List<BaseAsset>>(
|
||||||
|
future: timelineService.loadAssets(assetIndex, assetCount),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState != ConnectionState.done) {
|
||||||
|
return _buildPlaceholder(context);
|
||||||
|
}
|
||||||
|
return _buildAssetRow(context, snapshot.requireData);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPlaceholder(BuildContext context) {
|
||||||
return SegmentBuilder.buildPlaceholder(
|
return SegmentBuilder.buildPlaceholder(
|
||||||
ctx,
|
context,
|
||||||
count,
|
assetCount,
|
||||||
size: Size.square(tileHeight),
|
size: Size.square(tileHeight),
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bucket is already loaded, show the assets
|
Widget _buildAssetRow(BuildContext context, List<BaseAsset> assets) {
|
||||||
if (timelineService.hasRange(assetIndex, count)) {
|
return FixedTimelineRow(
|
||||||
final assets = timelineService.getAssets(assetIndex, count);
|
|
||||||
return _buildAssetRow(
|
|
||||||
ctx,
|
|
||||||
assets,
|
|
||||||
baseAssetIndex: assetIndex,
|
|
||||||
onTap: onTap,
|
|
||||||
onLongPress: onLongPress,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bucket is not loaded, show placeholders and load the bucket
|
|
||||||
return FutureBuilder(
|
|
||||||
future: timelineService.loadAssets(assetIndex, count),
|
|
||||||
builder: (ctxx, snap) {
|
|
||||||
if (snap.connectionState != ConnectionState.done) {
|
|
||||||
return SegmentBuilder.buildPlaceholder(
|
|
||||||
ctx,
|
|
||||||
count,
|
|
||||||
size: Size.square(tileHeight),
|
|
||||||
spacing: spacing,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _buildAssetRow(
|
|
||||||
ctxx,
|
|
||||||
snap.requireData,
|
|
||||||
baseAssetIndex: assetIndex,
|
|
||||||
onTap: onTap,
|
|
||||||
onLongPress: onLongPress,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Widget _buildAssetRow(
|
|
||||||
BuildContext context,
|
|
||||||
List<BaseAsset> assets, {
|
|
||||||
required void Function(BaseAsset) onTap,
|
|
||||||
required void Function(BaseAsset) onLongPress,
|
|
||||||
required int baseAssetIndex,
|
|
||||||
}) =>
|
|
||||||
FixedTimelineRow(
|
|
||||||
dimension: tileHeight,
|
dimension: tileHeight,
|
||||||
spacing: spacing,
|
spacing: spacing,
|
||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
children: List.generate(
|
children: [
|
||||||
assets.length,
|
for (int i = 0; i < assets.length; i++)
|
||||||
(i) => _AssetTileWidget(
|
_AssetTileWidget(
|
||||||
key: ValueKey(_generateUniqueKey(assets[i], baseAssetIndex + i)),
|
key: ValueKey(assets[i].heroTag),
|
||||||
asset: assets[i],
|
asset: assets[i],
|
||||||
onTap: onTap,
|
assetIndex: assetIndex + i,
|
||||||
onLongPress: onLongPress,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Generates a unique key for an asset that handles different asset types
|
|
||||||
/// and prevents duplicate keys even when assets have the same name/timestamp
|
|
||||||
String _generateUniqueKey(BaseAsset asset, int assetIndex) {
|
|
||||||
// Try to get the most unique identifier based on asset type
|
|
||||||
if (asset is RemoteAsset) {
|
|
||||||
// For remote/merged assets, use the remote ID which is globally unique
|
|
||||||
return 'asset_${asset.id}';
|
|
||||||
} else if (asset is LocalAsset) {
|
|
||||||
// For local assets, use the local ID which should be unique per device
|
|
||||||
return 'local_${asset.id}';
|
|
||||||
} else {
|
|
||||||
// Fallback for any other BaseAsset implementation
|
|
||||||
// Use checksum if available for additional uniqueness
|
|
||||||
final checksum = asset.checksum;
|
|
||||||
if (checksum != null && checksum.isNotEmpty) {
|
|
||||||
return 'checksum_${checksum.hashCode}';
|
|
||||||
} else {
|
|
||||||
// Last resort: use global asset index + object hash for uniqueness
|
|
||||||
return 'fallback_${assetIndex}_${asset.hashCode}_${asset.createdAt.microsecondsSinceEpoch}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AssetTileWidget extends StatelessWidget {
|
class _AssetTileWidget extends ConsumerWidget {
|
||||||
final BaseAsset asset;
|
final BaseAsset asset;
|
||||||
final void Function(BaseAsset) onTap;
|
final int assetIndex;
|
||||||
final void Function(BaseAsset) onLongPress;
|
|
||||||
|
|
||||||
const _AssetTileWidget({
|
const _AssetTileWidget({
|
||||||
super.key,
|
super.key,
|
||||||
required this.asset,
|
required this.asset,
|
||||||
required this.onTap,
|
required this.assetIndex,
|
||||||
required this.onLongPress,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void _handleOnTap(
|
||||||
|
BuildContext ctx,
|
||||||
|
WidgetRef ref,
|
||||||
|
int assetIndex,
|
||||||
|
BaseAsset asset,
|
||||||
|
) {
|
||||||
|
final multiSelectState = ref.read(multiSelectProvider);
|
||||||
|
if (!multiSelectState.isEnabled) {
|
||||||
|
ctx.pushRoute(
|
||||||
|
AssetViewerRoute(
|
||||||
|
initialIndex: assetIndex,
|
||||||
|
timelineService: ref.read(timelineServiceProvider),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleOnLongPress(WidgetRef ref, BaseAsset asset) {
|
||||||
|
final multiSelectState = ref.read(multiSelectProvider);
|
||||||
|
if (multiSelectState.isEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||||
|
ref.read(multiSelectProvider.notifier).toggleAssetSelection(asset);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => onTap(asset),
|
onTap: () => _handleOnTap(context, ref, assetIndex, asset),
|
||||||
onLongPress: () => onLongPress(asset),
|
onLongPress: () => _handleOnLongPress(ref, asset),
|
||||||
child: ThumbnailTile(asset),
|
child: ThumbnailTile(asset),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -26,6 +26,8 @@ class Scrubber extends ConsumerStatefulWidget {
|
|||||||
|
|
||||||
final double bottomPadding;
|
final double bottomPadding;
|
||||||
|
|
||||||
|
final double? monthSegmentSnappingOffset;
|
||||||
|
|
||||||
Scrubber({
|
Scrubber({
|
||||||
super.key,
|
super.key,
|
||||||
Key? scrollThumbKey,
|
Key? scrollThumbKey,
|
||||||
@ -33,6 +35,7 @@ class Scrubber extends ConsumerStatefulWidget {
|
|||||||
required this.timelineHeight,
|
required this.timelineHeight,
|
||||||
this.topPadding = 0,
|
this.topPadding = 0,
|
||||||
this.bottomPadding = 0,
|
this.bottomPadding = 0,
|
||||||
|
this.monthSegmentSnappingOffset,
|
||||||
required this.child,
|
required this.child,
|
||||||
}) : assert(child.scrollDirection == Axis.vertical);
|
}) : assert(child.scrollDirection == Axis.vertical);
|
||||||
|
|
||||||
@ -296,7 +299,10 @@ class ScrubberState extends ConsumerState<Scrubber>
|
|||||||
final viewportHeight = _scrollController.position.viewportDimension;
|
final viewportHeight = _scrollController.position.viewportDimension;
|
||||||
|
|
||||||
final targetScrollOffset = layoutSegment.startOffset;
|
final targetScrollOffset = layoutSegment.startOffset;
|
||||||
final centeredOffset = targetScrollOffset - (viewportHeight / 4) + 100;
|
final centeredOffset = targetScrollOffset -
|
||||||
|
(viewportHeight / 4) +
|
||||||
|
100 +
|
||||||
|
(widget.monthSegmentSnappingOffset ?? 0.0);
|
||||||
|
|
||||||
_scrollController.jumpTo(centeredOffset.clamp(0.0, maxScrollExtent));
|
_scrollController.jumpTo(centeredOffset.clamp(0.0, maxScrollExtent));
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
@ -6,6 +7,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/rendering.dart';
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/setting.model.dart';
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_app_bar/home_bottom_app_bar.widget.dart';
|
||||||
@ -18,7 +20,10 @@ import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
|||||||
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
|
||||||
|
|
||||||
class Timeline extends StatelessWidget {
|
class Timeline extends StatelessWidget {
|
||||||
const Timeline({super.key});
|
const Timeline({super.key, this.topSliverWidget, this.topSliverWidgetHeight});
|
||||||
|
|
||||||
|
final Widget? topSliverWidget;
|
||||||
|
final double? topSliverWidgetHeight;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@ -36,38 +41,49 @@ class Timeline extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const _SliverTimeline(),
|
child: _SliverTimeline(
|
||||||
|
topSliverWidget: topSliverWidget,
|
||||||
|
topSliverWidgetHeight: topSliverWidgetHeight,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SliverTimeline extends StatefulWidget {
|
class _SliverTimeline extends ConsumerStatefulWidget {
|
||||||
const _SliverTimeline();
|
const _SliverTimeline({this.topSliverWidget, this.topSliverWidgetHeight});
|
||||||
|
|
||||||
|
final Widget? topSliverWidget;
|
||||||
|
final double? topSliverWidgetHeight;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State createState() => _SliverTimelineState();
|
ConsumerState createState() => _SliverTimelineState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SliverTimelineState extends State<_SliverTimeline> {
|
class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
|
StreamSubscription? _reloadSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_reloadSubscription =
|
||||||
|
EventStream.shared.listen<TimelineReloadEvent>((_) => setState(() {}));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
|
_reloadSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext _) {
|
Widget build(BuildContext _) {
|
||||||
return Consumer(
|
|
||||||
builder: (context, ref, child) {
|
|
||||||
final asyncSegments = ref.watch(timelineSegmentProvider);
|
final asyncSegments = ref.watch(timelineSegmentProvider);
|
||||||
final maxHeight =
|
final maxHeight =
|
||||||
ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
|
ref.watch(timelineArgsProvider.select((args) => args.maxHeight));
|
||||||
final isMultiSelectEnabled =
|
|
||||||
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
|
||||||
return asyncSegments.widgetWhen(
|
return asyncSegments.widgetWhen(
|
||||||
onData: (segments) {
|
onData: (segments) {
|
||||||
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
|
final childCount = (segments.lastOrNull?.lastIndex ?? -1) + 1;
|
||||||
@ -83,21 +99,18 @@ class _SliverTimelineState extends State<_SliverTimeline> {
|
|||||||
layoutSegments: segments,
|
layoutSegments: segments,
|
||||||
timelineHeight: maxHeight,
|
timelineHeight: maxHeight,
|
||||||
topPadding: totalAppBarHeight + 10,
|
topPadding: totalAppBarHeight + 10,
|
||||||
bottomPadding:
|
bottomPadding: context.padding.bottom + scrubberBottomPadding,
|
||||||
context.padding.bottom + scrubberBottomPadding,
|
monthSegmentSnappingOffset: widget.topSliverWidgetHeight,
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
primary: true,
|
primary: true,
|
||||||
cacheExtent: maxHeight * 2,
|
cacheExtent: maxHeight * 2,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverAnimatedOpacity(
|
const ImmichSliverAppBar(
|
||||||
duration: Durations.medium1,
|
|
||||||
opacity: isMultiSelectEnabled ? 0 : 1,
|
|
||||||
sliver: const ImmichSliverAppBar(
|
|
||||||
floating: true,
|
floating: true,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
snap: false,
|
snap: false,
|
||||||
),
|
),
|
||||||
),
|
if (widget.topSliverWidget != null) widget.topSliverWidget!,
|
||||||
_SliverSegmentedList(
|
_SliverSegmentedList(
|
||||||
segments: segments,
|
segments: segments,
|
||||||
delegate: SliverChildBuilderDelegate(
|
delegate: SliverChildBuilderDelegate(
|
||||||
@ -121,18 +134,42 @@ class _SliverTimelineState extends State<_SliverTimeline> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isMultiSelectEnabled) ...[
|
Consumer(
|
||||||
const Positioned(
|
builder: (_, consumerRef, child) {
|
||||||
|
final isMultiSelectEnabled = consumerRef.watch(
|
||||||
|
multiSelectProvider.select(
|
||||||
|
(s) => s.isEnabled,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isMultiSelectEnabled) {
|
||||||
|
return child!;
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
child: const Positioned(
|
||||||
top: 60,
|
top: 60,
|
||||||
left: 25,
|
left: 25,
|
||||||
child: _MultiSelectStatusButton(),
|
child: _MultiSelectStatusButton(),
|
||||||
),
|
),
|
||||||
const HomeBottomAppBar(),
|
),
|
||||||
],
|
Consumer(
|
||||||
],
|
builder: (_, consumerRef, child) {
|
||||||
|
final isMultiSelectEnabled = consumerRef.watch(
|
||||||
|
multiSelectProvider.select(
|
||||||
|
(s) => s.isEnabled,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isMultiSelectEnabled) {
|
||||||
|
return child!;
|
||||||
|
}
|
||||||
|
return const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
|
child: const HomeBottomAppBar(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/services/action.service.dart';
|
import 'package:immich_mobile/services/action.service.dart';
|
||||||
@ -39,37 +40,40 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
_service = ref.watch(actionServiceProvider);
|
_service = ref.watch(actionServiceProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<String> _getIdsForSource<T extends BaseAsset>(ActionSource source) {
|
List<String> _getRemoteIdsForSource(ActionSource source) {
|
||||||
final currentUser = ref.read(currentUserProvider);
|
return _getIdsForSource<RemoteAsset>(source).toIds().toList();
|
||||||
if (T is RemoteAsset && currentUser == null) {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> _getOwnedRemoteForSource(ActionSource source) {
|
||||||
|
final ownerId = ref.read(currentUserProvider)?.id;
|
||||||
|
return _getIdsForSource<RemoteAsset>(source)
|
||||||
|
.ownedAssets(ownerId)
|
||||||
|
.toIds()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<T> _getIdsForSource<T extends BaseAsset>(ActionSource source) {
|
||||||
final Set<BaseAsset> assets = switch (source) {
|
final Set<BaseAsset> assets = switch (source) {
|
||||||
ActionSource.timeline =>
|
ActionSource.timeline =>
|
||||||
ref.read(multiSelectProvider.select((s) => s.selectedAssets)),
|
ref.read(multiSelectProvider.select((s) => s.selectedAssets)),
|
||||||
ActionSource.viewer => {},
|
ActionSource.viewer => switch (ref.read(currentAssetNotifier)) {
|
||||||
|
BaseAsset asset => {asset},
|
||||||
|
null => {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return switch (T) {
|
return switch (T) {
|
||||||
const (RemoteAsset) => assets
|
const (RemoteAsset) => assets.whereType<RemoteAsset>(),
|
||||||
.where(
|
const (LocalAsset) => assets.whereType<LocalAsset>(),
|
||||||
(asset) => asset is RemoteAsset && asset.ownerId == currentUser!.id,
|
_ => <T>[],
|
||||||
)
|
} as Iterable<T>;
|
||||||
.cast<RemoteAsset>()
|
|
||||||
.map((asset) => asset.id)
|
|
||||||
.toList(),
|
|
||||||
const (LocalAsset) =>
|
|
||||||
assets.whereType<LocalAsset>().map((asset) => asset.id).toList(),
|
|
||||||
_ => [],
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> shareLink(
|
Future<ActionResult> shareLink(
|
||||||
ActionSource source,
|
ActionSource source,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) async {
|
) async {
|
||||||
final ids = _getIdsForSource<RemoteAsset>(source);
|
final ids = _getRemoteIdsForSource(source);
|
||||||
try {
|
try {
|
||||||
await _service.shareLink(ids, context);
|
await _service.shareLink(ids, context);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
@ -84,7 +88,7 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> favorite(ActionSource source) async {
|
Future<ActionResult> favorite(ActionSource source) async {
|
||||||
final ids = _getIdsForSource<RemoteAsset>(source);
|
final ids = _getOwnedRemoteForSource(source);
|
||||||
try {
|
try {
|
||||||
await _service.favorite(ids);
|
await _service.favorite(ids);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
@ -99,7 +103,7 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> unFavorite(ActionSource source) async {
|
Future<ActionResult> unFavorite(ActionSource source) async {
|
||||||
final ids = _getIdsForSource<RemoteAsset>(source);
|
final ids = _getOwnedRemoteForSource(source);
|
||||||
try {
|
try {
|
||||||
await _service.unFavorite(ids);
|
await _service.unFavorite(ids);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
@ -114,7 +118,7 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> archive(ActionSource source) async {
|
Future<ActionResult> archive(ActionSource source) async {
|
||||||
final ids = _getIdsForSource<RemoteAsset>(source);
|
final ids = _getOwnedRemoteForSource(source);
|
||||||
try {
|
try {
|
||||||
await _service.archive(ids);
|
await _service.archive(ids);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
@ -129,7 +133,7 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> unArchive(ActionSource source) async {
|
Future<ActionResult> unArchive(ActionSource source) async {
|
||||||
final ids = _getIdsForSource<RemoteAsset>(source);
|
final ids = _getOwnedRemoteForSource(source);
|
||||||
try {
|
try {
|
||||||
await _service.unArchive(ids);
|
await _service.unArchive(ids);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
@ -144,7 +148,7 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> moveToLockFolder(ActionSource source) async {
|
Future<ActionResult> moveToLockFolder(ActionSource source) async {
|
||||||
final ids = _getIdsForSource<RemoteAsset>(source);
|
final ids = _getOwnedRemoteForSource(source);
|
||||||
try {
|
try {
|
||||||
await _service.moveToLockFolder(ids);
|
await _service.moveToLockFolder(ids);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
@ -159,7 +163,7 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<ActionResult> removeFromLockFolder(ActionSource source) async {
|
Future<ActionResult> removeFromLockFolder(ActionSource source) async {
|
||||||
final ids = _getIdsForSource<RemoteAsset>(source);
|
final ids = _getOwnedRemoteForSource(source);
|
||||||
try {
|
try {
|
||||||
await _service.removeFromLockFolder(ids);
|
await _service.removeFromLockFolder(ids);
|
||||||
return ActionResult(count: ids.length, success: true);
|
return ActionResult(count: ids.length, success: true);
|
||||||
@ -173,11 +177,41 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> trash(ActionSource source) async {
|
||||||
|
final ids = _getOwnedRemoteForSource(source);
|
||||||
|
try {
|
||||||
|
await _service.trash(ids);
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to trash assets', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ActionResult> delete(ActionSource source) async {
|
||||||
|
final ids = _getOwnedRemoteForSource(source);
|
||||||
|
try {
|
||||||
|
await _service.delete(ids);
|
||||||
|
return ActionResult(count: ids.length, success: true);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Failed to delete assets', error, stack);
|
||||||
|
return ActionResult(
|
||||||
|
count: ids.length,
|
||||||
|
success: false,
|
||||||
|
error: error.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<ActionResult?> editLocation(
|
Future<ActionResult?> editLocation(
|
||||||
ActionSource source,
|
ActionSource source,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) async {
|
) async {
|
||||||
final ids = _getIdsForSource<RemoteAsset>(source);
|
final ids = _getOwnedRemoteForSource(source);
|
||||||
try {
|
try {
|
||||||
final isEdited = await _service.editLocation(ids, context);
|
final isEdited = await _service.editLocation(ids, context);
|
||||||
if (!isEdited) {
|
if (!isEdited) {
|
||||||
@ -195,3 +229,12 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension on Iterable<RemoteAsset> {
|
||||||
|
Iterable<String> toIds() => map((e) => e.id);
|
||||||
|
|
||||||
|
Iterable<RemoteAsset> ownedAssets(String? ownerId) {
|
||||||
|
if (ownerId == null) return [];
|
||||||
|
return whereType<RemoteAsset>().where((a) => a.ownerId == ownerId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/asset.service.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
@ -7,6 +8,13 @@ final localAssetRepository = Provider<DriftLocalAssetRepository>(
|
|||||||
(ref) => DriftLocalAssetRepository(ref.watch(driftProvider)),
|
(ref) => DriftLocalAssetRepository(ref.watch(driftProvider)),
|
||||||
);
|
);
|
||||||
|
|
||||||
final remoteAssetRepository = Provider<DriftRemoteAssetRepository>(
|
final remoteAssetRepositoryProvider = Provider<RemoteAssetRepository>(
|
||||||
(ref) => DriftRemoteAssetRepository(ref.watch(driftProvider)),
|
(ref) => RemoteAssetRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final assetServiceProvider = Provider(
|
||||||
|
(ref) => AssetService(
|
||||||
|
remoteAssetRepository: ref.watch(remoteAssetRepositoryProvider),
|
||||||
|
localAssetRepository: ref.watch(localAssetRepository),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
|
||||||
|
final currentAssetNotifier =
|
||||||
|
AutoDisposeNotifierProvider<CurrentAssetNotifier, BaseAsset?>(
|
||||||
|
CurrentAssetNotifier.new,
|
||||||
|
);
|
||||||
|
|
||||||
|
class CurrentAssetNotifier extends AutoDisposeNotifier<BaseAsset?> {
|
||||||
|
KeepAliveLink? _keepAliveLink;
|
||||||
|
StreamSubscription<BaseAsset?>? _assetSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
BaseAsset? build() => null;
|
||||||
|
|
||||||
|
void setAsset(BaseAsset asset) {
|
||||||
|
_keepAliveLink?.close();
|
||||||
|
_assetSubscription?.cancel();
|
||||||
|
state = asset;
|
||||||
|
_assetSubscription = ref
|
||||||
|
.watch(assetServiceProvider)
|
||||||
|
.watchAsset(asset)
|
||||||
|
.listen((updatedAsset) {
|
||||||
|
if (updatedAsset != null) {
|
||||||
|
state = updatedAsset;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_keepAliveLink = ref.keepAlive();
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_keepAliveLink?.close();
|
||||||
|
_assetSubscription?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentAssetExifProvider = FutureProvider.autoDispose(
|
||||||
|
(ref) {
|
||||||
|
final currentAsset = ref.watch(currentAssetNotifier);
|
||||||
|
if (currentAsset == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return ref.watch(assetServiceProvider).getExif(currentAsset);
|
||||||
|
},
|
||||||
|
);
|
@ -8,7 +8,3 @@ part 'exif.provider.g.dart';
|
|||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
IsarExifRepository exifRepository(Ref ref) =>
|
IsarExifRepository exifRepository(Ref ref) =>
|
||||||
IsarExifRepository(ref.watch(isarProvider));
|
IsarExifRepository(ref.watch(isarProvider));
|
||||||
|
|
||||||
final remoteExifRepository = Provider<DriftRemoteExifRepository>(
|
|
||||||
(ref) => DriftRemoteExifRepository(ref.watch(driftProvider)),
|
|
||||||
);
|
|
||||||
|
27
mobile/lib/providers/infrastructure/memory.provider.dart
Normal file
27
mobile/lib/providers/infrastructure/memory.provider.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/memory.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/memory.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
import 'db.provider.dart';
|
||||||
|
|
||||||
|
final driftMemoryRepositoryProvider = Provider<DriftMemoryRepository>(
|
||||||
|
(ref) => DriftMemoryRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final driftMemoryServiceProvider = Provider<DriftMemoryService>(
|
||||||
|
(ref) => DriftMemoryService(ref.watch(driftMemoryRepositoryProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final driftMemoryFutureProvider =
|
||||||
|
FutureProvider.autoDispose<List<DriftMemory>>((ref) async {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
final service = ref.watch(driftMemoryServiceProvider);
|
||||||
|
|
||||||
|
return service.getMemoryLane(user.id);
|
||||||
|
});
|
@ -2,5 +2,5 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/storage.repository.dart';
|
||||||
|
|
||||||
final storageRepositoryProvider = Provider<StorageRepository>(
|
final storageRepositoryProvider = Provider<StorageRepository>(
|
||||||
(ref) => StorageRepository(),
|
(ref) => const StorageRepository(),
|
||||||
);
|
);
|
||||||
|
@ -48,6 +48,10 @@ class AssetApiRepository extends ApiRepository {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> delete(List<String> ids, bool force) async {
|
||||||
|
return _api.deleteAssets(AssetBulkDeleteDto(ids: ids, force: force));
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateVisibility(
|
Future<void> updateVisibility(
|
||||||
List<String> ids,
|
List<String> ids,
|
||||||
AssetVisibilityEnum visibility,
|
AssetVisibilityEnum visibility,
|
||||||
|
@ -2,7 +2,9 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/log.model.dart';
|
import 'package:immich_mobile/domain/models/log.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/memory.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/models/folder/recursive_folder.model.dart';
|
import 'package:immich_mobile/models/folder/recursive_folder.model.dart';
|
||||||
@ -70,6 +72,8 @@ import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
|||||||
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/media_stat.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/remote_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
import 'package:immich_mobile/presentation/pages/drift_album.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/drift_memory.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.page.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
import 'package:immich_mobile/providers/gallery_permission.provider.dart';
|
||||||
import 'package:immich_mobile/routing/auth_guard.dart';
|
import 'package:immich_mobile/routing/auth_guard.dart';
|
||||||
@ -81,6 +85,7 @@ import 'package:immich_mobile/services/api.service.dart';
|
|||||||
import 'package:immich_mobile/services/local_auth.service.dart';
|
import 'package:immich_mobile/services/local_auth.service.dart';
|
||||||
import 'package:immich_mobile/services/secure_storage.service.dart';
|
import 'package:immich_mobile/services/secure_storage.service.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||||
|
|
||||||
import 'package:maplibre_gl/maplibre_gl.dart';
|
import 'package:maplibre_gl/maplibre_gl.dart';
|
||||||
|
|
||||||
part 'router.gr.dart';
|
part 'router.gr.dart';
|
||||||
@ -371,6 +376,23 @@ class AppRouter extends RootStackRouter {
|
|||||||
page: RemoteTimelineRoute.page,
|
page: RemoteTimelineRoute.page,
|
||||||
guards: [_authGuard, _duplicateGuard],
|
guards: [_authGuard, _duplicateGuard],
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: AssetViewerRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
type: RouteType.custom(
|
||||||
|
customRouteBuilder: <T>(context, child, page) => PageRouteBuilder<T>(
|
||||||
|
fullscreenDialog: page.fullscreenDialog,
|
||||||
|
settings: page,
|
||||||
|
pageBuilder: (_, __, ___) => child,
|
||||||
|
opaque: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftMemoryRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
|
||||||
// required to handle all deeplinks in deep_link.service.dart
|
// required to handle all deeplinks in deep_link.service.dart
|
||||||
// auto_route_library#1722
|
// auto_route_library#1722
|
||||||
RedirectRoute(path: '*', redirectTo: '/'),
|
RedirectRoute(path: '*', redirectTo: '/'),
|
||||||
|
@ -403,6 +403,58 @@ class ArchiveRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [AssetViewerPage]
|
||||||
|
class AssetViewerRoute extends PageRouteInfo<AssetViewerRouteArgs> {
|
||||||
|
AssetViewerRoute({
|
||||||
|
Key? key,
|
||||||
|
required int initialIndex,
|
||||||
|
required TimelineService timelineService,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
AssetViewerRoute.name,
|
||||||
|
args: AssetViewerRouteArgs(
|
||||||
|
key: key,
|
||||||
|
initialIndex: initialIndex,
|
||||||
|
timelineService: timelineService,
|
||||||
|
),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'AssetViewerRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
final args = data.argsAs<AssetViewerRouteArgs>();
|
||||||
|
return AssetViewerPage(
|
||||||
|
key: args.key,
|
||||||
|
initialIndex: args.initialIndex,
|
||||||
|
timelineService: args.timelineService,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AssetViewerRouteArgs {
|
||||||
|
const AssetViewerRouteArgs({
|
||||||
|
this.key,
|
||||||
|
required this.initialIndex,
|
||||||
|
required this.timelineService,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
final int initialIndex;
|
||||||
|
|
||||||
|
final TimelineService timelineService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'AssetViewerRouteArgs{key: $key, initialIndex: $initialIndex, timelineService: $timelineService}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [BackupAlbumSelectionPage]
|
/// [BackupAlbumSelectionPage]
|
||||||
class BackupAlbumSelectionRoute extends PageRouteInfo<void> {
|
class BackupAlbumSelectionRoute extends PageRouteInfo<void> {
|
||||||
@ -566,6 +618,58 @@ class DriftAlbumsRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftMemoryPage]
|
||||||
|
class DriftMemoryRoute extends PageRouteInfo<DriftMemoryRouteArgs> {
|
||||||
|
DriftMemoryRoute({
|
||||||
|
required List<DriftMemory> memories,
|
||||||
|
required int memoryIndex,
|
||||||
|
Key? key,
|
||||||
|
List<PageRouteInfo>? children,
|
||||||
|
}) : super(
|
||||||
|
DriftMemoryRoute.name,
|
||||||
|
args: DriftMemoryRouteArgs(
|
||||||
|
memories: memories,
|
||||||
|
memoryIndex: memoryIndex,
|
||||||
|
key: key,
|
||||||
|
),
|
||||||
|
initialChildren: children,
|
||||||
|
);
|
||||||
|
|
||||||
|
static const String name = 'DriftMemoryRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
final args = data.argsAs<DriftMemoryRouteArgs>();
|
||||||
|
return DriftMemoryPage(
|
||||||
|
memories: args.memories,
|
||||||
|
memoryIndex: args.memoryIndex,
|
||||||
|
key: args.key,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class DriftMemoryRouteArgs {
|
||||||
|
const DriftMemoryRouteArgs({
|
||||||
|
required this.memories,
|
||||||
|
required this.memoryIndex,
|
||||||
|
this.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<DriftMemory> memories;
|
||||||
|
|
||||||
|
final int memoryIndex;
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'DriftMemoryRouteArgs{memories: $memories, memoryIndex: $memoryIndex, key: $key}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [EditImagePage]
|
/// [EditImagePage]
|
||||||
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
|
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
|
||||||
|
@ -2,10 +2,8 @@ import 'package:auto_route/auto_route.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/infrastructure/repositories/exif.repository.dart';
|
|
||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/exif.provider.dart';
|
|
||||||
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
import 'package:immich_mobile/repositories/asset_api.repository.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
import 'package:immich_mobile/widgets/common/location_picker.dart';
|
||||||
@ -15,20 +13,17 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
final actionServiceProvider = Provider<ActionService>(
|
final actionServiceProvider = Provider<ActionService>(
|
||||||
(ref) => ActionService(
|
(ref) => ActionService(
|
||||||
ref.watch(assetApiRepositoryProvider),
|
ref.watch(assetApiRepositoryProvider),
|
||||||
ref.watch(remoteAssetRepository),
|
ref.watch(remoteAssetRepositoryProvider),
|
||||||
ref.watch(remoteExifRepository),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
class ActionService {
|
class ActionService {
|
||||||
final AssetApiRepository _assetApiRepository;
|
final AssetApiRepository _assetApiRepository;
|
||||||
final DriftRemoteAssetRepository _remoteAssetRepository;
|
final RemoteAssetRepository _remoteAssetRepository;
|
||||||
final DriftRemoteExifRepository _remoteExifRepository;
|
|
||||||
|
|
||||||
const ActionService(
|
const ActionService(
|
||||||
this._assetApiRepository,
|
this._assetApiRepository,
|
||||||
this._remoteAssetRepository,
|
this._remoteAssetRepository,
|
||||||
this._remoteExifRepository,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
Future<void> shareLink(List<String> remoteIds, BuildContext context) async {
|
||||||
@ -93,13 +88,23 @@ class ActionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> trash(List<String> remoteIds) async {
|
||||||
|
await _assetApiRepository.delete(remoteIds, false);
|
||||||
|
await _remoteAssetRepository.trash(remoteIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> delete(List<String> remoteIds) async {
|
||||||
|
await _assetApiRepository.delete(remoteIds, true);
|
||||||
|
await _remoteAssetRepository.delete(remoteIds);
|
||||||
|
}
|
||||||
|
|
||||||
Future<bool> editLocation(
|
Future<bool> editLocation(
|
||||||
List<String> remoteIds,
|
List<String> remoteIds,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
) async {
|
) async {
|
||||||
LatLng? initialLatLng;
|
LatLng? initialLatLng;
|
||||||
if (remoteIds.length == 1) {
|
if (remoteIds.length == 1) {
|
||||||
final exif = await _remoteExifRepository.get(remoteIds[0]);
|
final exif = await _remoteAssetRepository.getExif(remoteIds[0]);
|
||||||
|
|
||||||
if (exif?.latitude != null && exif?.longitude != null) {
|
if (exif?.latitude != null && exif?.longitude != null) {
|
||||||
initialLatLng = LatLng(exif!.latitude!, exif.longitude!);
|
initialLatLng = LatLng(exif!.latitude!, exif.longitude!);
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/local_image_provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/images/remote_image_provider.dart';
|
||||||
import 'package:immich_mobile/providers/image/immich_local_image_provider.dart';
|
import 'package:immich_mobile/providers/image/immich_local_image_provider.dart';
|
||||||
import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
|
import 'package:immich_mobile/providers/image/immich_local_thumbnail_provider.dart';
|
||||||
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
import 'package:immich_mobile/providers/image/immich_remote_image_provider.dart';
|
||||||
@ -37,8 +39,10 @@ final class CustomImageCache implements ImageCache {
|
|||||||
/// Gets the cache for the given key
|
/// Gets the cache for the given key
|
||||||
/// [_large] is used for [ImmichLocalImageProvider] and [ImmichRemoteImageProvider]
|
/// [_large] is used for [ImmichLocalImageProvider] and [ImmichRemoteImageProvider]
|
||||||
/// [_small] is used for [ImmichLocalThumbnailProvider] and [ImmichRemoteThumbnailProvider]
|
/// [_small] is used for [ImmichLocalThumbnailProvider] and [ImmichRemoteThumbnailProvider]
|
||||||
ImageCache _cacheForKey(Object key) =>
|
ImageCache _cacheForKey(Object key) => (key is ImmichLocalImageProvider ||
|
||||||
(key is ImmichLocalImageProvider || key is ImmichRemoteImageProvider)
|
key is ImmichRemoteImageProvider ||
|
||||||
|
key is LocalFullImageProvider ||
|
||||||
|
key is RemoteFullImageProvider)
|
||||||
? _large
|
? _large
|
||||||
: _small;
|
: _small;
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:thumbhash/thumbhash.dart' as thumbhash;
|
import 'package:thumbhash/thumbhash.dart' as thumbhash;
|
||||||
|
|
||||||
@ -15,3 +16,15 @@ ObjectRef<Uint8List?> useBlurHashRef(Asset? asset) {
|
|||||||
|
|
||||||
return useRef(thumbhash.rgbaToBmp(rbga));
|
return useRef(thumbhash.rgbaToBmp(rbga));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ObjectRef<Uint8List?> useDriftBlurHashRef(RemoteAsset? asset) {
|
||||||
|
if (asset?.thumbHash == null) {
|
||||||
|
return useRef(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
final rbga = thumbhash.thumbHashToRGBA(
|
||||||
|
base64Decode(asset!.thumbHash!),
|
||||||
|
);
|
||||||
|
|
||||||
|
return useRef(thumbhash.rgbaToBmp(rbga));
|
||||||
|
}
|
||||||
|
@ -73,6 +73,9 @@ String getThumbnailUrlForRemoteId(
|
|||||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}';
|
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${type.value}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getPreviewUrlForRemoteId(final String id) =>
|
||||||
|
'${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=${AssetMediaSize.preview}';
|
||||||
|
|
||||||
String getPlaybackUrlForRemoteId(final String id) {
|
String getPlaybackUrlForRemoteId(final String id) {
|
||||||
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?';
|
return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/video/playback?';
|
||||||
}
|
}
|
||||||
|
@ -9,11 +9,13 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
class ExifMap extends StatelessWidget {
|
class ExifMap extends StatelessWidget {
|
||||||
final ExifInfo exifInfo;
|
final ExifInfo exifInfo;
|
||||||
final String? markerId;
|
final String? markerId;
|
||||||
|
final MapCreatedCallback? onMapCreated;
|
||||||
|
|
||||||
const ExifMap({
|
const ExifMap({
|
||||||
super.key,
|
super.key,
|
||||||
required this.exifInfo,
|
required this.exifInfo,
|
||||||
this.markerId = 'marker',
|
this.markerId = 'marker',
|
||||||
|
this.onMapCreated,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -82,6 +84,7 @@ class ExifMap extends StatelessWidget {
|
|||||||
debugPrint('Opening Map Uri: $uri');
|
debugPrint('Opening Map Uri: $uri');
|
||||||
launchUrl(uri);
|
launchUrl(uri);
|
||||||
},
|
},
|
||||||
|
onCreated: onMapCreated,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -10,6 +10,7 @@ import 'package:immich_mobile/providers/background_sync.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||||
@ -39,8 +40,13 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||||
|
final isMultiSelectEnabled =
|
||||||
|
ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAnimatedOpacity(
|
||||||
|
duration: Durations.medium1,
|
||||||
|
opacity: isMultiSelectEnabled ? 0 : 1,
|
||||||
|
sliver: SliverAppBar(
|
||||||
floating: floating,
|
floating: floating,
|
||||||
pinned: pinned,
|
pinned: pinned,
|
||||||
snap: snap,
|
snap: snap,
|
||||||
@ -97,6 +103,7 @@ class ImmichSliverAppBar extends ConsumerWidget {
|
|||||||
child: _ProfileIndicator(),
|
child: _ProfileIndicator(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
import 'package:immich_mobile/extensions/maplibrecontroller_extensions.dart';
|
||||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||||
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
import 'package:immich_mobile/widgets/map/positioned_asset_marker_icon.dart';
|
||||||
@ -24,6 +25,7 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
final double width;
|
final double width;
|
||||||
final ThemeMode? themeMode;
|
final ThemeMode? themeMode;
|
||||||
final bool showAttribution;
|
final bool showAttribution;
|
||||||
|
final MapCreatedCallback? onCreated;
|
||||||
|
|
||||||
const MapThumbnail({
|
const MapThumbnail({
|
||||||
super.key,
|
super.key,
|
||||||
@ -36,16 +38,19 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
this.showMarkerPin = false,
|
this.showMarkerPin = false,
|
||||||
this.themeMode,
|
this.themeMode,
|
||||||
this.showAttribution = true,
|
this.showAttribution = true,
|
||||||
|
this.onCreated,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final offsettedCentre = LatLng(centre.latitude + 0.002, centre.longitude);
|
final offsettedCentre = LatLng(centre.latitude + 0.002, centre.longitude);
|
||||||
final controller = useRef<MapLibreMapController?>(null);
|
final controller = useRef<MapLibreMapController?>(null);
|
||||||
|
final styleLoaded = useState(false);
|
||||||
final position = useValueNotifier<Point<num>?>(null);
|
final position = useValueNotifier<Point<num>?>(null);
|
||||||
|
|
||||||
Future<void> onMapCreated(MapLibreMapController mapController) async {
|
Future<void> onMapCreated(MapLibreMapController mapController) async {
|
||||||
controller.value = mapController;
|
controller.value = mapController;
|
||||||
|
styleLoaded.value = false;
|
||||||
if (assetMarkerRemoteId != null) {
|
if (assetMarkerRemoteId != null) {
|
||||||
// The iOS impl returns wrong toScreenLocation without the delay
|
// The iOS impl returns wrong toScreenLocation without the delay
|
||||||
Future.delayed(
|
Future.delayed(
|
||||||
@ -54,17 +59,26 @@ class MapThumbnail extends HookConsumerWidget {
|
|||||||
position.value = await mapController.toScreenLocation(centre),
|
position.value = await mapController.toScreenLocation(centre),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
onCreated?.call(mapController);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> onStyleLoaded() async {
|
Future<void> onStyleLoaded() async {
|
||||||
if (showMarkerPin && controller.value != null) {
|
if (showMarkerPin && controller.value != null) {
|
||||||
await controller.value?.addMarkerAtLatLng(centre);
|
await controller.value?.addMarkerAtLatLng(centre);
|
||||||
}
|
}
|
||||||
|
styleLoaded.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MapThemeOverride(
|
return MapThemeOverride(
|
||||||
themeMode: themeMode,
|
themeMode: themeMode,
|
||||||
mapBuilder: (style) => SizedBox(
|
mapBuilder: (style) => AnimatedContainer(
|
||||||
|
duration: Durations.medium2,
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
foregroundDecoration: BoxDecoration(
|
||||||
|
color: context.colorScheme.inverseSurface
|
||||||
|
.withAlpha(styleLoaded.value ? 0 : 200),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(15)),
|
||||||
|
),
|
||||||
height: height,
|
height: height,
|
||||||
width: width,
|
width: width,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart';
|
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart';
|
||||||
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart';
|
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart';
|
||||||
import 'package:immich_mobile/widgets/photo_view/src/core/photo_view_core.dart';
|
import 'package:immich_mobile/widgets/photo_view/src/core/photo_view_core.dart';
|
||||||
@ -16,6 +15,11 @@ export 'src/photo_view_computed_scale.dart';
|
|||||||
export 'src/photo_view_scale_state.dart';
|
export 'src/photo_view_scale_state.dart';
|
||||||
export 'src/utils/photo_view_hero_attributes.dart';
|
export 'src/utils/photo_view_hero_attributes.dart';
|
||||||
|
|
||||||
|
typedef PhotoViewControllerCallback = PhotoViewControllerBase Function();
|
||||||
|
typedef PhotoViewControllerCallbackBuilder = void Function(
|
||||||
|
PhotoViewControllerCallback photoViewMethod,
|
||||||
|
);
|
||||||
|
|
||||||
/// A [StatefulWidget] that contains all the photo view rendering elements.
|
/// A [StatefulWidget] that contains all the photo view rendering elements.
|
||||||
///
|
///
|
||||||
/// Sample code to use within an image:
|
/// Sample code to use within an image:
|
||||||
@ -239,8 +243,11 @@ class PhotoView extends StatefulWidget {
|
|||||||
this.wantKeepAlive = false,
|
this.wantKeepAlive = false,
|
||||||
this.gaplessPlayback = false,
|
this.gaplessPlayback = false,
|
||||||
this.heroAttributes,
|
this.heroAttributes,
|
||||||
|
this.onPageBuild,
|
||||||
|
this.controllerCallbackBuilder,
|
||||||
this.scaleStateChangedCallback,
|
this.scaleStateChangedCallback,
|
||||||
this.enableRotation = false,
|
this.enableRotation = false,
|
||||||
|
this.semanticLabel,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.scaleStateController,
|
this.scaleStateController,
|
||||||
this.maxScale,
|
this.maxScale,
|
||||||
@ -260,6 +267,7 @@ class PhotoView extends StatefulWidget {
|
|||||||
this.tightMode,
|
this.tightMode,
|
||||||
this.filterQuality,
|
this.filterQuality,
|
||||||
this.disableGestures,
|
this.disableGestures,
|
||||||
|
this.disableScaleGestures,
|
||||||
this.errorBuilder,
|
this.errorBuilder,
|
||||||
this.enablePanAlways,
|
this.enablePanAlways,
|
||||||
}) : child = null,
|
}) : child = null,
|
||||||
@ -278,6 +286,8 @@ class PhotoView extends StatefulWidget {
|
|||||||
this.backgroundDecoration,
|
this.backgroundDecoration,
|
||||||
this.wantKeepAlive = false,
|
this.wantKeepAlive = false,
|
||||||
this.heroAttributes,
|
this.heroAttributes,
|
||||||
|
this.onPageBuild,
|
||||||
|
this.controllerCallbackBuilder,
|
||||||
this.scaleStateChangedCallback,
|
this.scaleStateChangedCallback,
|
||||||
this.enableRotation = false,
|
this.enableRotation = false,
|
||||||
this.controller,
|
this.controller,
|
||||||
@ -298,9 +308,11 @@ class PhotoView extends StatefulWidget {
|
|||||||
this.gestureDetectorBehavior,
|
this.gestureDetectorBehavior,
|
||||||
this.tightMode,
|
this.tightMode,
|
||||||
this.filterQuality,
|
this.filterQuality,
|
||||||
|
this.disableScaleGestures,
|
||||||
this.disableGestures,
|
this.disableGestures,
|
||||||
this.enablePanAlways,
|
this.enablePanAlways,
|
||||||
}) : errorBuilder = null,
|
}) : semanticLabel = null,
|
||||||
|
errorBuilder = null,
|
||||||
imageProvider = null,
|
imageProvider = null,
|
||||||
gaplessPlayback = false,
|
gaplessPlayback = false,
|
||||||
loadingBuilder = null,
|
loadingBuilder = null,
|
||||||
@ -325,6 +337,11 @@ class PhotoView extends StatefulWidget {
|
|||||||
/// `true` -> keeps the state
|
/// `true` -> keeps the state
|
||||||
final bool wantKeepAlive;
|
final bool wantKeepAlive;
|
||||||
|
|
||||||
|
/// A Semantic description of the image.
|
||||||
|
///
|
||||||
|
/// Used to provide a description of the image to TalkBack on Android, and VoiceOver on iOS.
|
||||||
|
final String? semanticLabel;
|
||||||
|
|
||||||
/// This is used to continue showing the old image (`true`), or briefly show
|
/// This is used to continue showing the old image (`true`), or briefly show
|
||||||
/// nothing (`false`), when the `imageProvider` changes. By default it's set
|
/// nothing (`false`), when the `imageProvider` changes. By default it's set
|
||||||
/// to `false`.
|
/// to `false`.
|
||||||
@ -338,6 +355,12 @@ class PhotoView extends StatefulWidget {
|
|||||||
/// by default it is `MediaQuery.of(context).size`.
|
/// by default it is `MediaQuery.of(context).size`.
|
||||||
final Size? customSize;
|
final Size? customSize;
|
||||||
|
|
||||||
|
// Called when a new PhotoView widget is built
|
||||||
|
final ValueChanged<PhotoViewControllerBase>? onPageBuild;
|
||||||
|
|
||||||
|
// Called from the parent during page change to get the new controller
|
||||||
|
final PhotoViewControllerCallbackBuilder? controllerCallbackBuilder;
|
||||||
|
|
||||||
/// A [Function] to be called whenever the scaleState changes, this happens when the user double taps the content ou start to pinch-in.
|
/// A [Function] to be called whenever the scaleState changes, this happens when the user double taps the content ou start to pinch-in.
|
||||||
final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
|
final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
|
||||||
|
|
||||||
@ -419,6 +442,9 @@ class PhotoView extends StatefulWidget {
|
|||||||
// Useful when custom gesture detector is used in child widget.
|
// Useful when custom gesture detector is used in child widget.
|
||||||
final bool? disableGestures;
|
final bool? disableGestures;
|
||||||
|
|
||||||
|
/// Mirror to [PhotoView.disableGestures]
|
||||||
|
final bool? disableScaleGestures;
|
||||||
|
|
||||||
/// Enable pan the widget even if it's smaller than the hole parent widget.
|
/// Enable pan the widget even if it's smaller than the hole parent widget.
|
||||||
/// Useful when you want to drag a widget without restrictions.
|
/// Useful when you want to drag a widget without restrictions.
|
||||||
final bool? enablePanAlways;
|
final bool? enablePanAlways;
|
||||||
@ -452,6 +478,7 @@ class _PhotoViewState extends State<PhotoView>
|
|||||||
if (widget.controller == null) {
|
if (widget.controller == null) {
|
||||||
_controlledController = true;
|
_controlledController = true;
|
||||||
_controller = PhotoViewController();
|
_controller = PhotoViewController();
|
||||||
|
widget.onPageBuild?.call(_controller);
|
||||||
} else {
|
} else {
|
||||||
_controlledController = false;
|
_controlledController = false;
|
||||||
_controller = widget.controller!;
|
_controller = widget.controller!;
|
||||||
@ -466,6 +493,8 @@ class _PhotoViewState extends State<PhotoView>
|
|||||||
}
|
}
|
||||||
|
|
||||||
_scaleStateController.outputScaleStateStream.listen(scaleStateListener);
|
_scaleStateController.outputScaleStateStream.listen(scaleStateListener);
|
||||||
|
// Pass a ref to the method back to the gallery so it can fetch the controller on page changes
|
||||||
|
widget.controllerCallbackBuilder?.call(_controllerGetter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -474,6 +503,7 @@ class _PhotoViewState extends State<PhotoView>
|
|||||||
if (!_controlledController) {
|
if (!_controlledController) {
|
||||||
_controlledController = true;
|
_controlledController = true;
|
||||||
_controller = PhotoViewController();
|
_controller = PhotoViewController();
|
||||||
|
widget.onPageBuild?.call(_controller);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_controlledController = false;
|
_controlledController = false;
|
||||||
@ -509,6 +539,8 @@ class _PhotoViewState extends State<PhotoView>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PhotoViewControllerBase _controllerGetter() => _controller;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
@ -547,6 +579,7 @@ class _PhotoViewState extends State<PhotoView>
|
|||||||
tightMode: widget.tightMode,
|
tightMode: widget.tightMode,
|
||||||
filterQuality: widget.filterQuality,
|
filterQuality: widget.filterQuality,
|
||||||
disableGestures: widget.disableGestures,
|
disableGestures: widget.disableGestures,
|
||||||
|
disableScaleGestures: widget.disableScaleGestures,
|
||||||
enablePanAlways: widget.enablePanAlways,
|
enablePanAlways: widget.enablePanAlways,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
)
|
)
|
||||||
@ -554,6 +587,7 @@ class _PhotoViewState extends State<PhotoView>
|
|||||||
imageProvider: widget.imageProvider!,
|
imageProvider: widget.imageProvider!,
|
||||||
loadingBuilder: widget.loadingBuilder,
|
loadingBuilder: widget.loadingBuilder,
|
||||||
backgroundDecoration: backgroundDecoration,
|
backgroundDecoration: backgroundDecoration,
|
||||||
|
semanticLabel: widget.semanticLabel,
|
||||||
gaplessPlayback: widget.gaplessPlayback,
|
gaplessPlayback: widget.gaplessPlayback,
|
||||||
heroAttributes: widget.heroAttributes,
|
heroAttributes: widget.heroAttributes,
|
||||||
scaleStateChangedCallback: widget.scaleStateChangedCallback,
|
scaleStateChangedCallback: widget.scaleStateChangedCallback,
|
||||||
@ -577,6 +611,7 @@ class _PhotoViewState extends State<PhotoView>
|
|||||||
tightMode: widget.tightMode,
|
tightMode: widget.tightMode,
|
||||||
filterQuality: widget.filterQuality,
|
filterQuality: widget.filterQuality,
|
||||||
disableGestures: widget.disableGestures,
|
disableGestures: widget.disableGestures,
|
||||||
|
disableScaleGestures: widget.disableScaleGestures,
|
||||||
errorBuilder: widget.errorBuilder,
|
errorBuilder: widget.errorBuilder,
|
||||||
enablePanAlways: widget.enablePanAlways,
|
enablePanAlways: widget.enablePanAlways,
|
||||||
index: widget.index,
|
index: widget.index,
|
||||||
@ -625,7 +660,8 @@ typedef PhotoViewImageTapDownCallback = Function(
|
|||||||
typedef PhotoViewImageDragStartCallback = Function(
|
typedef PhotoViewImageDragStartCallback = Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
DragStartDetails details,
|
DragStartDetails details,
|
||||||
PhotoViewControllerValue controllerValue,
|
PhotoViewControllerBase controllerValue,
|
||||||
|
PhotoViewScaleStateController scaleStateController,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// A type definition for a callback when the user drags
|
/// A type definition for a callback when the user drags
|
||||||
|
@ -4,13 +4,14 @@ import 'package:immich_mobile/widgets/photo_view/photo_view.dart'
|
|||||||
show
|
show
|
||||||
LoadingBuilder,
|
LoadingBuilder,
|
||||||
PhotoView,
|
PhotoView,
|
||||||
|
PhotoViewControllerCallback,
|
||||||
|
PhotoViewImageDragEndCallback,
|
||||||
|
PhotoViewImageDragStartCallback,
|
||||||
|
PhotoViewImageDragUpdateCallback,
|
||||||
|
PhotoViewImageLongPressStartCallback,
|
||||||
|
PhotoViewImageScaleEndCallback,
|
||||||
PhotoViewImageTapDownCallback,
|
PhotoViewImageTapDownCallback,
|
||||||
PhotoViewImageTapUpCallback,
|
PhotoViewImageTapUpCallback,
|
||||||
PhotoViewImageDragStartCallback,
|
|
||||||
PhotoViewImageDragEndCallback,
|
|
||||||
PhotoViewImageDragUpdateCallback,
|
|
||||||
PhotoViewImageScaleEndCallback,
|
|
||||||
PhotoViewImageLongPressStartCallback,
|
|
||||||
ScaleStateCycle;
|
ScaleStateCycle;
|
||||||
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart';
|
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_controller.dart';
|
||||||
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart';
|
import 'package:immich_mobile/widgets/photo_view/src/controller/photo_view_scalestate_controller.dart';
|
||||||
@ -19,7 +20,10 @@ import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart
|
|||||||
import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart';
|
import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart';
|
||||||
|
|
||||||
/// A type definition for a [Function] that receives a index after a page change in [PhotoViewGallery]
|
/// A type definition for a [Function] that receives a index after a page change in [PhotoViewGallery]
|
||||||
typedef PhotoViewGalleryPageChangedCallback = void Function(int index);
|
typedef PhotoViewGalleryPageChangedCallback = void Function(
|
||||||
|
int index,
|
||||||
|
PhotoViewControllerBase? controller,
|
||||||
|
);
|
||||||
|
|
||||||
/// A type definition for a [Function] that defines a page in [PhotoViewGallery.build]
|
/// A type definition for a [Function] that defines a page in [PhotoViewGallery.build]
|
||||||
typedef PhotoViewGalleryBuilder = PhotoViewGalleryPageOptions Function(
|
typedef PhotoViewGalleryBuilder = PhotoViewGalleryPageOptions Function(
|
||||||
@ -114,12 +118,14 @@ class PhotoViewGallery extends StatefulWidget {
|
|||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
this.pageController,
|
this.pageController,
|
||||||
this.onPageChanged,
|
this.onPageChanged,
|
||||||
|
this.onPageBuild,
|
||||||
this.scaleStateChangedCallback,
|
this.scaleStateChangedCallback,
|
||||||
this.enableRotation = false,
|
this.enableRotation = false,
|
||||||
this.scrollPhysics,
|
this.scrollPhysics,
|
||||||
this.scrollDirection = Axis.horizontal,
|
this.scrollDirection = Axis.horizontal,
|
||||||
this.customSize,
|
this.customSize,
|
||||||
this.allowImplicitScrolling = false,
|
this.allowImplicitScrolling = false,
|
||||||
|
this.enablePanAlways = false,
|
||||||
}) : itemCount = null,
|
}) : itemCount = null,
|
||||||
builder = null;
|
builder = null;
|
||||||
|
|
||||||
@ -137,12 +143,14 @@ class PhotoViewGallery extends StatefulWidget {
|
|||||||
this.reverse = false,
|
this.reverse = false,
|
||||||
this.pageController,
|
this.pageController,
|
||||||
this.onPageChanged,
|
this.onPageChanged,
|
||||||
|
this.onPageBuild,
|
||||||
this.scaleStateChangedCallback,
|
this.scaleStateChangedCallback,
|
||||||
this.enableRotation = false,
|
this.enableRotation = false,
|
||||||
this.scrollPhysics,
|
this.scrollPhysics,
|
||||||
this.scrollDirection = Axis.horizontal,
|
this.scrollDirection = Axis.horizontal,
|
||||||
this.customSize,
|
this.customSize,
|
||||||
this.allowImplicitScrolling = false,
|
this.allowImplicitScrolling = false,
|
||||||
|
this.enablePanAlways = false,
|
||||||
}) : pageOptions = null,
|
}) : pageOptions = null,
|
||||||
assert(itemCount != null),
|
assert(itemCount != null),
|
||||||
assert(builder != null);
|
assert(builder != null);
|
||||||
@ -168,6 +176,9 @@ class PhotoViewGallery extends StatefulWidget {
|
|||||||
/// Mirror to [PhotoView.wantKeepAlive]
|
/// Mirror to [PhotoView.wantKeepAlive]
|
||||||
final bool wantKeepAlive;
|
final bool wantKeepAlive;
|
||||||
|
|
||||||
|
/// Mirror to [PhotoView.enablePanAlways]
|
||||||
|
final bool enablePanAlways;
|
||||||
|
|
||||||
/// Mirror to [PhotoView.gaplessPlayback]
|
/// Mirror to [PhotoView.gaplessPlayback]
|
||||||
final bool gaplessPlayback;
|
final bool gaplessPlayback;
|
||||||
|
|
||||||
@ -180,6 +191,9 @@ class PhotoViewGallery extends StatefulWidget {
|
|||||||
/// An callback to be called on a page change
|
/// An callback to be called on a page change
|
||||||
final PhotoViewGalleryPageChangedCallback? onPageChanged;
|
final PhotoViewGalleryPageChangedCallback? onPageChanged;
|
||||||
|
|
||||||
|
/// Mirror to [PhotoView.onPageBuild]
|
||||||
|
final ValueChanged<PhotoViewControllerBase>? onPageBuild;
|
||||||
|
|
||||||
/// Mirror to [PhotoView.scaleStateChangedCallback]
|
/// Mirror to [PhotoView.scaleStateChangedCallback]
|
||||||
final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
|
final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
|
||||||
|
|
||||||
@ -206,6 +220,7 @@ class PhotoViewGallery extends StatefulWidget {
|
|||||||
class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
||||||
late final PageController _controller =
|
late final PageController _controller =
|
||||||
widget.pageController ?? PageController();
|
widget.pageController ?? PageController();
|
||||||
|
PhotoViewControllerCallback? _getController;
|
||||||
|
|
||||||
void scaleStateChangedCallback(PhotoViewScaleState scaleState) {
|
void scaleStateChangedCallback(PhotoViewScaleState scaleState) {
|
||||||
if (widget.scaleStateChangedCallback != null) {
|
if (widget.scaleStateChangedCallback != null) {
|
||||||
@ -224,6 +239,14 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||||||
return widget.pageOptions!.length;
|
return widget.pageOptions!.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _getControllerCallbackBuilder(PhotoViewControllerCallback method) {
|
||||||
|
_getController = method;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPageChange(int page) {
|
||||||
|
widget.onPageChanged?.call(page, _getController?.call());
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Enable corner hit test
|
// Enable corner hit test
|
||||||
@ -232,7 +255,7 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||||||
child: PageView.builder(
|
child: PageView.builder(
|
||||||
reverse: widget.reverse,
|
reverse: widget.reverse,
|
||||||
controller: _controller,
|
controller: _controller,
|
||||||
onPageChanged: widget.onPageChanged,
|
onPageChanged: _onPageChange,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemBuilder: _buildItem,
|
itemBuilder: _buildItem,
|
||||||
scrollDirection: widget.scrollDirection,
|
scrollDirection: widget.scrollDirection,
|
||||||
@ -248,13 +271,15 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||||||
|
|
||||||
final PhotoView photoView = isCustomChild
|
final PhotoView photoView = isCustomChild
|
||||||
? PhotoView.customChild(
|
? PhotoView.customChild(
|
||||||
key: ObjectKey(index),
|
key: pageOption.key ?? ObjectKey(index),
|
||||||
childSize: pageOption.childSize,
|
childSize: pageOption.childSize,
|
||||||
backgroundDecoration: widget.backgroundDecoration,
|
backgroundDecoration: widget.backgroundDecoration,
|
||||||
wantKeepAlive: widget.wantKeepAlive,
|
wantKeepAlive: widget.wantKeepAlive,
|
||||||
controller: pageOption.controller,
|
controller: pageOption.controller,
|
||||||
scaleStateController: pageOption.scaleStateController,
|
scaleStateController: pageOption.scaleStateController,
|
||||||
customSize: widget.customSize,
|
customSize: widget.customSize,
|
||||||
|
onPageBuild: widget.onPageBuild,
|
||||||
|
controllerCallbackBuilder: _getControllerCallbackBuilder,
|
||||||
scaleStateChangedCallback: scaleStateChangedCallback,
|
scaleStateChangedCallback: scaleStateChangedCallback,
|
||||||
enableRotation: widget.enableRotation,
|
enableRotation: widget.enableRotation,
|
||||||
initialScale: pageOption.initialScale,
|
initialScale: pageOption.initialScale,
|
||||||
@ -273,17 +298,22 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||||||
filterQuality: pageOption.filterQuality,
|
filterQuality: pageOption.filterQuality,
|
||||||
basePosition: pageOption.basePosition,
|
basePosition: pageOption.basePosition,
|
||||||
disableGestures: pageOption.disableGestures,
|
disableGestures: pageOption.disableGestures,
|
||||||
|
disableScaleGestures: pageOption.disableScaleGestures,
|
||||||
heroAttributes: pageOption.heroAttributes,
|
heroAttributes: pageOption.heroAttributes,
|
||||||
|
enablePanAlways: widget.enablePanAlways,
|
||||||
child: pageOption.child,
|
child: pageOption.child,
|
||||||
)
|
)
|
||||||
: PhotoView(
|
: PhotoView(
|
||||||
key: ObjectKey(index),
|
key: pageOption.key ?? ObjectKey(index),
|
||||||
index: index,
|
index: index,
|
||||||
imageProvider: pageOption.imageProvider,
|
imageProvider: pageOption.imageProvider,
|
||||||
loadingBuilder: widget.loadingBuilder,
|
loadingBuilder: widget.loadingBuilder,
|
||||||
backgroundDecoration: widget.backgroundDecoration,
|
backgroundDecoration: widget.backgroundDecoration,
|
||||||
|
semanticLabel: pageOption.semanticLabel,
|
||||||
wantKeepAlive: widget.wantKeepAlive,
|
wantKeepAlive: widget.wantKeepAlive,
|
||||||
controller: pageOption.controller,
|
controller: pageOption.controller,
|
||||||
|
onPageBuild: widget.onPageBuild,
|
||||||
|
controllerCallbackBuilder: _getControllerCallbackBuilder,
|
||||||
scaleStateController: pageOption.scaleStateController,
|
scaleStateController: pageOption.scaleStateController,
|
||||||
customSize: widget.customSize,
|
customSize: widget.customSize,
|
||||||
gaplessPlayback: widget.gaplessPlayback,
|
gaplessPlayback: widget.gaplessPlayback,
|
||||||
@ -305,6 +335,8 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||||||
filterQuality: pageOption.filterQuality,
|
filterQuality: pageOption.filterQuality,
|
||||||
basePosition: pageOption.basePosition,
|
basePosition: pageOption.basePosition,
|
||||||
disableGestures: pageOption.disableGestures,
|
disableGestures: pageOption.disableGestures,
|
||||||
|
disableScaleGestures: pageOption.disableScaleGestures,
|
||||||
|
enablePanAlways: widget.enablePanAlways,
|
||||||
errorBuilder: pageOption.errorBuilder,
|
errorBuilder: pageOption.errorBuilder,
|
||||||
heroAttributes: pageOption.heroAttributes,
|
heroAttributes: pageOption.heroAttributes,
|
||||||
);
|
);
|
||||||
@ -331,9 +363,10 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||||||
///
|
///
|
||||||
class PhotoViewGalleryPageOptions {
|
class PhotoViewGalleryPageOptions {
|
||||||
PhotoViewGalleryPageOptions({
|
PhotoViewGalleryPageOptions({
|
||||||
Key? key,
|
this.key,
|
||||||
required this.imageProvider,
|
required this.imageProvider,
|
||||||
this.heroAttributes,
|
this.heroAttributes,
|
||||||
|
this.semanticLabel,
|
||||||
this.minScale,
|
this.minScale,
|
||||||
this.maxScale,
|
this.maxScale,
|
||||||
this.initialScale,
|
this.initialScale,
|
||||||
@ -351,6 +384,7 @@ class PhotoViewGalleryPageOptions {
|
|||||||
this.gestureDetectorBehavior,
|
this.gestureDetectorBehavior,
|
||||||
this.tightMode,
|
this.tightMode,
|
||||||
this.filterQuality,
|
this.filterQuality,
|
||||||
|
this.disableScaleGestures,
|
||||||
this.disableGestures,
|
this.disableGestures,
|
||||||
this.errorBuilder,
|
this.errorBuilder,
|
||||||
}) : child = null,
|
}) : child = null,
|
||||||
@ -358,8 +392,10 @@ class PhotoViewGalleryPageOptions {
|
|||||||
assert(imageProvider != null);
|
assert(imageProvider != null);
|
||||||
|
|
||||||
const PhotoViewGalleryPageOptions.customChild({
|
const PhotoViewGalleryPageOptions.customChild({
|
||||||
|
this.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
this.childSize,
|
this.childSize,
|
||||||
|
this.semanticLabel,
|
||||||
this.heroAttributes,
|
this.heroAttributes,
|
||||||
this.minScale,
|
this.minScale,
|
||||||
this.maxScale,
|
this.maxScale,
|
||||||
@ -378,16 +414,22 @@ class PhotoViewGalleryPageOptions {
|
|||||||
this.gestureDetectorBehavior,
|
this.gestureDetectorBehavior,
|
||||||
this.tightMode,
|
this.tightMode,
|
||||||
this.filterQuality,
|
this.filterQuality,
|
||||||
|
this.disableScaleGestures,
|
||||||
this.disableGestures,
|
this.disableGestures,
|
||||||
}) : errorBuilder = null,
|
}) : errorBuilder = null,
|
||||||
imageProvider = null;
|
imageProvider = null;
|
||||||
|
|
||||||
|
final Key? key;
|
||||||
|
|
||||||
/// Mirror to [PhotoView.imageProvider]
|
/// Mirror to [PhotoView.imageProvider]
|
||||||
final ImageProvider? imageProvider;
|
final ImageProvider? imageProvider;
|
||||||
|
|
||||||
/// Mirror to [PhotoView.heroAttributes]
|
/// Mirror to [PhotoView.heroAttributes]
|
||||||
final PhotoViewHeroAttributes? heroAttributes;
|
final PhotoViewHeroAttributes? heroAttributes;
|
||||||
|
|
||||||
|
/// Mirror to [PhotoView.semanticLabel]
|
||||||
|
final String? semanticLabel;
|
||||||
|
|
||||||
/// Mirror to [PhotoView.minScale]
|
/// Mirror to [PhotoView.minScale]
|
||||||
final dynamic minScale;
|
final dynamic minScale;
|
||||||
|
|
||||||
@ -445,6 +487,9 @@ class PhotoViewGalleryPageOptions {
|
|||||||
/// Mirror to [PhotoView.disableGestures]
|
/// Mirror to [PhotoView.disableGestures]
|
||||||
final bool? disableGestures;
|
final bool? disableGestures;
|
||||||
|
|
||||||
|
/// Mirror to [PhotoView.disableGestures]
|
||||||
|
final bool? disableScaleGestures;
|
||||||
|
|
||||||
/// Quality levels for image filters.
|
/// Quality levels for image filters.
|
||||||
final FilterQuality? filterQuality;
|
final FilterQuality? filterQuality;
|
||||||
|
|
||||||
|
@ -37,6 +37,13 @@ abstract class PhotoViewControllerBase<T extends PhotoViewControllerValue> {
|
|||||||
/// Closes streams and removes eventual listeners.
|
/// Closes streams and removes eventual listeners.
|
||||||
void dispose();
|
void dispose();
|
||||||
|
|
||||||
|
void positionAnimationBuilder(void Function(Offset)? value);
|
||||||
|
void scaleAnimationBuilder(void Function(double)? value);
|
||||||
|
void rotationAnimationBuilder(void Function(double)? value);
|
||||||
|
|
||||||
|
/// Animates multiple fields of the state
|
||||||
|
void animateMultiple({Offset? position, double? scale, double? rotation});
|
||||||
|
|
||||||
/// Add a listener that will ignore updates made internally
|
/// Add a listener that will ignore updates made internally
|
||||||
///
|
///
|
||||||
/// Since it is made for internal use, it is not performatic to use more than one
|
/// Since it is made for internal use, it is not performatic to use more than one
|
||||||
@ -147,12 +154,31 @@ class PhotoViewController
|
|||||||
|
|
||||||
late StreamController<PhotoViewControllerValue> _outputCtrl;
|
late StreamController<PhotoViewControllerValue> _outputCtrl;
|
||||||
|
|
||||||
|
late void Function(Offset)? _animatePosition;
|
||||||
|
late void Function(double)? _animateScale;
|
||||||
|
late void Function(double)? _animateRotation;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<PhotoViewControllerValue> get outputStateStream => _outputCtrl.stream;
|
Stream<PhotoViewControllerValue> get outputStateStream => _outputCtrl.stream;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
late PhotoViewControllerValue prevValue;
|
late PhotoViewControllerValue prevValue;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void positionAnimationBuilder(void Function(Offset)? value) {
|
||||||
|
_animatePosition = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void scaleAnimationBuilder(void Function(double)? value) {
|
||||||
|
_animateScale = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void rotationAnimationBuilder(void Function(double)? value) {
|
||||||
|
_animateRotation = value;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void reset() {
|
void reset() {
|
||||||
value = initial;
|
value = initial;
|
||||||
@ -172,6 +198,21 @@ class PhotoViewController
|
|||||||
_valueNotifier.removeIgnorableListener(callback);
|
_valueNotifier.removeIgnorableListener(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void animateMultiple({Offset? position, double? scale, double? rotation}) {
|
||||||
|
if (position != null && _animatePosition != null) {
|
||||||
|
_animatePosition!(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scale != null && _animateScale != null) {
|
||||||
|
_animateScale!(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rotation != null && _animateRotation != null) {
|
||||||
|
_animateRotation!(rotation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_outputCtrl.close();
|
_outputCtrl.close();
|
||||||
|
@ -111,6 +111,16 @@ mixin PhotoViewControllerDelegate on State<PhotoViewCore> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PhotoViewScaleState getScaleStateFromNewScale(double newScale) {
|
||||||
|
PhotoViewScaleState newScaleState = PhotoViewScaleState.initial;
|
||||||
|
if (scale != scaleBoundaries.initialScale) {
|
||||||
|
newScaleState = (newScale > scaleBoundaries.initialScale)
|
||||||
|
? PhotoViewScaleState.zoomedIn
|
||||||
|
: PhotoViewScaleState.zoomedOut;
|
||||||
|
}
|
||||||
|
return newScaleState;
|
||||||
|
}
|
||||||
|
|
||||||
void updateScaleStateFromNewScale(double newScale) {
|
void updateScaleStateFromNewScale(double newScale) {
|
||||||
PhotoViewScaleState newScaleState = PhotoViewScaleState.initial;
|
PhotoViewScaleState newScaleState = PhotoViewScaleState.initial;
|
||||||
if (scale != scaleBoundaries.initialScale) {
|
if (scale != scaleBoundaries.initialScale) {
|
||||||
|
@ -26,6 +26,8 @@ class PhotoViewScaleStateController {
|
|||||||
StreamController<PhotoViewScaleState>.broadcast()
|
StreamController<PhotoViewScaleState>.broadcast()
|
||||||
..sink.add(PhotoViewScaleState.initial);
|
..sink.add(PhotoViewScaleState.initial);
|
||||||
|
|
||||||
|
bool _hasZoomedOutManually = false;
|
||||||
|
|
||||||
/// The output for state/value updates
|
/// The output for state/value updates
|
||||||
Stream<PhotoViewScaleState> get outputScaleStateStream =>
|
Stream<PhotoViewScaleState> get outputScaleStateStream =>
|
||||||
_outputScaleStateCtrl.stream;
|
_outputScaleStateCtrl.stream;
|
||||||
@ -42,10 +44,20 @@ class PhotoViewScaleStateController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newValue == PhotoViewScaleState.zoomedOut) {
|
||||||
|
_hasZoomedOutManually = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue == PhotoViewScaleState.initial) {
|
||||||
|
_hasZoomedOutManually = false;
|
||||||
|
}
|
||||||
|
|
||||||
prevScaleState = _scaleStateNotifier.value;
|
prevScaleState = _scaleStateNotifier.value;
|
||||||
_scaleStateNotifier.value = newValue;
|
_scaleStateNotifier.value = newValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get hasZoomedOutManually => _hasZoomedOutManually;
|
||||||
|
|
||||||
/// Checks if its actual value is different than previousValue
|
/// Checks if its actual value is different than previousValue
|
||||||
bool get hasChanged => prevScaleState != scaleState;
|
bool get hasChanged => prevScaleState != scaleState;
|
||||||
|
|
||||||
@ -71,6 +83,15 @@ class PhotoViewScaleStateController {
|
|||||||
if (_scaleStateNotifier.value == newValue) {
|
if (_scaleStateNotifier.value == newValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (newValue == PhotoViewScaleState.zoomedOut) {
|
||||||
|
_hasZoomedOutManually = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue == PhotoViewScaleState.initial) {
|
||||||
|
_hasZoomedOutManually = false;
|
||||||
|
}
|
||||||
|
|
||||||
prevScaleState = _scaleStateNotifier.value;
|
prevScaleState = _scaleStateNotifier.value;
|
||||||
_scaleStateNotifier.updateIgnoring(newValue);
|
_scaleStateNotifier.updateIgnoring(newValue);
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ class PhotoViewCore extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.imageProvider,
|
required this.imageProvider,
|
||||||
required this.backgroundDecoration,
|
required this.backgroundDecoration,
|
||||||
|
required this.semanticLabel,
|
||||||
required this.gaplessPlayback,
|
required this.gaplessPlayback,
|
||||||
required this.heroAttributes,
|
required this.heroAttributes,
|
||||||
required this.enableRotation,
|
required this.enableRotation,
|
||||||
@ -48,6 +49,7 @@ class PhotoViewCore extends StatefulWidget {
|
|||||||
required this.tightMode,
|
required this.tightMode,
|
||||||
required this.filterQuality,
|
required this.filterQuality,
|
||||||
required this.disableGestures,
|
required this.disableGestures,
|
||||||
|
required this.disableScaleGestures,
|
||||||
required this.enablePanAlways,
|
required this.enablePanAlways,
|
||||||
}) : customChild = null;
|
}) : customChild = null;
|
||||||
|
|
||||||
@ -73,12 +75,15 @@ class PhotoViewCore extends StatefulWidget {
|
|||||||
required this.tightMode,
|
required this.tightMode,
|
||||||
required this.filterQuality,
|
required this.filterQuality,
|
||||||
required this.disableGestures,
|
required this.disableGestures,
|
||||||
|
required this.disableScaleGestures,
|
||||||
required this.enablePanAlways,
|
required this.enablePanAlways,
|
||||||
}) : imageProvider = null,
|
}) : semanticLabel = null,
|
||||||
|
imageProvider = null,
|
||||||
gaplessPlayback = false;
|
gaplessPlayback = false;
|
||||||
|
|
||||||
final Decoration? backgroundDecoration;
|
final Decoration? backgroundDecoration;
|
||||||
final ImageProvider? imageProvider;
|
final ImageProvider? imageProvider;
|
||||||
|
final String? semanticLabel;
|
||||||
final bool? gaplessPlayback;
|
final bool? gaplessPlayback;
|
||||||
final PhotoViewHeroAttributes? heroAttributes;
|
final PhotoViewHeroAttributes? heroAttributes;
|
||||||
final bool enableRotation;
|
final bool enableRotation;
|
||||||
@ -103,6 +108,7 @@ class PhotoViewCore extends StatefulWidget {
|
|||||||
final HitTestBehavior? gestureDetectorBehavior;
|
final HitTestBehavior? gestureDetectorBehavior;
|
||||||
final bool tightMode;
|
final bool tightMode;
|
||||||
final bool disableGestures;
|
final bool disableGestures;
|
||||||
|
final bool disableScaleGestures;
|
||||||
final bool enablePanAlways;
|
final bool enablePanAlways;
|
||||||
|
|
||||||
final FilterQuality filterQuality;
|
final FilterQuality filterQuality;
|
||||||
@ -120,6 +126,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
TickerProviderStateMixin,
|
TickerProviderStateMixin,
|
||||||
PhotoViewControllerDelegate,
|
PhotoViewControllerDelegate,
|
||||||
HitCornersDetector {
|
HitCornersDetector {
|
||||||
|
Offset? _normalizedPosition;
|
||||||
double? _scaleBefore;
|
double? _scaleBefore;
|
||||||
double? _rotationBefore;
|
double? _rotationBefore;
|
||||||
|
|
||||||
@ -152,32 +159,33 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
void onScaleStart(ScaleStartDetails details) {
|
void onScaleStart(ScaleStartDetails details) {
|
||||||
_rotationBefore = controller.rotation;
|
_rotationBefore = controller.rotation;
|
||||||
_scaleBefore = scale;
|
_scaleBefore = scale;
|
||||||
|
_normalizedPosition = details.focalPoint - controller.position;
|
||||||
_scaleAnimationController.stop();
|
_scaleAnimationController.stop();
|
||||||
_positionAnimationController.stop();
|
_positionAnimationController.stop();
|
||||||
_rotationAnimationController.stop();
|
_rotationAnimationController.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _shouldAllowPanRotate() => switch (scaleStateController.scaleState) {
|
||||||
|
PhotoViewScaleState.zoomedIn =>
|
||||||
|
scaleStateController.hasZoomedOutManually,
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
|
||||||
void onScaleUpdate(ScaleUpdateDetails details) {
|
void onScaleUpdate(ScaleUpdateDetails details) {
|
||||||
final centeredFocalPoint = Offset(
|
|
||||||
details.focalPoint.dx - scaleBoundaries.outerSize.width / 2,
|
|
||||||
details.focalPoint.dy - scaleBoundaries.outerSize.height / 2,
|
|
||||||
);
|
|
||||||
final double newScale = _scaleBefore! * details.scale;
|
final double newScale = _scaleBefore! * details.scale;
|
||||||
final double scaleDelta = newScale / scale;
|
Offset delta = details.focalPoint - _normalizedPosition!;
|
||||||
final Offset newPosition =
|
|
||||||
(controller.position + details.focalPointDelta) * scaleDelta -
|
|
||||||
centeredFocalPoint * (scaleDelta - 1);
|
|
||||||
|
|
||||||
updateScaleStateFromNewScale(newScale);
|
updateScaleStateFromNewScale(newScale);
|
||||||
|
|
||||||
|
final panEnabled = widget.enablePanAlways && _shouldAllowPanRotate();
|
||||||
|
final rotationEnabled = widget.enableRotation && _shouldAllowPanRotate();
|
||||||
|
|
||||||
updateMultiple(
|
updateMultiple(
|
||||||
scale: newScale,
|
scale: newScale,
|
||||||
position: widget.enablePanAlways
|
position:
|
||||||
? newPosition
|
panEnabled ? delta : clampPosition(position: delta * details.scale),
|
||||||
: clampPosition(position: newPosition),
|
rotation: rotationEnabled ? _rotationBefore! + details.rotation : null,
|
||||||
rotation:
|
rotationFocusPoint: rotationEnabled ? details.focalPoint : null,
|
||||||
widget.enableRotation ? _rotationBefore! + details.rotation : null,
|
|
||||||
rotationFocusPoint: widget.enableRotation ? details.focalPoint : null,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,6 +197,16 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
|
|
||||||
widget.onScaleEnd?.call(context, details, controller.value);
|
widget.onScaleEnd?.call(context, details, controller.value);
|
||||||
|
|
||||||
|
final scaleState = getScaleStateFromNewScale(scale);
|
||||||
|
if (scaleState == PhotoViewScaleState.zoomedOut) {
|
||||||
|
scaleStateController.scaleState = PhotoViewScaleState.originalSize;
|
||||||
|
} else if (scaleState == PhotoViewScaleState.zoomedIn) {
|
||||||
|
animateRotation(controller.rotation, 0);
|
||||||
|
if (_shouldAllowPanRotate()) {
|
||||||
|
animatePosition(controller.position, Offset.zero);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//animate back to maxScale if gesture exceeded the maxScale specified
|
//animate back to maxScale if gesture exceeded the maxScale specified
|
||||||
if (s > maxScale) {
|
if (s > maxScale) {
|
||||||
final double scaleComebackRatio = maxScale / s;
|
final double scaleComebackRatio = maxScale / s;
|
||||||
@ -232,6 +250,9 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void animateScale(double from, double to) {
|
void animateScale(double from, double to) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_scaleAnimation = Tween<double>(
|
_scaleAnimation = Tween<double>(
|
||||||
begin: from,
|
begin: from,
|
||||||
end: to,
|
end: to,
|
||||||
@ -242,6 +263,9 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void animatePosition(Offset from, Offset to) {
|
void animatePosition(Offset from, Offset to) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_positionAnimation = Tween<Offset>(begin: from, end: to)
|
_positionAnimation = Tween<Offset>(begin: from, end: to)
|
||||||
.animate(_positionAnimationController);
|
.animate(_positionAnimationController);
|
||||||
_positionAnimationController
|
_positionAnimationController
|
||||||
@ -250,6 +274,9 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void animateRotation(double from, double to) {
|
void animateRotation(double from, double to) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_rotationAnimation = Tween<double>(begin: from, end: to)
|
_rotationAnimation = Tween<double>(begin: from, end: to)
|
||||||
.animate(_rotationAnimationController);
|
.animate(_rotationAnimationController);
|
||||||
_rotationAnimationController
|
_rotationAnimationController
|
||||||
@ -271,11 +298,28 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _animateControllerPosition(Offset position) {
|
||||||
|
animatePosition(controller.position, position);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _animateControllerScale(double scale) {
|
||||||
|
if (controller.scale != null) {
|
||||||
|
animateScale(controller.scale!, scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _animateControllerRotation(double rotation) {
|
||||||
|
animateRotation(controller.rotation, rotation);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
initDelegate();
|
initDelegate();
|
||||||
addAnimateOnScaleStateUpdate(animateOnScaleStateUpdate);
|
addAnimateOnScaleStateUpdate(animateOnScaleStateUpdate);
|
||||||
|
controller.positionAnimationBuilder(_animateControllerPosition);
|
||||||
|
controller.scaleAnimationBuilder(_animateControllerScale);
|
||||||
|
controller.rotationAnimationBuilder(_animateControllerRotation);
|
||||||
|
|
||||||
cachedScaleBoundaries = widget.scaleBoundaries;
|
cachedScaleBoundaries = widget.scaleBoundaries;
|
||||||
|
|
||||||
@ -341,7 +385,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
basePosition,
|
basePosition,
|
||||||
useImageScale,
|
useImageScale,
|
||||||
),
|
),
|
||||||
child: _buildHero(),
|
child: _buildHero(_buildChild()),
|
||||||
);
|
);
|
||||||
|
|
||||||
final child = Container(
|
final child = Container(
|
||||||
@ -363,18 +407,29 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return PhotoViewGestureDetector(
|
return PhotoViewGestureDetector(
|
||||||
onDoubleTap: nextScaleState,
|
disableScaleGestures: widget.disableScaleGestures,
|
||||||
onScaleStart: onScaleStart,
|
onDoubleTap: widget.disableScaleGestures ? null : onDoubleTap,
|
||||||
onScaleUpdate: onScaleUpdate,
|
onScaleStart: widget.disableScaleGestures ? null : onScaleStart,
|
||||||
onScaleEnd: onScaleEnd,
|
onScaleUpdate: widget.disableScaleGestures ? null : onScaleUpdate,
|
||||||
|
onScaleEnd: widget.disableScaleGestures ? null : onScaleEnd,
|
||||||
onDragStart: widget.onDragStart != null
|
onDragStart: widget.onDragStart != null
|
||||||
? (details) => widget.onDragStart!(context, details, value)
|
? (details) => widget.onDragStart!(
|
||||||
|
context,
|
||||||
|
details,
|
||||||
|
widget.controller,
|
||||||
|
widget.scaleStateController,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
onDragEnd: widget.onDragEnd != null
|
onDragEnd: widget.onDragEnd != null
|
||||||
? (details) => widget.onDragEnd!(context, details, value)
|
? (details) =>
|
||||||
|
widget.onDragEnd!(context, details, widget.controller.value)
|
||||||
: null,
|
: null,
|
||||||
onDragUpdate: widget.onDragUpdate != null
|
onDragUpdate: widget.onDragUpdate != null
|
||||||
? (details) => widget.onDragUpdate!(context, details, value)
|
? (details) => widget.onDragUpdate!(
|
||||||
|
context,
|
||||||
|
details,
|
||||||
|
widget.controller.value,
|
||||||
|
)
|
||||||
: null,
|
: null,
|
||||||
hitDetector: this,
|
hitDetector: this,
|
||||||
onTapUp: widget.onTapUp != null
|
onTapUp: widget.onTapUp != null
|
||||||
@ -395,7 +450,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildHero() {
|
Widget _buildHero(Widget child) {
|
||||||
return heroAttributes != null
|
return heroAttributes != null
|
||||||
? Hero(
|
? Hero(
|
||||||
tag: heroAttributes!.tag,
|
tag: heroAttributes!.tag,
|
||||||
@ -403,16 +458,20 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
flightShuttleBuilder: heroAttributes!.flightShuttleBuilder,
|
flightShuttleBuilder: heroAttributes!.flightShuttleBuilder,
|
||||||
placeholderBuilder: heroAttributes!.placeholderBuilder,
|
placeholderBuilder: heroAttributes!.placeholderBuilder,
|
||||||
transitionOnUserGestures: heroAttributes!.transitionOnUserGestures,
|
transitionOnUserGestures: heroAttributes!.transitionOnUserGestures,
|
||||||
child: _buildChild(),
|
child: child,
|
||||||
)
|
)
|
||||||
: _buildChild();
|
: child;
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildChild() {
|
Widget _buildChild() {
|
||||||
return widget.hasCustomChild
|
return widget.hasCustomChild
|
||||||
? widget.customChild!
|
? widget.customChild!
|
||||||
: Image(
|
: Image(
|
||||||
|
key: widget.heroAttributes?.tag != null
|
||||||
|
? ObjectKey(widget.heroAttributes!.tag)
|
||||||
|
: null,
|
||||||
image: widget.imageProvider!,
|
image: widget.imageProvider!,
|
||||||
|
semanticLabel: widget.semanticLabel,
|
||||||
gaplessPlayback: widget.gaplessPlayback ?? false,
|
gaplessPlayback: widget.gaplessPlayback ?? false,
|
||||||
filterQuality: widget.filterQuality,
|
filterQuality: widget.filterQuality,
|
||||||
width: scaleBoundaries.childSize.width * scale,
|
width: scaleBoundaries.childSize.width * scale,
|
||||||
@ -442,6 +501,7 @@ class _CenterWithOriginalSizeDelegate extends SingleChildLayoutDelegate {
|
|||||||
|
|
||||||
final double offsetX = halfWidth * (basePosition.x + 1);
|
final double offsetX = halfWidth * (basePosition.x + 1);
|
||||||
final double offsetY = halfHeight * (basePosition.y + 1);
|
final double offsetY = halfHeight * (basePosition.y + 1);
|
||||||
|
|
||||||
return Offset(offsetX, offsetY);
|
return Offset(offsetX, offsetY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ class PhotoViewGestureDetector extends StatelessWidget {
|
|||||||
this.onTapUp,
|
this.onTapUp,
|
||||||
this.onTapDown,
|
this.onTapDown,
|
||||||
this.behavior,
|
this.behavior,
|
||||||
|
this.disableScaleGestures = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
final GestureDoubleTapCallback? onDoubleTap;
|
final GestureDoubleTapCallback? onDoubleTap;
|
||||||
@ -43,6 +44,8 @@ class PhotoViewGestureDetector extends StatelessWidget {
|
|||||||
|
|
||||||
final HitTestBehavior? behavior;
|
final HitTestBehavior? behavior;
|
||||||
|
|
||||||
|
final bool disableScaleGestures;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final scope = PhotoViewGestureDetectorScope.of(context);
|
final scope = PhotoViewGestureDetectorScope.of(context);
|
||||||
@ -96,9 +99,11 @@ class PhotoViewGestureDetector extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
(PhotoViewGestureRecognizer instance) {
|
(PhotoViewGestureRecognizer instance) {
|
||||||
instance
|
instance
|
||||||
|
..dragStartBehavior = DragStartBehavior.start
|
||||||
..onStart = onScaleStart
|
..onStart = onScaleStart
|
||||||
..onUpdate = onScaleUpdate
|
..onUpdate = onScaleUpdate
|
||||||
..onEnd = onScaleEnd;
|
..onEnd = onScaleEnd
|
||||||
|
..disableScaleGestures = disableScaleGestures;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -124,10 +129,12 @@ class PhotoViewGestureRecognizer extends ScaleGestureRecognizer {
|
|||||||
this.validateAxis,
|
this.validateAxis,
|
||||||
this.touchSlopFactor = 1,
|
this.touchSlopFactor = 1,
|
||||||
PointerDeviceKind? kind,
|
PointerDeviceKind? kind,
|
||||||
|
this.disableScaleGestures = false,
|
||||||
}) : super(supportedDevices: null);
|
}) : super(supportedDevices: null);
|
||||||
final HitCornersDetector? hitDetector;
|
final HitCornersDetector? hitDetector;
|
||||||
final Axis? validateAxis;
|
final Axis? validateAxis;
|
||||||
final double touchSlopFactor;
|
final double touchSlopFactor;
|
||||||
|
bool disableScaleGestures;
|
||||||
|
|
||||||
Map<int, Offset> _pointerLocations = <int, Offset>{};
|
Map<int, Offset> _pointerLocations = <int, Offset>{};
|
||||||
|
|
||||||
@ -155,7 +162,7 @@ class PhotoViewGestureRecognizer extends ScaleGestureRecognizer {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void handleEvent(PointerEvent event) {
|
void handleEvent(PointerEvent event) {
|
||||||
if (validateAxis != null) {
|
if (validateAxis != null && !disableScaleGestures) {
|
||||||
bool didChangeConfiguration = false;
|
bool didChangeConfiguration = false;
|
||||||
if (event is PointerMoveEvent) {
|
if (event is PointerMoveEvent) {
|
||||||
if (!event.synthesized) {
|
if (!event.synthesized) {
|
||||||
|
@ -11,6 +11,7 @@ class ImageWrapper extends StatefulWidget {
|
|||||||
required this.imageProvider,
|
required this.imageProvider,
|
||||||
required this.loadingBuilder,
|
required this.loadingBuilder,
|
||||||
required this.backgroundDecoration,
|
required this.backgroundDecoration,
|
||||||
|
required this.semanticLabel,
|
||||||
required this.gaplessPlayback,
|
required this.gaplessPlayback,
|
||||||
required this.heroAttributes,
|
required this.heroAttributes,
|
||||||
required this.scaleStateChangedCallback,
|
required this.scaleStateChangedCallback,
|
||||||
@ -34,6 +35,7 @@ class ImageWrapper extends StatefulWidget {
|
|||||||
required this.tightMode,
|
required this.tightMode,
|
||||||
required this.filterQuality,
|
required this.filterQuality,
|
||||||
required this.disableGestures,
|
required this.disableGestures,
|
||||||
|
this.disableScaleGestures,
|
||||||
required this.errorBuilder,
|
required this.errorBuilder,
|
||||||
required this.enablePanAlways,
|
required this.enablePanAlways,
|
||||||
required this.index,
|
required this.index,
|
||||||
@ -43,6 +45,7 @@ class ImageWrapper extends StatefulWidget {
|
|||||||
final LoadingBuilder? loadingBuilder;
|
final LoadingBuilder? loadingBuilder;
|
||||||
final ImageErrorWidgetBuilder? errorBuilder;
|
final ImageErrorWidgetBuilder? errorBuilder;
|
||||||
final BoxDecoration backgroundDecoration;
|
final BoxDecoration backgroundDecoration;
|
||||||
|
final String? semanticLabel;
|
||||||
final bool gaplessPlayback;
|
final bool gaplessPlayback;
|
||||||
final PhotoViewHeroAttributes? heroAttributes;
|
final PhotoViewHeroAttributes? heroAttributes;
|
||||||
final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
|
final ValueChanged<PhotoViewScaleState>? scaleStateChangedCallback;
|
||||||
@ -66,6 +69,7 @@ class ImageWrapper extends StatefulWidget {
|
|||||||
final bool? tightMode;
|
final bool? tightMode;
|
||||||
final FilterQuality? filterQuality;
|
final FilterQuality? filterQuality;
|
||||||
final bool? disableGestures;
|
final bool? disableGestures;
|
||||||
|
final bool? disableScaleGestures;
|
||||||
final bool? enablePanAlways;
|
final bool? enablePanAlways;
|
||||||
final int index;
|
final int index;
|
||||||
|
|
||||||
@ -193,6 +197,7 @@ class _ImageWrapperState extends State<ImageWrapper> {
|
|||||||
return PhotoViewCore(
|
return PhotoViewCore(
|
||||||
imageProvider: widget.imageProvider,
|
imageProvider: widget.imageProvider,
|
||||||
backgroundDecoration: widget.backgroundDecoration,
|
backgroundDecoration: widget.backgroundDecoration,
|
||||||
|
semanticLabel: widget.semanticLabel,
|
||||||
gaplessPlayback: widget.gaplessPlayback,
|
gaplessPlayback: widget.gaplessPlayback,
|
||||||
enableRotation: widget.enableRotation,
|
enableRotation: widget.enableRotation,
|
||||||
heroAttributes: widget.heroAttributes,
|
heroAttributes: widget.heroAttributes,
|
||||||
@ -212,6 +217,7 @@ class _ImageWrapperState extends State<ImageWrapper> {
|
|||||||
tightMode: widget.tightMode ?? false,
|
tightMode: widget.tightMode ?? false,
|
||||||
filterQuality: widget.filterQuality ?? FilterQuality.none,
|
filterQuality: widget.filterQuality ?? FilterQuality.none,
|
||||||
disableGestures: widget.disableGestures ?? false,
|
disableGestures: widget.disableGestures ?? false,
|
||||||
|
disableScaleGestures: widget.disableScaleGestures ?? false,
|
||||||
enablePanAlways: widget.enablePanAlways ?? false,
|
enablePanAlways: widget.enablePanAlways ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -266,6 +272,7 @@ class CustomChildWrapper extends StatelessWidget {
|
|||||||
required this.tightMode,
|
required this.tightMode,
|
||||||
required this.filterQuality,
|
required this.filterQuality,
|
||||||
required this.disableGestures,
|
required this.disableGestures,
|
||||||
|
this.disableScaleGestures,
|
||||||
required this.enablePanAlways,
|
required this.enablePanAlways,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -296,6 +303,7 @@ class CustomChildWrapper extends StatelessWidget {
|
|||||||
final HitTestBehavior? gestureDetectorBehavior;
|
final HitTestBehavior? gestureDetectorBehavior;
|
||||||
final bool? tightMode;
|
final bool? tightMode;
|
||||||
final FilterQuality? filterQuality;
|
final FilterQuality? filterQuality;
|
||||||
|
final bool? disableScaleGestures;
|
||||||
final bool? disableGestures;
|
final bool? disableGestures;
|
||||||
final bool? enablePanAlways;
|
final bool? enablePanAlways;
|
||||||
|
|
||||||
@ -330,6 +338,7 @@ class CustomChildWrapper extends StatelessWidget {
|
|||||||
tightMode: tightMode ?? false,
|
tightMode: tightMode ?? false,
|
||||||
filterQuality: filterQuality ?? FilterQuality.none,
|
filterQuality: filterQuality ?? FilterQuality.none,
|
||||||
disableGestures: disableGestures ?? false,
|
disableGestures: disableGestures ?? false,
|
||||||
|
disableScaleGestures: disableScaleGestures ?? false,
|
||||||
enablePanAlways: enablePanAlways ?? false,
|
enablePanAlways: enablePanAlways ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,14 @@ void main() {
|
|||||||
debugLabel: any(named: 'debugLabel'),
|
debugLabel: any(named: 'debugLabel'),
|
||||||
),
|
),
|
||||||
).thenAnswer(successHandler);
|
).thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.updateMemoriesV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.deleteMemoriesV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.updateMemoryAssetsV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any()))
|
||||||
|
.thenAnswer(successHandler);
|
||||||
|
|
||||||
sut = SyncStreamService(
|
sut = SyncStreamService(
|
||||||
syncApiRepository: mockSyncApiRepo,
|
syncApiRepository: mockSyncApiRepo,
|
||||||
@ -227,5 +235,94 @@ void main() {
|
|||||||
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
|
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
test("processes memory sync events successfully", () async {
|
||||||
|
final events = [
|
||||||
|
SyncStreamStub.memoryV1,
|
||||||
|
SyncStreamStub.memoryDeleteV1,
|
||||||
|
SyncStreamStub.memoryToAssetV1,
|
||||||
|
SyncStreamStub.memoryToAssetDeleteV1,
|
||||||
|
];
|
||||||
|
|
||||||
|
await simulateEvents(events);
|
||||||
|
|
||||||
|
verifyInOrder([
|
||||||
|
() => mockSyncStreamRepo.updateMemoriesV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["5"]),
|
||||||
|
() => mockSyncStreamRepo.deleteMemoriesV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["6"]),
|
||||||
|
() => mockSyncStreamRepo.updateMemoryAssetsV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["7"]),
|
||||||
|
() => mockSyncStreamRepo.deleteMemoryAssetsV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["8"]),
|
||||||
|
]);
|
||||||
|
verifyNever(() => mockAbortCallbackWrapper());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("processes mixed memory and user events in correct order", () async {
|
||||||
|
final events = [
|
||||||
|
SyncStreamStub.memoryDeleteV1,
|
||||||
|
SyncStreamStub.userV1Admin,
|
||||||
|
SyncStreamStub.memoryToAssetV1,
|
||||||
|
SyncStreamStub.memoryV1,
|
||||||
|
];
|
||||||
|
|
||||||
|
await simulateEvents(events);
|
||||||
|
|
||||||
|
verifyInOrder([
|
||||||
|
() => mockSyncStreamRepo.deleteMemoriesV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["6"]),
|
||||||
|
() => mockSyncStreamRepo.updateUsersV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["1"]),
|
||||||
|
() => mockSyncStreamRepo.updateMemoryAssetsV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["7"]),
|
||||||
|
() => mockSyncStreamRepo.updateMemoriesV1(any()),
|
||||||
|
() => mockSyncApiRepo.ack(["5"]),
|
||||||
|
]);
|
||||||
|
verifyNever(() => mockAbortCallbackWrapper());
|
||||||
|
});
|
||||||
|
|
||||||
|
test("handles memory sync failure gracefully", () async {
|
||||||
|
when(() => mockSyncStreamRepo.updateMemoriesV1(any()))
|
||||||
|
.thenThrow(Exception("Memory sync failed"));
|
||||||
|
|
||||||
|
final events = [
|
||||||
|
SyncStreamStub.memoryV1,
|
||||||
|
SyncStreamStub.userV1Admin,
|
||||||
|
];
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() async => await simulateEvents(events),
|
||||||
|
throwsA(isA<Exception>()),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("processes memory asset events with correct data types", () async {
|
||||||
|
final events = [SyncStreamStub.memoryToAssetV1];
|
||||||
|
|
||||||
|
await simulateEvents(events);
|
||||||
|
|
||||||
|
verify(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())).called(1);
|
||||||
|
verify(() => mockSyncApiRepo.ack(["7"])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("processes memory delete events with correct data types", () async {
|
||||||
|
final events = [SyncStreamStub.memoryDeleteV1];
|
||||||
|
|
||||||
|
await simulateEvents(events);
|
||||||
|
|
||||||
|
verify(() => mockSyncStreamRepo.deleteMemoriesV1(any())).called(1);
|
||||||
|
verify(() => mockSyncApiRepo.ack(["6"])).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("processes memory create/update events with correct data types",
|
||||||
|
() async {
|
||||||
|
final events = [SyncStreamStub.memoryV1];
|
||||||
|
|
||||||
|
await simulateEvents(events);
|
||||||
|
|
||||||
|
verify(() => mockSyncStreamRepo.updateMemoriesV1(any())).called(1);
|
||||||
|
verify(() => mockSyncApiRepo.ack(["5"])).called(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user