mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
merge main
This commit is contained in:
commit
bdd0a93d7d
@ -62,6 +62,7 @@ Once you have a new OAuth client application configured, Immich can be configure
|
|||||||
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||||
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
||||||
|
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
|
||||||
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage**¹** |
|
||||||
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
|
||||||
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
|
||||||
|
@ -16,7 +16,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
|
|||||||
| `HEIC` | `.heic` | :white_check_mark: | |
|
| `HEIC` | `.heic` | :white_check_mark: | |
|
||||||
| `HEIF` | `.heif` | :white_check_mark: | |
|
| `HEIF` | `.heif` | :white_check_mark: | |
|
||||||
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
||||||
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
||||||
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
||||||
| `PNG` | `.png` | :white_check_mark: | |
|
| `PNG` | `.png` | :white_check_mark: | |
|
||||||
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
||||||
|
@ -227,6 +227,21 @@ describe(`/oauth`, () => {
|
|||||||
expect(user.storageLabel).toBe('user-username');
|
expect(user.storageLabel).toBe('user-username');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set the admin status from a role claim', async () => {
|
||||||
|
const callbackParams = await loginWithOAuth(OAuthUser.WITH_ROLE);
|
||||||
|
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||||
|
expect(status).toBe(201);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
accessToken: expect.any(String),
|
||||||
|
userId: expect.any(String),
|
||||||
|
userEmail: 'oauth-with-role@immich.app',
|
||||||
|
isAdmin: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await getMyUser({ headers: asBearerAuth(body.accessToken) });
|
||||||
|
expect(user.isAdmin).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should work with RS256 signed tokens', async () => {
|
it('should work with RS256 signed tokens', async () => {
|
||||||
await setupOAuth(admin.accessToken, {
|
await setupOAuth(admin.accessToken, {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -12,6 +12,7 @@ export enum OAuthUser {
|
|||||||
NO_NAME = 'no-name',
|
NO_NAME = 'no-name',
|
||||||
WITH_QUOTA = 'with-quota',
|
WITH_QUOTA = 'with-quota',
|
||||||
WITH_USERNAME = 'with-username',
|
WITH_USERNAME = 'with-username',
|
||||||
|
WITH_ROLE = 'with-role',
|
||||||
}
|
}
|
||||||
|
|
||||||
const claims = [
|
const claims = [
|
||||||
@ -34,6 +35,12 @@ const claims = [
|
|||||||
preferred_username: 'user-quota',
|
preferred_username: 'user-quota',
|
||||||
immich_quota: 25,
|
immich_quota: 25,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
sub: OAuthUser.WITH_ROLE,
|
||||||
|
email: 'oauth-with-role@immich.app',
|
||||||
|
email_verified: true,
|
||||||
|
immich_role: 'admin',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const withDefaultClaims = (sub: string) => ({
|
const withDefaultClaims = (sub: string) => ({
|
||||||
@ -64,7 +71,15 @@ const setup = async () => {
|
|||||||
claims: {
|
claims: {
|
||||||
openid: ['sub'],
|
openid: ['sub'],
|
||||||
email: ['email', 'email_verified'],
|
email: ['email', 'email_verified'],
|
||||||
profile: ['name', 'given_name', 'family_name', 'preferred_username', 'immich_quota', 'immich_username'],
|
profile: [
|
||||||
|
'name',
|
||||||
|
'given_name',
|
||||||
|
'family_name',
|
||||||
|
'preferred_username',
|
||||||
|
'immich_quota',
|
||||||
|
'immich_username',
|
||||||
|
'immich_role',
|
||||||
|
],
|
||||||
},
|
},
|
||||||
features: {
|
features: {
|
||||||
jwtUserinfo: {
|
jwtUserinfo: {
|
||||||
|
@ -196,6 +196,8 @@
|
|||||||
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
||||||
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
|
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
|
||||||
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
|
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
|
||||||
|
"oauth_role_claim": "Role Claim",
|
||||||
|
"oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.",
|
||||||
"oauth_settings": "OAuth",
|
"oauth_settings": "OAuth",
|
||||||
"oauth_settings_description": "Manage OAuth login settings",
|
"oauth_settings_description": "Manage OAuth login settings",
|
||||||
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
|
"oauth_settings_more_details": "For more details about this feature, refer to the <link>docs</link>.",
|
||||||
|
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]
|
||||||
|
2
mobile/drift_schemas/main/drift_schema_v1.json
generated
2
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!
|
||||||
@ -48,6 +48,13 @@ sealed class BaseAsset {
|
|||||||
return null;
|
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
|
// Overridden in subclasses
|
||||||
AssetState get storage;
|
AssetState get storage;
|
||||||
String get heroTag;
|
String get heroTag;
|
||||||
|
@ -25,8 +25,6 @@ class LocalAsset extends BaseAsset {
|
|||||||
@override
|
@override
|
||||||
String get heroTag => '${id}_${remoteId ?? checksum}';
|
String get heroTag => '${id}_${remoteId ?? checksum}';
|
||||||
|
|
||||||
bool get hasRemote => remoteId != null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''LocalAsset {
|
return '''LocalAsset {
|
||||||
|
@ -41,8 +41,6 @@ class RemoteAsset extends BaseAsset {
|
|||||||
@override
|
@override
|
||||||
String get heroTag => '${localId ?? checksum}_$id';
|
String get heroTag => '${localId ?? checksum}_$id';
|
||||||
|
|
||||||
bool get hasLocal => localId != null;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return '''Asset {
|
return '''Asset {
|
||||||
@ -85,4 +83,38 @@ class RemoteAsset extends BaseAsset {
|
|||||||
thumbHash.hashCode ^
|
thumbHash.hashCode ^
|
||||||
localDateTime.hashCode ^
|
localDateTime.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,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@ enum Setting<T> {
|
|||||||
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
|
showStorageIndicator<bool>(StoreKey.storageIndicator, true),
|
||||||
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
loadOriginal<bool>(StoreKey.loadOriginal, false),
|
||||||
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
|
preferRemoteImage<bool>(StoreKey.preferRemoteImage, false),
|
||||||
|
advancedTroubleshooting<bool>(StoreKey.advancedTroubleshooting, false),
|
||||||
;
|
;
|
||||||
|
|
||||||
const Setting(this.storeKey, this.defaultValue);
|
const Setting(this.storeKey, this.defaultValue);
|
||||||
|
84
mobile/lib/domain/models/stack.model.dart
Normal file
84
mobile/lib/domain/models/stack.model.dart
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
// Model for a stack stored in the server
|
||||||
|
class Stack {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final String ownerId;
|
||||||
|
final String primaryAssetId;
|
||||||
|
|
||||||
|
const Stack({
|
||||||
|
required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.primaryAssetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
Stack copyWith({
|
||||||
|
String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
String? ownerId,
|
||||||
|
String? primaryAssetId,
|
||||||
|
}) {
|
||||||
|
return Stack(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return <String, dynamic>{
|
||||||
|
'id': id,
|
||||||
|
'createdAt': createdAt.millisecondsSinceEpoch,
|
||||||
|
'updatedAt': updatedAt.millisecondsSinceEpoch,
|
||||||
|
'ownerId': ownerId,
|
||||||
|
'primaryAssetId': primaryAssetId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
factory Stack.fromMap(Map<String, dynamic> map) {
|
||||||
|
return Stack(
|
||||||
|
id: map['id'] as String,
|
||||||
|
createdAt: DateTime.fromMillisecondsSinceEpoch(map['createdAt'] as int),
|
||||||
|
updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updatedAt'] as int),
|
||||||
|
ownerId: map['ownerId'] as String,
|
||||||
|
primaryAssetId: map['primaryAssetId'] as String,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String toJson() => json.encode(toMap());
|
||||||
|
|
||||||
|
factory Stack.fromJson(String source) =>
|
||||||
|
Stack.fromMap(json.decode(source) as Map<String, dynamic>);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'Stack(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, ownerId: $ownerId, primaryAssetId: $primaryAssetId)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant Stack other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.id == id &&
|
||||||
|
other.createdAt == createdAt &&
|
||||||
|
other.updatedAt == updatedAt &&
|
||||||
|
other.ownerId == ownerId &&
|
||||||
|
other.primaryAssetId == primaryAssetId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return id.hashCode ^
|
||||||
|
createdAt.hashCode ^
|
||||||
|
updatedAt.hashCode ^
|
||||||
|
ownerId.hashCode ^
|
||||||
|
primaryAssetId.hashCode;
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,24 @@
|
|||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||||
|
import 'package:immich_mobile/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';
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
final RemoteAssetRepository _remoteAssetRepository;
|
final RemoteAssetRepository _remoteAssetRepository;
|
||||||
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
|
|
||||||
const AssetService({
|
const AssetService({
|
||||||
required RemoteAssetRepository remoteAssetRepository,
|
required RemoteAssetRepository remoteAssetRepository,
|
||||||
}) : _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 {
|
Future<ExifInfo?> getExif(BaseAsset asset) async {
|
||||||
if (asset is LocalAsset || asset is! RemoteAsset) {
|
if (asset is LocalAsset || asset is! RemoteAsset) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -146,6 +146,33 @@ 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());
|
||||||
|
case SyncEntityType.stackV1:
|
||||||
|
return _syncStreamRepository.updateStacksV1(data.cast());
|
||||||
|
case SyncEntityType.stackDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteStacksV1(data.cast());
|
||||||
|
case SyncEntityType.partnerStackV1:
|
||||||
|
return _syncStreamRepository.updateStacksV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'partner',
|
||||||
|
);
|
||||||
|
case SyncEntityType.partnerStackBackfillV1:
|
||||||
|
return _syncStreamRepository.updateStacksV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'partner backfill',
|
||||||
|
);
|
||||||
|
case SyncEntityType.partnerStackDeleteV1:
|
||||||
|
return _syncStreamRepository.deleteStacksV1(
|
||||||
|
data.cast(),
|
||||||
|
debugLabel: 'partner',
|
||||||
|
);
|
||||||
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';
|
||||||
|
|
||||||
@ -52,6 +53,29 @@ class TimelineFactory {
|
|||||||
bucketSource: () =>
|
bucketSource: () =>
|
||||||
_timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy),
|
_timelineRepository.watchRemoteBucket(albumId, groupBy: groupBy),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
TimelineService trash(String userId) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getTrashBucketAssets(userId, offset: offset, count: count),
|
||||||
|
bucketSource: () =>
|
||||||
|
_timelineRepository.watchTrashBucket(userId, groupBy: groupBy),
|
||||||
|
);
|
||||||
|
|
||||||
|
TimelineService archive(String userId) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getArchiveBucketAssets(userId, offset: offset, count: count),
|
||||||
|
bucketSource: () =>
|
||||||
|
_timelineRepository.watchArchiveBucket(userId, groupBy: groupBy),
|
||||||
|
);
|
||||||
|
|
||||||
|
TimelineService lockedFolder(String userId) => TimelineService(
|
||||||
|
assetSource: (offset, count) => _timelineRepository
|
||||||
|
.getLockedFolderBucketAssets(userId, offset: offset, count: count),
|
||||||
|
bucketSource: () => _timelineRepository.watchLockedFolderBucket(
|
||||||
|
userId,
|
||||||
|
groupBy: groupBy,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class TimelineService {
|
class TimelineService {
|
||||||
@ -68,7 +92,7 @@ class TimelineService {
|
|||||||
_bucketSubscription = _bucketSource().listen((buckets) {
|
_bucketSubscription = _bucketSource().listen((buckets) {
|
||||||
_totalAssets =
|
_totalAssets =
|
||||||
buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
buckets.fold<int>(0, (acc, bucket) => acc + bucket.assetCount);
|
||||||
unawaited(reloadBucket());
|
unawaited(_reloadBucket());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,8 +103,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) =>
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -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
mobile/lib/infrastructure/entities/stack.entity.dart
Normal file
22
mobile/lib/infrastructure/entities/stack.entity.dart
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
|
||||||
|
|
||||||
|
class StackEntity extends Table with DriftDefaultsMixin {
|
||||||
|
const StackEntity();
|
||||||
|
|
||||||
|
TextColumn get id => text()();
|
||||||
|
|
||||||
|
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||||
|
|
||||||
|
TextColumn get ownerId =>
|
||||||
|
text().references(UserEntity, #id, onDelete: KeyAction.cascade)();
|
||||||
|
|
||||||
|
TextColumn get primaryAssetId => text().references(RemoteAssetEntity, #id)();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<Column> get primaryKey => {id};
|
||||||
|
}
|
706
mobile/lib/infrastructure/entities/stack.entity.drift.dart
generated
Normal file
706
mobile/lib/infrastructure/entities/stack.entity.drift.dart
generated
Normal file
@ -0,0 +1,706 @@
|
|||||||
|
// dart format width=80
|
||||||
|
// ignore_for_file: type=lint
|
||||||
|
import 'package:drift/drift.dart' as i0;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart'
|
||||||
|
as i1;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.dart' as i2;
|
||||||
|
import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart'
|
||||||
|
as i4;
|
||||||
|
import 'package:drift/internal/modular.dart' as i5;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart'
|
||||||
|
as i6;
|
||||||
|
|
||||||
|
typedef $$StackEntityTableCreateCompanionBuilder = i1.StackEntityCompanion
|
||||||
|
Function({
|
||||||
|
required String id,
|
||||||
|
i0.Value<DateTime> createdAt,
|
||||||
|
i0.Value<DateTime> updatedAt,
|
||||||
|
required String ownerId,
|
||||||
|
required String primaryAssetId,
|
||||||
|
});
|
||||||
|
typedef $$StackEntityTableUpdateCompanionBuilder = i1.StackEntityCompanion
|
||||||
|
Function({
|
||||||
|
i0.Value<String> id,
|
||||||
|
i0.Value<DateTime> createdAt,
|
||||||
|
i0.Value<DateTime> updatedAt,
|
||||||
|
i0.Value<String> ownerId,
|
||||||
|
i0.Value<String> primaryAssetId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final class $$StackEntityTableReferences extends i0.BaseReferences<
|
||||||
|
i0.GeneratedDatabase, i1.$StackEntityTable, i1.StackEntityData> {
|
||||||
|
$$StackEntityTableReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||||
|
|
||||||
|
static i4.$UserEntityTable _ownerIdTable(i0.GeneratedDatabase db) =>
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity')
|
||||||
|
.createAlias(i0.$_aliasNameGenerator(
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i1.$StackEntityTable>('stack_entity')
|
||||||
|
.ownerId,
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity')
|
||||||
|
.id));
|
||||||
|
|
||||||
|
i4.$$UserEntityTableProcessedTableManager get ownerId {
|
||||||
|
final $_column = $_itemColumn<String>('owner_id')!;
|
||||||
|
|
||||||
|
final manager = i4
|
||||||
|
.$$UserEntityTableTableManager(
|
||||||
|
$_db,
|
||||||
|
i5.ReadDatabaseContainer($_db)
|
||||||
|
.resultSet<i4.$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]));
|
||||||
|
}
|
||||||
|
|
||||||
|
static i6.$RemoteAssetEntityTable _primaryAssetIdTable(
|
||||||
|
i0.GeneratedDatabase db) =>
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||||
|
.createAlias(i0.$_aliasNameGenerator(
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i1.$StackEntityTable>('stack_entity')
|
||||||
|
.primaryAssetId,
|
||||||
|
i5.ReadDatabaseContainer(db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity')
|
||||||
|
.id));
|
||||||
|
|
||||||
|
i6.$$RemoteAssetEntityTableProcessedTableManager get primaryAssetId {
|
||||||
|
final $_column = $_itemColumn<String>('primary_asset_id')!;
|
||||||
|
|
||||||
|
final manager = i6
|
||||||
|
.$$RemoteAssetEntityTableTableManager(
|
||||||
|
$_db,
|
||||||
|
i5.ReadDatabaseContainer($_db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'))
|
||||||
|
.filter((f) => f.id.sqlEquals($_column));
|
||||||
|
final item = $_typedResult.readTableOrNull(_primaryAssetIdTable($_db));
|
||||||
|
if (item == null) return manager;
|
||||||
|
return i0.ProcessedTableManager(
|
||||||
|
manager.$state.copyWith(prefetchedData: [item]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$StackEntityTableFilterComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
|
||||||
|
$$StackEntityTableFilterComposer({
|
||||||
|
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));
|
||||||
|
|
||||||
|
i4.$$UserEntityTableFilterComposer get ownerId {
|
||||||
|
final i4.$$UserEntityTableFilterComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i4.$$UserEntityTableFilterComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
|
||||||
|
i6.$$RemoteAssetEntityTableFilterComposer get primaryAssetId {
|
||||||
|
final i6.$$RemoteAssetEntityTableFilterComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.primaryAssetId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i6.$$RemoteAssetEntityTableFilterComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$StackEntityTableOrderingComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
|
||||||
|
$$StackEntityTableOrderingComposer({
|
||||||
|
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));
|
||||||
|
|
||||||
|
i4.$$UserEntityTableOrderingComposer get ownerId {
|
||||||
|
final i4.$$UserEntityTableOrderingComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i4.$$UserEntityTableOrderingComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
|
||||||
|
i6.$$RemoteAssetEntityTableOrderingComposer get primaryAssetId {
|
||||||
|
final i6.$$RemoteAssetEntityTableOrderingComposer composer =
|
||||||
|
$composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.primaryAssetId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i6.$$RemoteAssetEntityTableOrderingComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>(
|
||||||
|
'remote_asset_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$StackEntityTableAnnotationComposer
|
||||||
|
extends i0.Composer<i0.GeneratedDatabase, i1.$StackEntityTable> {
|
||||||
|
$$StackEntityTableAnnotationComposer({
|
||||||
|
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);
|
||||||
|
|
||||||
|
i4.$$UserEntityTableAnnotationComposer get ownerId {
|
||||||
|
final i4.$$UserEntityTableAnnotationComposer composer = $composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.ownerId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i4.$$UserEntityTableAnnotationComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i4.$UserEntityTable>('user_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
|
||||||
|
i6.$$RemoteAssetEntityTableAnnotationComposer get primaryAssetId {
|
||||||
|
final i6.$$RemoteAssetEntityTableAnnotationComposer composer =
|
||||||
|
$composerBuilder(
|
||||||
|
composer: this,
|
||||||
|
getCurrentColumn: (t) => t.primaryAssetId,
|
||||||
|
referencedTable: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>('remote_asset_entity'),
|
||||||
|
getReferencedColumn: (t) => t.id,
|
||||||
|
builder: (joinBuilder,
|
||||||
|
{$addJoinBuilderToRootComposer,
|
||||||
|
$removeJoinBuilderFromRootComposer}) =>
|
||||||
|
i6.$$RemoteAssetEntityTableAnnotationComposer(
|
||||||
|
$db: $db,
|
||||||
|
$table: i5.ReadDatabaseContainer($db)
|
||||||
|
.resultSet<i6.$RemoteAssetEntityTable>(
|
||||||
|
'remote_asset_entity'),
|
||||||
|
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
|
||||||
|
joinBuilder: joinBuilder,
|
||||||
|
$removeJoinBuilderFromRootComposer:
|
||||||
|
$removeJoinBuilderFromRootComposer,
|
||||||
|
));
|
||||||
|
return composer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class $$StackEntityTableTableManager extends i0.RootTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$StackEntityTable,
|
||||||
|
i1.StackEntityData,
|
||||||
|
i1.$$StackEntityTableFilterComposer,
|
||||||
|
i1.$$StackEntityTableOrderingComposer,
|
||||||
|
i1.$$StackEntityTableAnnotationComposer,
|
||||||
|
$$StackEntityTableCreateCompanionBuilder,
|
||||||
|
$$StackEntityTableUpdateCompanionBuilder,
|
||||||
|
(i1.StackEntityData, i1.$$StackEntityTableReferences),
|
||||||
|
i1.StackEntityData,
|
||||||
|
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})> {
|
||||||
|
$$StackEntityTableTableManager(
|
||||||
|
i0.GeneratedDatabase db, i1.$StackEntityTable table)
|
||||||
|
: super(i0.TableManagerState(
|
||||||
|
db: db,
|
||||||
|
table: table,
|
||||||
|
createFilteringComposer: () =>
|
||||||
|
i1.$$StackEntityTableFilterComposer($db: db, $table: table),
|
||||||
|
createOrderingComposer: () =>
|
||||||
|
i1.$$StackEntityTableOrderingComposer($db: db, $table: table),
|
||||||
|
createComputedFieldComposer: () =>
|
||||||
|
i1.$$StackEntityTableAnnotationComposer($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<String> ownerId = const i0.Value.absent(),
|
||||||
|
i0.Value<String> primaryAssetId = const i0.Value.absent(),
|
||||||
|
}) =>
|
||||||
|
i1.StackEntityCompanion(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
primaryAssetId: primaryAssetId,
|
||||||
|
),
|
||||||
|
createCompanionCallback: ({
|
||||||
|
required String id,
|
||||||
|
i0.Value<DateTime> createdAt = const i0.Value.absent(),
|
||||||
|
i0.Value<DateTime> updatedAt = const i0.Value.absent(),
|
||||||
|
required String ownerId,
|
||||||
|
required String primaryAssetId,
|
||||||
|
}) =>
|
||||||
|
i1.StackEntityCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
primaryAssetId: primaryAssetId,
|
||||||
|
),
|
||||||
|
withReferenceMapper: (p0) => p0
|
||||||
|
.map((e) => (
|
||||||
|
e.readTable(table),
|
||||||
|
i1.$$StackEntityTableReferences(db, table, e)
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
prefetchHooksCallback: ({ownerId = false, primaryAssetId = 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.$$StackEntityTableReferences._ownerIdTable(db),
|
||||||
|
referencedColumn:
|
||||||
|
i1.$$StackEntityTableReferences._ownerIdTable(db).id,
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
if (primaryAssetId) {
|
||||||
|
state = state.withJoin(
|
||||||
|
currentTable: table,
|
||||||
|
currentColumn: table.primaryAssetId,
|
||||||
|
referencedTable: i1.$$StackEntityTableReferences
|
||||||
|
._primaryAssetIdTable(db),
|
||||||
|
referencedColumn: i1.$$StackEntityTableReferences
|
||||||
|
._primaryAssetIdTable(db)
|
||||||
|
.id,
|
||||||
|
) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state;
|
||||||
|
},
|
||||||
|
getPrefetchedDataCallback: (items) async {
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef $$StackEntityTableProcessedTableManager = i0.ProcessedTableManager<
|
||||||
|
i0.GeneratedDatabase,
|
||||||
|
i1.$StackEntityTable,
|
||||||
|
i1.StackEntityData,
|
||||||
|
i1.$$StackEntityTableFilterComposer,
|
||||||
|
i1.$$StackEntityTableOrderingComposer,
|
||||||
|
i1.$$StackEntityTableAnnotationComposer,
|
||||||
|
$$StackEntityTableCreateCompanionBuilder,
|
||||||
|
$$StackEntityTableUpdateCompanionBuilder,
|
||||||
|
(i1.StackEntityData, i1.$$StackEntityTableReferences),
|
||||||
|
i1.StackEntityData,
|
||||||
|
i0.PrefetchHooks Function({bool ownerId, bool primaryAssetId})>;
|
||||||
|
|
||||||
|
class $StackEntityTable extends i2.StackEntity
|
||||||
|
with i0.TableInfo<$StackEntityTable, i1.StackEntityData> {
|
||||||
|
@override
|
||||||
|
final i0.GeneratedDatabase attachedDatabase;
|
||||||
|
final String? _alias;
|
||||||
|
$StackEntityTable(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: i3.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: i3.currentDateAndTime);
|
||||||
|
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'));
|
||||||
|
static const i0.VerificationMeta _primaryAssetIdMeta =
|
||||||
|
const i0.VerificationMeta('primaryAssetId');
|
||||||
|
@override
|
||||||
|
late final i0.GeneratedColumn<String> primaryAssetId =
|
||||||
|
i0.GeneratedColumn<String>(
|
||||||
|
'primary_asset_id', aliasedName, false,
|
||||||
|
type: i0.DriftSqlType.string,
|
||||||
|
requiredDuringInsert: true,
|
||||||
|
defaultConstraints: i0.GeneratedColumn.constraintIsAlways(
|
||||||
|
'REFERENCES remote_asset_entity (id)'));
|
||||||
|
@override
|
||||||
|
List<i0.GeneratedColumn> get $columns =>
|
||||||
|
[id, createdAt, updatedAt, ownerId, primaryAssetId];
|
||||||
|
@override
|
||||||
|
String get aliasedName => _alias ?? actualTableName;
|
||||||
|
@override
|
||||||
|
String get actualTableName => $name;
|
||||||
|
static const String $name = 'stack_entity';
|
||||||
|
@override
|
||||||
|
i0.VerificationContext validateIntegrity(
|
||||||
|
i0.Insertable<i1.StackEntityData> 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('owner_id')) {
|
||||||
|
context.handle(_ownerIdMeta,
|
||||||
|
ownerId.isAcceptableOrUnknown(data['owner_id']!, _ownerIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_ownerIdMeta);
|
||||||
|
}
|
||||||
|
if (data.containsKey('primary_asset_id')) {
|
||||||
|
context.handle(
|
||||||
|
_primaryAssetIdMeta,
|
||||||
|
primaryAssetId.isAcceptableOrUnknown(
|
||||||
|
data['primary_asset_id']!, _primaryAssetIdMeta));
|
||||||
|
} else if (isInserting) {
|
||||||
|
context.missing(_primaryAssetIdMeta);
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<i0.GeneratedColumn> get $primaryKey => {id};
|
||||||
|
@override
|
||||||
|
i1.StackEntityData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||||
|
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||||
|
return i1.StackEntityData(
|
||||||
|
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'])!,
|
||||||
|
ownerId: attachedDatabase.typeMapping
|
||||||
|
.read(i0.DriftSqlType.string, data['${effectivePrefix}owner_id'])!,
|
||||||
|
primaryAssetId: attachedDatabase.typeMapping.read(
|
||||||
|
i0.DriftSqlType.string, data['${effectivePrefix}primary_asset_id'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
$StackEntityTable createAlias(String alias) {
|
||||||
|
return $StackEntityTable(attachedDatabase, alias);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get withoutRowId => true;
|
||||||
|
@override
|
||||||
|
bool get isStrict => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StackEntityData extends i0.DataClass
|
||||||
|
implements i0.Insertable<i1.StackEntityData> {
|
||||||
|
final String id;
|
||||||
|
final DateTime createdAt;
|
||||||
|
final DateTime updatedAt;
|
||||||
|
final String ownerId;
|
||||||
|
final String primaryAssetId;
|
||||||
|
const StackEntityData(
|
||||||
|
{required this.id,
|
||||||
|
required this.createdAt,
|
||||||
|
required this.updatedAt,
|
||||||
|
required this.ownerId,
|
||||||
|
required this.primaryAssetId});
|
||||||
|
@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);
|
||||||
|
map['owner_id'] = i0.Variable<String>(ownerId);
|
||||||
|
map['primary_asset_id'] = i0.Variable<String>(primaryAssetId);
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
factory StackEntityData.fromJson(Map<String, dynamic> json,
|
||||||
|
{i0.ValueSerializer? serializer}) {
|
||||||
|
serializer ??= i0.driftRuntimeOptions.defaultSerializer;
|
||||||
|
return StackEntityData(
|
||||||
|
id: serializer.fromJson<String>(json['id']),
|
||||||
|
createdAt: serializer.fromJson<DateTime>(json['createdAt']),
|
||||||
|
updatedAt: serializer.fromJson<DateTime>(json['updatedAt']),
|
||||||
|
ownerId: serializer.fromJson<String>(json['ownerId']),
|
||||||
|
primaryAssetId: serializer.fromJson<String>(json['primaryAssetId']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@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),
|
||||||
|
'ownerId': serializer.toJson<String>(ownerId),
|
||||||
|
'primaryAssetId': serializer.toJson<String>(primaryAssetId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.StackEntityData copyWith(
|
||||||
|
{String? id,
|
||||||
|
DateTime? createdAt,
|
||||||
|
DateTime? updatedAt,
|
||||||
|
String? ownerId,
|
||||||
|
String? primaryAssetId}) =>
|
||||||
|
i1.StackEntityData(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
|
||||||
|
);
|
||||||
|
StackEntityData copyWithCompanion(i1.StackEntityCompanion data) {
|
||||||
|
return StackEntityData(
|
||||||
|
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,
|
||||||
|
ownerId: data.ownerId.present ? data.ownerId.value : this.ownerId,
|
||||||
|
primaryAssetId: data.primaryAssetId.present
|
||||||
|
? data.primaryAssetId.value
|
||||||
|
: this.primaryAssetId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('StackEntityData(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('updatedAt: $updatedAt, ')
|
||||||
|
..write('ownerId: $ownerId, ')
|
||||||
|
..write('primaryAssetId: $primaryAssetId')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
Object.hash(id, createdAt, updatedAt, ownerId, primaryAssetId);
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) =>
|
||||||
|
identical(this, other) ||
|
||||||
|
(other is i1.StackEntityData &&
|
||||||
|
other.id == this.id &&
|
||||||
|
other.createdAt == this.createdAt &&
|
||||||
|
other.updatedAt == this.updatedAt &&
|
||||||
|
other.ownerId == this.ownerId &&
|
||||||
|
other.primaryAssetId == this.primaryAssetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
class StackEntityCompanion extends i0.UpdateCompanion<i1.StackEntityData> {
|
||||||
|
final i0.Value<String> id;
|
||||||
|
final i0.Value<DateTime> createdAt;
|
||||||
|
final i0.Value<DateTime> updatedAt;
|
||||||
|
final i0.Value<String> ownerId;
|
||||||
|
final i0.Value<String> primaryAssetId;
|
||||||
|
const StackEntityCompanion({
|
||||||
|
this.id = const i0.Value.absent(),
|
||||||
|
this.createdAt = const i0.Value.absent(),
|
||||||
|
this.updatedAt = const i0.Value.absent(),
|
||||||
|
this.ownerId = const i0.Value.absent(),
|
||||||
|
this.primaryAssetId = const i0.Value.absent(),
|
||||||
|
});
|
||||||
|
StackEntityCompanion.insert({
|
||||||
|
required String id,
|
||||||
|
this.createdAt = const i0.Value.absent(),
|
||||||
|
this.updatedAt = const i0.Value.absent(),
|
||||||
|
required String ownerId,
|
||||||
|
required String primaryAssetId,
|
||||||
|
}) : id = i0.Value(id),
|
||||||
|
ownerId = i0.Value(ownerId),
|
||||||
|
primaryAssetId = i0.Value(primaryAssetId);
|
||||||
|
static i0.Insertable<i1.StackEntityData> custom({
|
||||||
|
i0.Expression<String>? id,
|
||||||
|
i0.Expression<DateTime>? createdAt,
|
||||||
|
i0.Expression<DateTime>? updatedAt,
|
||||||
|
i0.Expression<String>? ownerId,
|
||||||
|
i0.Expression<String>? primaryAssetId,
|
||||||
|
}) {
|
||||||
|
return i0.RawValuesInsertable({
|
||||||
|
if (id != null) 'id': id,
|
||||||
|
if (createdAt != null) 'created_at': createdAt,
|
||||||
|
if (updatedAt != null) 'updated_at': updatedAt,
|
||||||
|
if (ownerId != null) 'owner_id': ownerId,
|
||||||
|
if (primaryAssetId != null) 'primary_asset_id': primaryAssetId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
i1.StackEntityCompanion copyWith(
|
||||||
|
{i0.Value<String>? id,
|
||||||
|
i0.Value<DateTime>? createdAt,
|
||||||
|
i0.Value<DateTime>? updatedAt,
|
||||||
|
i0.Value<String>? ownerId,
|
||||||
|
i0.Value<String>? primaryAssetId}) {
|
||||||
|
return i1.StackEntityCompanion(
|
||||||
|
id: id ?? this.id,
|
||||||
|
createdAt: createdAt ?? this.createdAt,
|
||||||
|
updatedAt: updatedAt ?? this.updatedAt,
|
||||||
|
ownerId: ownerId ?? this.ownerId,
|
||||||
|
primaryAssetId: primaryAssetId ?? this.primaryAssetId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 (ownerId.present) {
|
||||||
|
map['owner_id'] = i0.Variable<String>(ownerId.value);
|
||||||
|
}
|
||||||
|
if (primaryAssetId.present) {
|
||||||
|
map['primary_asset_id'] = i0.Variable<String>(primaryAssetId.value);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return (StringBuffer('StackEntityCompanion(')
|
||||||
|
..write('id: $id, ')
|
||||||
|
..write('createdAt: $createdAt, ')
|
||||||
|
..write('updatedAt: $updatedAt, ')
|
||||||
|
..write('ownerId: $ownerId, ')
|
||||||
|
..write('primaryAssetId: $primaryAssetId')
|
||||||
|
..write(')'))
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
@ -7,11 +7,14 @@ 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';
|
||||||
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/remote_album_user.entity.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/entities/stack.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
|
||||||
import 'package:isar/isar.dart';
|
import 'package:isar/isar.dart';
|
||||||
@ -46,6 +49,9 @@ class IsarDatabaseRepository implements IDatabaseRepository {
|
|||||||
RemoteAlbumEntity,
|
RemoteAlbumEntity,
|
||||||
RemoteAlbumAssetEntity,
|
RemoteAlbumAssetEntity,
|
||||||
RemoteAlbumUserEntity,
|
RemoteAlbumUserEntity,
|
||||||
|
MemoryEntity,
|
||||||
|
MemoryAssetEntity,
|
||||||
|
StackEntity,
|
||||||
],
|
],
|
||||||
include: {
|
include: {
|
||||||
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
'package:immich_mobile/infrastructure/entities/merged_asset.drift',
|
||||||
|
@ -23,9 +23,15 @@ 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/stack.entity.drift.dart'
|
||||||
|
as i14;
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
|
||||||
|
as i15;
|
||||||
|
import 'package:drift/internal/modular.dart' as i16;
|
||||||
|
|
||||||
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 +57,12 @@ 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);
|
||||||
|
late final i14.$StackEntityTable stackEntity = i14.$StackEntityTable(this);
|
||||||
|
i15.MergedAssetDrift get mergedAssetDrift => i16.ReadDatabaseContainer(this)
|
||||||
|
.accessor<i15.MergedAssetDrift>(i15.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 +81,10 @@ abstract class $Drift extends i0.GeneratedDatabase {
|
|||||||
remoteExifEntity,
|
remoteExifEntity,
|
||||||
remoteAlbumEntity,
|
remoteAlbumEntity,
|
||||||
remoteAlbumAssetEntity,
|
remoteAlbumAssetEntity,
|
||||||
remoteAlbumUserEntity
|
remoteAlbumUserEntity,
|
||||||
|
memoryEntity,
|
||||||
|
memoryAssetEntity,
|
||||||
|
stackEntity
|
||||||
];
|
];
|
||||||
@override
|
@override
|
||||||
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
i0.StreamQueryUpdateRules get streamUpdateRules =>
|
||||||
@ -175,6 +188,34 @@ 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
i0.WritePropagation(
|
||||||
|
on: i0.TableUpdateQuery.onTableName('user_entity',
|
||||||
|
limitUpdateKind: i0.UpdateKind.delete),
|
||||||
|
result: [
|
||||||
|
i0.TableUpdate('stack_entity', kind: i0.UpdateKind.delete),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@override
|
@override
|
||||||
@ -208,4 +249,10 @@ 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);
|
||||||
|
i14.$$StackEntityTableTableManager get stackEntity =>
|
||||||
|
i14.$$StackEntityTableTableManager(_db, _db.stackEntity);
|
||||||
}
|
}
|
||||||
|
@ -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: [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -13,6 +13,26 @@ class RemoteAssetRepository extends DriftDatabaseRepository {
|
|||||||
final Drift _db;
|
final Drift _db;
|
||||||
const RemoteAssetRepository(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) {
|
Future<ExifInfo?> getExif(String id) {
|
||||||
return _db.managers.remoteExifEntity
|
return _db.managers.remoteExifEntity
|
||||||
.filter((row) => row.assetId.id.equals(id))
|
.filter((row) => row.assetId.id.equals(id))
|
||||||
|
30
mobile/lib/infrastructure/repositories/stack.repository.dart
Normal file
30
mobile/lib/infrastructure/repositories/stack.repository.dart
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/stack.model.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/entities/stack.entity.drift.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||||
|
|
||||||
|
class DriftStackRepository extends DriftDatabaseRepository {
|
||||||
|
final Drift _db;
|
||||||
|
const DriftStackRepository(this._db) : super(_db);
|
||||||
|
|
||||||
|
Future<List<Stack>> getAll(String userId) {
|
||||||
|
final query = _db.stackEntity.select()
|
||||||
|
..where((e) => e.ownerId.equals(userId));
|
||||||
|
|
||||||
|
return query.map((stack) {
|
||||||
|
return stack.toDto();
|
||||||
|
}).get();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension on StackEntityData {
|
||||||
|
Stack toDto() {
|
||||||
|
return Stack(
|
||||||
|
id: id,
|
||||||
|
createdAt: createdAt,
|
||||||
|
updatedAt: updatedAt,
|
||||||
|
ownerId: ownerId,
|
||||||
|
primaryAssetId: primaryAssetId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -52,6 +52,10 @@ class SyncApiRepository {
|
|||||||
SyncRequestType.albumAssetsV1,
|
SyncRequestType.albumAssetsV1,
|
||||||
SyncRequestType.albumAssetExifsV1,
|
SyncRequestType.albumAssetExifsV1,
|
||||||
SyncRequestType.albumToAssetsV1,
|
SyncRequestType.albumToAssetsV1,
|
||||||
|
SyncRequestType.memoriesV1,
|
||||||
|
SyncRequestType.memoryToAssetsV1,
|
||||||
|
SyncRequestType.stacksV1,
|
||||||
|
SyncRequestType.partnerStacksV1,
|
||||||
],
|
],
|
||||||
).toJson(),
|
).toJson(),
|
||||||
);
|
);
|
||||||
@ -157,6 +161,15 @@ 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,
|
||||||
|
SyncEntityType.stackV1: SyncStackV1.fromJson,
|
||||||
|
SyncEntityType.stackDeleteV1: SyncStackDeleteV1.fromJson,
|
||||||
|
SyncEntityType.partnerStackV1: SyncStackV1.fromJson,
|
||||||
|
SyncEntityType.partnerStackBackfillV1: SyncStackV1.fromJson,
|
||||||
|
SyncEntityType.partnerStackDeleteV1: SyncStackDeleteV1.fromJson,
|
||||||
};
|
};
|
||||||
|
|
||||||
class _SyncAckV1 {
|
class _SyncAckV1 {
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
|
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/stack.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 +70,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: SyncPartnerDeleteV1', e, s);
|
_logger.severe('Error: SyncPartnerDeleteV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -87,8 +93,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: SyncPartnerV1', e, s);
|
_logger.severe('Error: SyncPartnerV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,10 +104,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((e) => e.assetId)),
|
||||||
} catch (e, s) {
|
);
|
||||||
_logger.severe('Error: deleteAssetsV1 - $debugLabel', e, s);
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: deleteAssetsV1 - $debugLabel', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -136,8 +143,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: updateAssetsV1 - $debugLabel', e, s);
|
_logger.severe('Error: updateAssetsV1 - $debugLabel', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -180,18 +187,23 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: updateAssetsExifV1 - $debugLabel', e, s);
|
_logger.severe(
|
||||||
|
'Error: updateAssetsExifV1 - $debugLabel',
|
||||||
|
error,
|
||||||
|
stack,
|
||||||
|
);
|
||||||
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, stack) {
|
||||||
|
_logger.severe('Error: deleteAlbumsV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -218,8 +230,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: updateAlbumsV1', e, s);
|
_logger.severe('Error: updateAlbumsV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,8 +249,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteAlbumUsersV1', e, s);
|
_logger.severe('Error: deleteAlbumUsersV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -264,8 +276,12 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: updateAlbumUsersV1 - $debugLabel', e, s);
|
_logger.severe(
|
||||||
|
'Error: updateAlbumUsersV1 - $debugLabel',
|
||||||
|
error,
|
||||||
|
stack,
|
||||||
|
);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -285,8 +301,8 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: deleteAlbumToAssetsV1', e, s);
|
_logger.severe('Error: deleteAlbumToAssetsV1', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,8 +326,137 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e, s) {
|
} catch (error, stack) {
|
||||||
_logger.severe('Error: updateAlbumToAssetsV1 - $debugLabel', e, s);
|
_logger.severe(
|
||||||
|
'Error: updateAlbumToAssetsV1 - $debugLabel',
|
||||||
|
error,
|
||||||
|
stack,
|
||||||
|
);
|
||||||
|
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, stack) {
|
||||||
|
_logger.severe('Error: updateMemoriesV1', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteMemoriesV1(Iterable<SyncMemoryDeleteV1> data) async {
|
||||||
|
try {
|
||||||
|
await _db.memoryEntity.deleteWhere(
|
||||||
|
(row) => row.id.isIn(data.map((e) => e.memoryId)),
|
||||||
|
);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: deleteMemoriesV1', error, stack);
|
||||||
|
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, stack) {
|
||||||
|
_logger.severe('Error: updateMemoryAssetsV1', error, stack);
|
||||||
|
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, stack) {
|
||||||
|
_logger.severe('Error: deleteMemoryAssetsV1', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateStacksV1(
|
||||||
|
Iterable<SyncStackV1> data, {
|
||||||
|
String debugLabel = 'user',
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _db.batch((batch) {
|
||||||
|
for (final stack in data) {
|
||||||
|
final companion = StackEntityCompanion(
|
||||||
|
createdAt: Value(stack.createdAt),
|
||||||
|
updatedAt: Value(stack.updatedAt),
|
||||||
|
ownerId: Value(stack.ownerId),
|
||||||
|
primaryAssetId: Value(stack.primaryAssetId),
|
||||||
|
);
|
||||||
|
|
||||||
|
batch.insert(
|
||||||
|
_db.stackEntity,
|
||||||
|
companion.copyWith(id: Value(stack.id)),
|
||||||
|
onConflict: DoUpdate((_) => companion),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: updateStacksV1 - $debugLabel', error, stack);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteStacksV1(
|
||||||
|
Iterable<SyncStackDeleteV1> data, {
|
||||||
|
String debugLabel = 'user',
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _db.stackEntity.deleteWhere(
|
||||||
|
(row) => row.id.isIn(data.map((e) => e.stackId)),
|
||||||
|
);
|
||||||
|
} catch (error, stack) {
|
||||||
|
_logger.severe('Error: deleteStacksV1 - $debugLabel', error, stack);
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -335,6 +480,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,
|
||||||
|
@ -213,6 +213,158 @@ class DriftTimelineRepository extends DriftDatabaseRepository {
|
|||||||
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
.map((row) => row.readTable(_db.remoteAssetEntity).toDto())
|
||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> watchTrashBucket(
|
||||||
|
String userId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
return _db.remoteAssetEntity
|
||||||
|
.count(
|
||||||
|
where: (row) =>
|
||||||
|
row.deletedAt.isNotNull() & row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
.map(_generateBuckets)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
|
_db.remoteAssetEntity.deletedAt.isNotNull(),
|
||||||
|
)
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getTrashBucketAssets(
|
||||||
|
String userId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.remoteAssetEntity.select()
|
||||||
|
..where(
|
||||||
|
(row) => row.deletedAt.isNotNull() & row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> watchArchiveBucket(
|
||||||
|
String userId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
return _db.remoteAssetEntity
|
||||||
|
.count(
|
||||||
|
where: (row) =>
|
||||||
|
row.visibility.equalsValue(AssetVisibility.archive) &
|
||||||
|
row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
.map(_generateBuckets)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
|
_db.remoteAssetEntity.visibility
|
||||||
|
.equalsValue(AssetVisibility.archive),
|
||||||
|
)
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getArchiveBucketAssets(
|
||||||
|
String userId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.remoteAssetEntity.select()
|
||||||
|
..where(
|
||||||
|
(row) =>
|
||||||
|
row.ownerId.equals(userId) &
|
||||||
|
row.visibility.equalsValue(AssetVisibility.archive),
|
||||||
|
)
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<Bucket>> watchLockedFolderBucket(
|
||||||
|
String userId, {
|
||||||
|
GroupAssetsBy groupBy = GroupAssetsBy.day,
|
||||||
|
}) {
|
||||||
|
if (groupBy == GroupAssetsBy.none) {
|
||||||
|
return _db.remoteAssetEntity
|
||||||
|
.count(
|
||||||
|
where: (row) =>
|
||||||
|
row.visibility.equalsValue(AssetVisibility.locked) &
|
||||||
|
row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
.map(_generateBuckets)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
final assetCountExp = _db.remoteAssetEntity.id.count();
|
||||||
|
final dateExp = _db.remoteAssetEntity.createdAt.dateFmt(groupBy);
|
||||||
|
|
||||||
|
final query = _db.remoteAssetEntity.selectOnly()
|
||||||
|
..addColumns([assetCountExp, dateExp])
|
||||||
|
..where(
|
||||||
|
_db.remoteAssetEntity.ownerId.equals(userId) &
|
||||||
|
_db.remoteAssetEntity.visibility
|
||||||
|
.equalsValue(AssetVisibility.locked),
|
||||||
|
)
|
||||||
|
..groupBy([dateExp])
|
||||||
|
..orderBy([OrderingTerm.desc(dateExp)]);
|
||||||
|
|
||||||
|
return query.map((row) {
|
||||||
|
final timeline = row.read(dateExp)!.dateFmt(groupBy);
|
||||||
|
final assetCount = row.read(assetCountExp)!;
|
||||||
|
return TimeBucket(date: timeline, assetCount: assetCount);
|
||||||
|
}).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<BaseAsset>> getLockedFolderBucketAssets(
|
||||||
|
String userId, {
|
||||||
|
required int offset,
|
||||||
|
required int count,
|
||||||
|
}) {
|
||||||
|
final query = _db.remoteAssetEntity.select()
|
||||||
|
..where(
|
||||||
|
(row) =>
|
||||||
|
row.visibility.equalsValue(AssetVisibility.locked) &
|
||||||
|
row.ownerId.equals(userId),
|
||||||
|
)
|
||||||
|
..orderBy([(row) => OrderingTerm.desc(row.createdAt)])
|
||||||
|
..limit(count, offset: offset);
|
||||||
|
|
||||||
|
return query.map((row) => row.toDto()).get();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension on Expression<DateTime> {
|
extension on Expression<DateTime> {
|
||||||
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
33
mobile/lib/presentation/pages/dev/drift_archive.page.dart
Normal file
33
mobile/lib/presentation/pages/dev/drift_archive.page.dart
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftArchivePage extends StatelessWidget {
|
||||||
|
const DriftArchivePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User must be logged in to access archive');
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).archive(user.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftLockedFolderPage extends StatelessWidget {
|
||||||
|
const DriftLockedFolderPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User must be logged in to access locked folder');
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).lockedFolder(user.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
33
mobile/lib/presentation/pages/dev/drift_trash.page.dart
Normal file
33
mobile/lib/presentation/pages/dev/drift_trash.page.dart
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
|
@RoutePage()
|
||||||
|
class DriftTrashPage extends StatelessWidget {
|
||||||
|
const DriftTrashPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
timelineServiceProvider.overrideWith(
|
||||||
|
(ref) {
|
||||||
|
final user = ref.watch(currentUserProvider);
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User must be logged in to access trash');
|
||||||
|
}
|
||||||
|
|
||||||
|
final timelineService =
|
||||||
|
ref.watch(timelineFactoryProvider).trash(user.id);
|
||||||
|
ref.onDispose(timelineService.dispose);
|
||||||
|
return timelineService;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: const Timeline(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -66,6 +66,9 @@ final _features = [
|
|||||||
await db.remoteAlbumEntity.deleteAll();
|
await db.remoteAlbumEntity.deleteAll();
|
||||||
await db.remoteAlbumUserEntity.deleteAll();
|
await db.remoteAlbumUserEntity.deleteAll();
|
||||||
await db.remoteAlbumAssetEntity.deleteAll();
|
await db.remoteAlbumAssetEntity.deleteAll();
|
||||||
|
await db.memoryEntity.deleteAll();
|
||||||
|
await db.memoryAssetEntity.deleteAll();
|
||||||
|
await db.stackEntity.deleteAll();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_Feature(
|
_Feature(
|
||||||
@ -96,6 +99,21 @@ final _features = [
|
|||||||
icon: Icons.timeline_rounded,
|
icon: Icons.timeline_rounded,
|
||||||
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
onTap: (ctx, _) => ctx.pushRoute(const TabShellRoute()),
|
||||||
),
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Trash',
|
||||||
|
icon: Icons.delete_outline_rounded,
|
||||||
|
onTap: (ctx, _) => ctx.pushRoute(const DriftTrashRoute()),
|
||||||
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Archive',
|
||||||
|
icon: Icons.archive_outlined,
|
||||||
|
onTap: (ctx, _) => ctx.pushRoute(const DriftArchiveRoute()),
|
||||||
|
),
|
||||||
|
_Feature(
|
||||||
|
name: 'Locked Folder',
|
||||||
|
icon: Icons.lock_outline_rounded,
|
||||||
|
onTap: (ctx, _) => ctx.pushRoute(const DriftLockedFolderRoute()),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
|
@ -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,18 @@ 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(),
|
||||||
|
),
|
||||||
|
_Stat(
|
||||||
|
name: 'Stacks',
|
||||||
|
load: (db) => db.managers.stackEntity.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,
|
||||||
),
|
),
|
||||||
|
@ -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 DeletePermanentActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).delete(source);
|
final result = await ref.read(actionProvider.notifier).delete(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'delete_action_prompt'.t(
|
final successMessage = 'delete_action_prompt'.t(
|
||||||
|
@ -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(
|
||||||
|
@ -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 TrashActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).trash(source);
|
final result = await ref.read(actionProvider.notifier).trash(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'trash_action_prompt'.t(
|
final successMessage = 'trash_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';
|
||||||
|
|
||||||
@ -20,7 +19,6 @@ class UnarchiveActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).unArchive(source);
|
final result = await ref.read(actionProvider.notifier).unArchive(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'unarchive_action_prompt'.t(
|
final successMessage = 'unarchive_action_prompt'.t(
|
||||||
|
@ -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 UnFavoriteActionButton extends ConsumerWidget {
|
class UnFavoriteActionButton extends ConsumerWidget {
|
||||||
final ActionSource source;
|
final ActionSource source;
|
||||||
|
final bool menuItem;
|
||||||
|
|
||||||
const UnFavoriteActionButton({super.key, required this.source});
|
const UnFavoriteActionButton({
|
||||||
|
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 UnFavoriteActionButton extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await ref.read(actionProvider.notifier).unFavorite(source);
|
final result = await ref.read(actionProvider.notifier).unFavorite(source);
|
||||||
await ref.read(timelineServiceProvider).reloadBucket();
|
|
||||||
|
if (source == ActionSource.viewer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
|
|
||||||
final successMessage = 'unfavorite_action_prompt'.t(
|
final successMessage = 'unfavorite_action_prompt'.t(
|
||||||
@ -46,6 +54,7 @@ class UnFavoriteActionButton extends ConsumerWidget {
|
|||||||
iconData: Icons.favorite_rounded,
|
iconData: Icons.favorite_rounded,
|
||||||
label: "unfavorite".t(context: context),
|
label: "unfavorite".t(context: context),
|
||||||
onPressed: () => _onTap(context, ref),
|
onPressed: () => _onTap(context, ref),
|
||||||
|
menuItem: menuItem,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,13 @@ 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/services/timeline.service.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/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
import 'package:immich_mobile/extensions/scroll_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet.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/image_provider.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.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/asset_viewer/current_asset.provider.dart';
|
||||||
@ -57,9 +61,10 @@ const double _kBottomSheetSnapExtent = 0.7;
|
|||||||
class _AssetViewerState extends ConsumerState<AssetViewer> {
|
class _AssetViewerState extends ConsumerState<AssetViewer> {
|
||||||
late PageController pageController;
|
late PageController pageController;
|
||||||
late DraggableScrollableController bottomSheetController;
|
late DraggableScrollableController bottomSheetController;
|
||||||
PersistentBottomSheetController? sheetCloseNotifier;
|
PersistentBottomSheetController? sheetCloseController;
|
||||||
// PhotoViewGallery takes care of disposing it's controllers
|
// PhotoViewGallery takes care of disposing it's controllers
|
||||||
PhotoViewControllerBase? viewController;
|
PhotoViewControllerBase? viewController;
|
||||||
|
StreamSubscription? reloadSubscription;
|
||||||
|
|
||||||
late Platform platform;
|
late Platform platform;
|
||||||
late PhotoViewControllerValue initialPhotoViewState;
|
late PhotoViewControllerValue initialPhotoViewState;
|
||||||
@ -68,12 +73,11 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
bool blockGestures = false;
|
bool blockGestures = false;
|
||||||
bool dragInProgress = false;
|
bool dragInProgress = false;
|
||||||
bool shouldPopOnDrag = false;
|
bool shouldPopOnDrag = false;
|
||||||
bool showingBottomSheet = false;
|
|
||||||
double? initialScale;
|
double? initialScale;
|
||||||
double previousExtent = _kBottomSheetMinimumExtent;
|
double previousExtent = _kBottomSheetMinimumExtent;
|
||||||
Offset dragDownPosition = Offset.zero;
|
Offset dragDownPosition = Offset.zero;
|
||||||
int totalAssets = 0;
|
int totalAssets = 0;
|
||||||
int backgroundOpacity = 255;
|
BuildContext? scaffoldContext;
|
||||||
|
|
||||||
// Delayed operations that should be cancelled on disposal
|
// Delayed operations that should be cancelled on disposal
|
||||||
final List<Timer> _delayedOperations = [];
|
final List<Timer> _delayedOperations = [];
|
||||||
@ -88,6 +92,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
_onAssetChanged(widget.initialIndex);
|
_onAssetChanged(widget.initialIndex);
|
||||||
});
|
});
|
||||||
|
reloadSubscription = EventStream.shared.listen(_onEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -95,14 +100,17 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
pageController.dispose();
|
pageController.dispose();
|
||||||
bottomSheetController.dispose();
|
bottomSheetController.dispose();
|
||||||
_cancelTimers();
|
_cancelTimers();
|
||||||
|
reloadSubscription?.cancel();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool get showingBottomSheet =>
|
||||||
|
ref.read(assetViewerProvider.select((s) => s.showingBottomSheet));
|
||||||
|
|
||||||
Color get backgroundColor {
|
Color get backgroundColor {
|
||||||
if (showingBottomSheet && !context.isDarkTheme) {
|
final opacity =
|
||||||
return Colors.white;
|
ref.read(assetViewerProvider.select((s) => s.backgroundOpacity));
|
||||||
}
|
return Colors.black.withAlpha(opacity);
|
||||||
return Colors.black.withAlpha(backgroundOpacity);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _cancelTimers() {
|
void _cancelTimers() {
|
||||||
@ -119,6 +127,9 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
(viewController?.prevValue.scale ?? viewController?.value.scale ?? 1.0) +
|
(viewController?.prevValue.scale ?? viewController?.value.scale ?? 1.0) +
|
||||||
0.01;
|
0.01;
|
||||||
|
|
||||||
|
double _getVerticalOffsetForBottomSheet(double extent) =>
|
||||||
|
(context.height * extent) - (context.height * _kBottomSheetMinimumExtent);
|
||||||
|
|
||||||
Future<void> _precacheImage(int index) async {
|
Future<void> _precacheImage(int index) async {
|
||||||
if (!mounted || index < 0 || index >= totalAssets) {
|
if (!mounted || index < 0 || index >= totalAssets) {
|
||||||
return;
|
return;
|
||||||
@ -186,11 +197,12 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
void _onDragStart(
|
void _onDragStart(
|
||||||
_,
|
_,
|
||||||
DragStartDetails details,
|
DragStartDetails details,
|
||||||
PhotoViewControllerValue value,
|
PhotoViewControllerBase controller,
|
||||||
PhotoViewScaleStateController scaleStateController,
|
PhotoViewScaleStateController scaleStateController,
|
||||||
) {
|
) {
|
||||||
|
viewController = controller;
|
||||||
dragDownPosition = details.localPosition;
|
dragDownPosition = details.localPosition;
|
||||||
initialPhotoViewState = value;
|
initialPhotoViewState = controller.value;
|
||||||
final isZoomed =
|
final isZoomed =
|
||||||
scaleStateController.scaleState == PhotoViewScaleState.zoomedIn ||
|
scaleStateController.scaleState == PhotoViewScaleState.zoomedIn ||
|
||||||
scaleStateController.scaleState == PhotoViewScaleState.covering;
|
scaleStateController.scaleState == PhotoViewScaleState.covering;
|
||||||
@ -220,16 +232,14 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
|
||||||
shouldPopOnDrag = false;
|
shouldPopOnDrag = false;
|
||||||
hasDraggedDown = null;
|
hasDraggedDown = null;
|
||||||
backgroundOpacity = 255;
|
|
||||||
viewController?.animateMultiple(
|
viewController?.animateMultiple(
|
||||||
position: initialPhotoViewState.position,
|
position: initialPhotoViewState.position,
|
||||||
scale: initialPhotoViewState.scale,
|
scale: initialPhotoViewState.scale,
|
||||||
rotation: initialPhotoViewState.rotation,
|
rotation: initialPhotoViewState.rotation,
|
||||||
);
|
);
|
||||||
});
|
ref.read(assetViewerProvider.notifier).setOpacity(255);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDragUpdate(BuildContext ctx, DragUpdateDetails details, _) {
|
void _onDragUpdate(BuildContext ctx, DragUpdateDetails details, _) {
|
||||||
@ -250,18 +260,10 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
|
|
||||||
void _handleDragUp(BuildContext ctx, Offset delta) {
|
void _handleDragUp(BuildContext ctx, Offset delta) {
|
||||||
const double openThreshold = 50;
|
const double openThreshold = 50;
|
||||||
const double closeThreshold = 25;
|
|
||||||
|
|
||||||
final position = initialPhotoViewState.position + Offset(0, delta.dy);
|
final position = initialPhotoViewState.position + Offset(0, delta.dy);
|
||||||
final distanceToOrigin = position.distance;
|
final distanceToOrigin = position.distance;
|
||||||
|
|
||||||
if (showingBottomSheet && distanceToOrigin < closeThreshold) {
|
|
||||||
// Prevents the user from dragging the bottom sheet further down
|
|
||||||
blockGestures = true;
|
|
||||||
sheetCloseNotifier?.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
viewController?.updateMultiple(position: position);
|
viewController?.updateMultiple(position: position);
|
||||||
// Moves the bottom sheet when the asset is being dragged up
|
// Moves the bottom sheet when the asset is being dragged up
|
||||||
if (showingBottomSheet && bottomSheetController.isAttached) {
|
if (showingBottomSheet && bottomSheetController.isAttached) {
|
||||||
@ -274,68 +276,37 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _openBottomSheet(BuildContext ctx) {
|
void _handleDragDown(BuildContext ctx, Offset delta) {
|
||||||
setState(() {
|
const double dragRatio = 0.2;
|
||||||
initialScale = viewController?.scale;
|
const double popThreshold = 75;
|
||||||
viewController?.animateMultiple(scale: _getScaleForBottomSheet);
|
|
||||||
showingBottomSheet = true;
|
final distance = delta.distance;
|
||||||
previousExtent = _kBottomSheetMinimumExtent;
|
shouldPopOnDrag = delta.dy > 0 && distance > popThreshold;
|
||||||
sheetCloseNotifier = showBottomSheet(
|
|
||||||
context: ctx,
|
final maxScaleDistance = ctx.height * 0.5;
|
||||||
sheetAnimationStyle: AnimationStyle(
|
final scaleReduction = (distance / maxScaleDistance).clamp(0.0, dragRatio);
|
||||||
duration: Duration.zero,
|
double? updatedScale;
|
||||||
reverseDuration: Duration.zero,
|
if (initialPhotoViewState.scale != null) {
|
||||||
),
|
updatedScale = initialPhotoViewState.scale! * (1.0 - scaleReduction);
|
||||||
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: _kBottomSheetMinimumExtent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
sheetCloseNotifier?.closed.then((_) => _handleSheetClose());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleSheetClose() {
|
final backgroundOpacity =
|
||||||
setState(() {
|
(255 * (1.0 - (scaleReduction / dragRatio))).round();
|
||||||
showingBottomSheet = false;
|
|
||||||
sheetCloseNotifier = null;
|
viewController?.updateMultiple(
|
||||||
viewController?.animateMultiple(
|
position: initialPhotoViewState.position + delta,
|
||||||
position: Offset.zero,
|
scale: updatedScale,
|
||||||
scale: initialScale,
|
|
||||||
);
|
);
|
||||||
shouldPopOnDrag = false;
|
ref.read(assetViewerProvider.notifier).setOpacity(backgroundOpacity);
|
||||||
hasDraggedDown = null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _snapBottomSheet() {
|
void _onTapDown(_, __, ___) {
|
||||||
if (bottomSheetController.size > _kBottomSheetSnapExtent ||
|
if (!showingBottomSheet) {
|
||||||
bottomSheetController.size < 0.4) {
|
ref.read(assetViewerProvider.notifier).toggleControls();
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
isSnapping = true;
|
|
||||||
bottomSheetController.animateTo(
|
|
||||||
_kBottomSheetSnapExtent,
|
|
||||||
duration: Durations.short3,
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _onNotification(Notification delta) {
|
bool _onNotification(Notification delta) {
|
||||||
// Ignore notifications when user dragging the asset
|
|
||||||
if (dragInProgress) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delta is DraggableScrollableNotification) {
|
if (delta is DraggableScrollableNotification) {
|
||||||
_handleDraggableNotification(delta);
|
_handleDraggableNotification(delta);
|
||||||
}
|
}
|
||||||
@ -350,50 +321,117 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleDraggableNotification(DraggableScrollableNotification delta) {
|
void _handleDraggableNotification(DraggableScrollableNotification delta) {
|
||||||
final verticalOffset = (context.height * delta.extent) -
|
final currentExtent = delta.extent;
|
||||||
(context.height * _kBottomSheetMinimumExtent);
|
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
|
// Moves the asset when the bottom sheet is being dragged
|
||||||
if (verticalOffset > 0) {
|
if (verticalOffset > 0) {
|
||||||
viewController?.position = Offset(0, -verticalOffset);
|
viewController?.position = Offset(0, -verticalOffset);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final currentExtent = delta.extent;
|
void _onEvent(Event event) {
|
||||||
final isDraggingDown = currentExtent < previousExtent;
|
if (event is TimelineReloadEvent) {
|
||||||
previousExtent = currentExtent;
|
_onTimelineReload(event);
|
||||||
// Closes the bottom sheet if the user is dragging down and the extent is less than the snap extent
|
return;
|
||||||
if (isDraggingDown && delta.extent < _kBottomSheetSnapExtent - 0.1) {
|
}
|
||||||
sheetCloseNotifier?.close();
|
|
||||||
|
if (event is ViewerOpenBottomSheetEvent) {
|
||||||
|
final extent = _kBottomSheetMinimumExtent + 0.3;
|
||||||
|
_openBottomSheet(scaffoldContext!, extent: extent);
|
||||||
|
final offset = _getVerticalOffsetForBottomSheet(extent);
|
||||||
|
viewController?.position = Offset(0, -offset);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _handleDragDown(BuildContext ctx, Offset delta) {
|
void _onTimelineReload(_) {
|
||||||
const double dragRatio = 0.2;
|
|
||||||
const double popThreshold = 75;
|
|
||||||
|
|
||||||
final distance = delta.distance;
|
|
||||||
final newShouldPopOnDrag = 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 newBackgroundOpacity =
|
|
||||||
(255 * (1.0 - (scaleReduction / dragRatio))).round();
|
|
||||||
|
|
||||||
viewController?.updateMultiple(
|
|
||||||
position: initialPhotoViewState.position + delta,
|
|
||||||
scale: updatedScale,
|
|
||||||
);
|
|
||||||
if (shouldPopOnDrag != newShouldPopOnDrag ||
|
|
||||||
backgroundOpacity != newBackgroundOpacity) {
|
|
||||||
setState(() {
|
setState(() {
|
||||||
shouldPopOnDrag = newShouldPopOnDrag;
|
totalAssets = ref.read(timelineServiceProvider).totalAssets;
|
||||||
backgroundOpacity = newBackgroundOpacity;
|
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(
|
Widget _placeholderBuilder(
|
||||||
@ -418,12 +456,13 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
|
PhotoViewGalleryPageOptions _assetBuilder(BuildContext ctx, int index) {
|
||||||
|
scaffoldContext ??= ctx;
|
||||||
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
final asset = ref.read(timelineServiceProvider).getAsset(index);
|
||||||
final size = Size(ctx.width, ctx.height);
|
final size = Size(ctx.width, ctx.height);
|
||||||
final imageProvider = getFullImageProvider(asset, size: size);
|
|
||||||
|
|
||||||
return PhotoViewGalleryPageOptions(
|
return PhotoViewGalleryPageOptions(
|
||||||
imageProvider: imageProvider,
|
key: ValueKey(asset.heroTag),
|
||||||
|
imageProvider: getFullImageProvider(asset, size: size),
|
||||||
heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag),
|
heroAttributes: PhotoViewHeroAttributes(tag: asset.heroTag),
|
||||||
filterQuality: FilterQuality.high,
|
filterQuality: FilterQuality.high,
|
||||||
tightMode: true,
|
tightMode: true,
|
||||||
@ -433,6 +472,7 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
onDragStart: _onDragStart,
|
onDragStart: _onDragStart,
|
||||||
onDragUpdate: _onDragUpdate,
|
onDragUpdate: _onDragUpdate,
|
||||||
onDragEnd: _onDragEnd,
|
onDragEnd: _onDragEnd,
|
||||||
|
onTapDown: _onTapDown,
|
||||||
errorBuilder: (_, __, ___) => Container(
|
errorBuilder: (_, __, ___) => Container(
|
||||||
width: ctx.width,
|
width: ctx.width,
|
||||||
height: ctx.height,
|
height: ctx.height,
|
||||||
@ -446,13 +486,27 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onPop<T>(bool didPop, T? result) {
|
||||||
|
ref.read(currentAssetNotifier.notifier).dispose();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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.
|
// 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
|
// Issue: https://github.com/flutter/flutter/issues/109037
|
||||||
// TODO: Add a custom scrum builder once the fix lands on stable
|
// TODO: Add a custom scrum builder once the fix lands on stable
|
||||||
return Scaffold(
|
return PopScope(
|
||||||
backgroundColor: Colors.black.withAlpha(backgroundOpacity),
|
onPopInvokedWithResult: _onPop,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
appBar: const ViewerTopAppBar(),
|
||||||
|
extendBody: true,
|
||||||
|
extendBodyBehindAppBar: true,
|
||||||
body: PhotoViewGallery.builder(
|
body: PhotoViewGallery.builder(
|
||||||
gaplessPlayback: true,
|
gaplessPlayback: true,
|
||||||
loadingBuilder: _placeholderBuilder,
|
loadingBuilder: _placeholderBuilder,
|
||||||
@ -468,6 +522,8 @@ class _AssetViewerState extends ConsumerState<AssetViewer> {
|
|||||||
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
backgroundDecoration: BoxDecoration(color: backgroundColor),
|
||||||
enablePanAlways: true,
|
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),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +1,81 @@
|
|||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.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/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/exif.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/build_context_extensions.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/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/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/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/utils/bytes_units.dart';
|
import 'package:immich_mobile/utils/bytes_units.dart';
|
||||||
|
|
||||||
const _kSeparator = ' • ';
|
const _kSeparator = ' • ';
|
||||||
|
|
||||||
class AssetDetailBottomSheet extends BaseBottomSheet {
|
class AssetDetailBottomSheet extends ConsumerWidget {
|
||||||
|
final DraggableScrollableController? controller;
|
||||||
|
final double initialChildSize;
|
||||||
|
|
||||||
const AssetDetailBottomSheet({
|
const AssetDetailBottomSheet({
|
||||||
super.controller,
|
this.controller,
|
||||||
super.initialChildSize,
|
this.initialChildSize = 0.35,
|
||||||
super.key,
|
super.key,
|
||||||
}) : super(
|
});
|
||||||
actions: const [],
|
|
||||||
|
@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()],
|
slivers: const [_AssetDetailBottomSheet()],
|
||||||
|
controller: controller,
|
||||||
|
initialChildSize: initialChildSize,
|
||||||
minChildSize: 0.1,
|
minChildSize: 0.1,
|
||||||
maxChildSize: 1.0,
|
maxChildSize: 0.88,
|
||||||
expand: false,
|
expand: false,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
resizeOnScroll: false,
|
resizeOnScroll: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _AssetDetailBottomSheet extends ConsumerWidget {
|
class _AssetDetailBottomSheet extends ConsumerWidget {
|
||||||
const _AssetDetailBottomSheet();
|
const _AssetDetailBottomSheet();
|
||||||
@ -88,6 +138,10 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final asset = ref.watch(currentAssetNotifier);
|
final asset = ref.watch(currentAssetNotifier);
|
||||||
|
if (asset == null) {
|
||||||
|
return const SliverToBoxAdapter(child: SizedBox.shrink());
|
||||||
|
}
|
||||||
|
|
||||||
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull;
|
||||||
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
final cameraTitle = _getCameraInfoTitle(exifInfo);
|
||||||
|
|
||||||
@ -96,16 +150,16 @@ class _AssetDetailBottomSheet extends ConsumerWidget {
|
|||||||
// Asset Date and Time
|
// Asset Date and Time
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
title: _getDateTime(context, asset),
|
title: _getDateTime(context, asset),
|
||||||
titleStyle: context.textTheme.bodyLarge
|
titleStyle: context.textTheme.bodyLarge?.copyWith(
|
||||||
?.copyWith(fontWeight: FontWeight.w600),
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SheetLocationDetails(),
|
||||||
// Details header
|
// Details header
|
||||||
_SheetTile(
|
_SheetTile(
|
||||||
title: 'exif_bottom_sheet_details'.t(context: context),
|
title: 'exif_bottom_sheet_details'.t(context: context),
|
||||||
titleStyle: context.textTheme.labelLarge?.copyWith(
|
titleStyle: context.textTheme.labelLarge,
|
||||||
color: context.textTheme.labelLarge?.color,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
// File info
|
// File info
|
||||||
_SheetTile(
|
_SheetTile(
|
@ -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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -89,17 +89,17 @@ class _BaseDraggableScrollableSheetState
|
|||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
if (widget.actions.isNotEmpty)
|
if (widget.actions.isNotEmpty)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 80,
|
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 SizedBox(height: 14),
|
if (widget.actions.isNotEmpty) ...[
|
||||||
if (widget.actions.isNotEmpty)
|
const Divider(indent: 16, endIndent: 16),
|
||||||
const Divider(indent: 20, endIndent: 20),
|
const SizedBox(height: 16),
|
||||||
if (widget.actions.isNotEmpty) const SizedBox(height: 14),
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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';
|
||||||
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -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';
|
||||||
@ -55,7 +56,10 @@ class ActionNotifier extends Notifier<void> {
|
|||||||
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) {
|
||||||
|
@ -15,5 +15,6 @@ final remoteAssetRepositoryProvider = Provider<RemoteAssetRepository>(
|
|||||||
final assetServiceProvider = Provider(
|
final assetServiceProvider = Provider(
|
||||||
(ref) => AssetService(
|
(ref) => AssetService(
|
||||||
remoteAssetRepository: ref.watch(remoteAssetRepositoryProvider),
|
remoteAssetRepository: ref.watch(remoteAssetRepositoryProvider),
|
||||||
|
localAssetRepository: ref.watch(localAssetRepository),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -1,26 +1,48 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset.provider.dart';
|
||||||
|
|
||||||
final currentAssetNotifier =
|
final currentAssetNotifier =
|
||||||
NotifierProvider<CurrentAssetNotifier, BaseAsset>(CurrentAssetNotifier.new);
|
AutoDisposeNotifierProvider<CurrentAssetNotifier, BaseAsset?>(
|
||||||
|
CurrentAssetNotifier.new,
|
||||||
class CurrentAssetNotifier extends Notifier<BaseAsset> {
|
|
||||||
@override
|
|
||||||
BaseAsset build() {
|
|
||||||
throw UnimplementedError(
|
|
||||||
'An asset must be set before using the currentAssetProvider.',
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
class CurrentAssetNotifier extends AutoDisposeNotifier<BaseAsset?> {
|
||||||
|
KeepAliveLink? _keepAliveLink;
|
||||||
|
StreamSubscription<BaseAsset?>? _assetSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
BaseAsset? build() => null;
|
||||||
|
|
||||||
void setAsset(BaseAsset asset) {
|
void setAsset(BaseAsset asset) {
|
||||||
|
_keepAliveLink?.close();
|
||||||
|
_assetSubscription?.cancel();
|
||||||
state = asset;
|
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(
|
final currentAssetExifProvider = FutureProvider.autoDispose(
|
||||||
(ref) {
|
(ref) {
|
||||||
final currentAsset = ref.watch(currentAssetNotifier);
|
final currentAsset = ref.watch(currentAssetNotifier);
|
||||||
|
if (currentAsset == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return ref.watch(assetServiceProvider).getExif(currentAsset);
|
return ref.watch(assetServiceProvider).getExif(currentAsset);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
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);
|
||||||
|
});
|
7
mobile/lib/providers/stack.provider.dart
Normal file
7
mobile/lib/providers/stack.provider.dart
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/stack.repository.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/db.provider.dart';
|
||||||
|
|
||||||
|
final driftStackProvider = Provider<DriftStackRepository>(
|
||||||
|
(ref) => DriftStackRepository(ref.watch(driftProvider)),
|
||||||
|
);
|
@ -40,6 +40,9 @@ class AuthRepository extends DatabaseRepository {
|
|||||||
_drift.remoteAlbumEntity.deleteAll(),
|
_drift.remoteAlbumEntity.deleteAll(),
|
||||||
_drift.remoteAlbumAssetEntity.deleteAll(),
|
_drift.remoteAlbumAssetEntity.deleteAll(),
|
||||||
_drift.remoteAlbumUserEntity.deleteAll(),
|
_drift.remoteAlbumUserEntity.deleteAll(),
|
||||||
|
_drift.memoryEntity.deleteAll(),
|
||||||
|
_drift.memoryAssetEntity.deleteAll(),
|
||||||
|
_drift.stackEntity.deleteAll(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ 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/domain/services/timeline.service.dart';
|
||||||
import 'package:immich_mobile/entities/album.entity.dart';
|
import 'package:immich_mobile/entities/album.entity.dart';
|
||||||
@ -65,12 +66,16 @@ import 'package:immich_mobile/pages/search/person_result.page.dart';
|
|||||||
import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
import 'package:immich_mobile/pages/search/recently_taken.page.dart';
|
||||||
import 'package:immich_mobile/pages/search/search.page.dart';
|
import 'package:immich_mobile/pages/search/search.page.dart';
|
||||||
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
import 'package:immich_mobile/pages/share_intent/share_intent.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/drift_trash.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/drift_archive.page.dart';
|
||||||
|
import 'package:immich_mobile/presentation/pages/dev/drift_locked_folder.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/feat_in_development.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart';
|
import 'package:immich_mobile/presentation/pages/dev/local_timeline.page.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/dev/main_timeline.page.dart';
|
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/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';
|
||||||
@ -83,6 +88,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';
|
||||||
@ -385,6 +391,22 @@ class AppRouter extends RootStackRouter {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftMemoryRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftTrashRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftArchiveRoute.page,
|
||||||
|
guards: [_authGuard, _duplicateGuard],
|
||||||
|
),
|
||||||
|
AutoRoute(
|
||||||
|
page: DriftLockedFolderRoute.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: '/'),
|
||||||
|
@ -618,6 +618,106 @@ class DriftAlbumsRoute extends PageRouteInfo<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftArchivePage]
|
||||||
|
class DriftArchiveRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftArchiveRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftArchiveRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftArchiveRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftArchivePage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// generated route for
|
||||||
|
/// [DriftLockedFolderPage]
|
||||||
|
class DriftLockedFolderRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftLockedFolderRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftLockedFolderRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftLockedFolderRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftLockedFolderPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
/// [DriftTrashPage]
|
||||||
|
class DriftTrashRoute extends PageRouteInfo<void> {
|
||||||
|
const DriftTrashRoute({List<PageRouteInfo>? children})
|
||||||
|
: super(DriftTrashRoute.name, initialChildren: children);
|
||||||
|
|
||||||
|
static const String name = 'DriftTrashRoute';
|
||||||
|
|
||||||
|
static PageInfo page = PageInfo(
|
||||||
|
name,
|
||||||
|
builder: (data) {
|
||||||
|
return const DriftTrashPage();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// generated route for
|
/// generated route for
|
||||||
/// [EditImagePage]
|
/// [EditImagePage]
|
||||||
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
|
class EditImageRoute extends PageRouteInfo<EditImageRouteArgs> {
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
@ -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(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
import 'package:immich_mobile/domain/models/store.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/models/user_metadata.model.dart';
|
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|
||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/widgets/common/transparent_image.dart';
|
import 'package:immich_mobile/widgets/common/transparent_image.dart';
|
||||||
|
|
||||||
@ -26,7 +24,7 @@ class UserCircleAvatar extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
bool isDarkTheme = context.themeData.brightness == Brightness.dark;
|
final userAvatarColor = user.avatarColor.toColor();
|
||||||
final profileImageUrl =
|
final profileImageUrl =
|
||||||
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
|
'${Store.get(StoreKey.serverEndpoint)}/users/${user.id}/profile-image?d=${Random().nextInt(1024)}';
|
||||||
|
|
||||||
@ -34,14 +32,14 @@ class UserCircleAvatar extends ConsumerWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: isDarkTheme && user.avatarColor == AvatarColor.primary
|
color: userAvatarColor.computeLuminance() > 0.5
|
||||||
? Colors.black
|
? Colors.black
|
||||||
: Colors.white,
|
: Colors.white,
|
||||||
),
|
),
|
||||||
child: Text(user.name[0].toUpperCase()),
|
child: Text(user.name[0].toUpperCase()),
|
||||||
);
|
);
|
||||||
return CircleAvatar(
|
return CircleAvatar(
|
||||||
backgroundColor: user.avatarColor.toColor(),
|
backgroundColor: userAvatarColor,
|
||||||
radius: radius,
|
radius: radius,
|
||||||
child: user.profileImagePath == null
|
child: user.profileImagePath == null
|
||||||
? textIcon
|
? textIcon
|
||||||
|
@ -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(
|
||||||
|
@ -660,7 +660,7 @@ typedef PhotoViewImageTapDownCallback = Function(
|
|||||||
typedef PhotoViewImageDragStartCallback = Function(
|
typedef PhotoViewImageDragStartCallback = Function(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
DragStartDetails details,
|
DragStartDetails details,
|
||||||
PhotoViewControllerValue controllerValue,
|
PhotoViewControllerBase controllerValue,
|
||||||
PhotoViewScaleStateController scaleStateController,
|
PhotoViewScaleStateController scaleStateController,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -271,7 +271,7 @@ 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,
|
||||||
@ -304,7 +304,7 @@ class _PhotoViewGalleryState extends State<PhotoViewGallery> {
|
|||||||
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,
|
||||||
@ -363,7 +363,7 @@ 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.semanticLabel,
|
||||||
@ -392,6 +392,7 @@ 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.semanticLabel,
|
||||||
@ -418,6 +419,8 @@ class PhotoViewGalleryPageOptions {
|
|||||||
}) : 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;
|
||||||
|
|
||||||
|
@ -416,7 +416,7 @@ class PhotoViewCoreState extends State<PhotoViewCore>
|
|||||||
? (details) => widget.onDragStart!(
|
? (details) => widget.onDragStart!(
|
||||||
context,
|
context,
|
||||||
details,
|
details,
|
||||||
widget.controller.value,
|
widget.controller,
|
||||||
widget.scaleStateController,
|
widget.scaleStateController,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
@ -24,6 +24,7 @@ class SystemConfigOAuthDto {
|
|||||||
required this.mobileOverrideEnabled,
|
required this.mobileOverrideEnabled,
|
||||||
required this.mobileRedirectUri,
|
required this.mobileRedirectUri,
|
||||||
required this.profileSigningAlgorithm,
|
required this.profileSigningAlgorithm,
|
||||||
|
required this.roleClaim,
|
||||||
required this.scope,
|
required this.scope,
|
||||||
required this.signingAlgorithm,
|
required this.signingAlgorithm,
|
||||||
required this.storageLabelClaim,
|
required this.storageLabelClaim,
|
||||||
@ -55,6 +56,8 @@ class SystemConfigOAuthDto {
|
|||||||
|
|
||||||
String profileSigningAlgorithm;
|
String profileSigningAlgorithm;
|
||||||
|
|
||||||
|
String roleClaim;
|
||||||
|
|
||||||
String scope;
|
String scope;
|
||||||
|
|
||||||
String signingAlgorithm;
|
String signingAlgorithm;
|
||||||
@ -81,6 +84,7 @@ class SystemConfigOAuthDto {
|
|||||||
other.mobileOverrideEnabled == mobileOverrideEnabled &&
|
other.mobileOverrideEnabled == mobileOverrideEnabled &&
|
||||||
other.mobileRedirectUri == mobileRedirectUri &&
|
other.mobileRedirectUri == mobileRedirectUri &&
|
||||||
other.profileSigningAlgorithm == profileSigningAlgorithm &&
|
other.profileSigningAlgorithm == profileSigningAlgorithm &&
|
||||||
|
other.roleClaim == roleClaim &&
|
||||||
other.scope == scope &&
|
other.scope == scope &&
|
||||||
other.signingAlgorithm == signingAlgorithm &&
|
other.signingAlgorithm == signingAlgorithm &&
|
||||||
other.storageLabelClaim == storageLabelClaim &&
|
other.storageLabelClaim == storageLabelClaim &&
|
||||||
@ -102,6 +106,7 @@ class SystemConfigOAuthDto {
|
|||||||
(mobileOverrideEnabled.hashCode) +
|
(mobileOverrideEnabled.hashCode) +
|
||||||
(mobileRedirectUri.hashCode) +
|
(mobileRedirectUri.hashCode) +
|
||||||
(profileSigningAlgorithm.hashCode) +
|
(profileSigningAlgorithm.hashCode) +
|
||||||
|
(roleClaim.hashCode) +
|
||||||
(scope.hashCode) +
|
(scope.hashCode) +
|
||||||
(signingAlgorithm.hashCode) +
|
(signingAlgorithm.hashCode) +
|
||||||
(storageLabelClaim.hashCode) +
|
(storageLabelClaim.hashCode) +
|
||||||
@ -110,7 +115,7 @@ class SystemConfigOAuthDto {
|
|||||||
(tokenEndpointAuthMethod.hashCode);
|
(tokenEndpointAuthMethod.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
|
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, roleClaim=$roleClaim, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim, timeout=$timeout, tokenEndpointAuthMethod=$tokenEndpointAuthMethod]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -129,6 +134,7 @@ class SystemConfigOAuthDto {
|
|||||||
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
|
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
|
||||||
json[r'mobileRedirectUri'] = this.mobileRedirectUri;
|
json[r'mobileRedirectUri'] = this.mobileRedirectUri;
|
||||||
json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm;
|
json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm;
|
||||||
|
json[r'roleClaim'] = this.roleClaim;
|
||||||
json[r'scope'] = this.scope;
|
json[r'scope'] = this.scope;
|
||||||
json[r'signingAlgorithm'] = this.signingAlgorithm;
|
json[r'signingAlgorithm'] = this.signingAlgorithm;
|
||||||
json[r'storageLabelClaim'] = this.storageLabelClaim;
|
json[r'storageLabelClaim'] = this.storageLabelClaim;
|
||||||
@ -158,6 +164,7 @@ class SystemConfigOAuthDto {
|
|||||||
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,
|
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,
|
||||||
mobileRedirectUri: mapValueOfType<String>(json, r'mobileRedirectUri')!,
|
mobileRedirectUri: mapValueOfType<String>(json, r'mobileRedirectUri')!,
|
||||||
profileSigningAlgorithm: mapValueOfType<String>(json, r'profileSigningAlgorithm')!,
|
profileSigningAlgorithm: mapValueOfType<String>(json, r'profileSigningAlgorithm')!,
|
||||||
|
roleClaim: mapValueOfType<String>(json, r'roleClaim')!,
|
||||||
scope: mapValueOfType<String>(json, r'scope')!,
|
scope: mapValueOfType<String>(json, r'scope')!,
|
||||||
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
|
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
|
||||||
storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
|
storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
|
||||||
@ -222,6 +229,7 @@ class SystemConfigOAuthDto {
|
|||||||
'mobileOverrideEnabled',
|
'mobileOverrideEnabled',
|
||||||
'mobileRedirectUri',
|
'mobileRedirectUri',
|
||||||
'profileSigningAlgorithm',
|
'profileSigningAlgorithm',
|
||||||
|
'roleClaim',
|
||||||
'scope',
|
'scope',
|
||||||
'signingAlgorithm',
|
'signingAlgorithm',
|
||||||
'storageLabelClaim',
|
'storageLabelClaim',
|
||||||
|
@ -81,6 +81,26 @@ 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);
|
||||||
|
when(
|
||||||
|
() => mockSyncStreamRepo.updateStacksV1(
|
||||||
|
any(),
|
||||||
|
debugLabel: any(named: 'debugLabel'),
|
||||||
|
),
|
||||||
|
).thenAnswer(successHandler);
|
||||||
|
when(
|
||||||
|
() => mockSyncStreamRepo.deleteStacksV1(
|
||||||
|
any(),
|
||||||
|
debugLabel: any(named: 'debugLabel'),
|
||||||
|
),
|
||||||
|
).thenAnswer(successHandler);
|
||||||
|
|
||||||
sut = SyncStreamService(
|
sut = SyncStreamService(
|
||||||
syncApiRepository: mockSyncApiRepo,
|
syncApiRepository: mockSyncApiRepo,
|
||||||
@ -227,5 +247,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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
43
mobile/test/fixtures/sync_stream.stub.dart
vendored
43
mobile/test/fixtures/sync_stream.stub.dart
vendored
@ -42,4 +42,47 @@ abstract final class SyncStreamStub {
|
|||||||
data: SyncPartnerDeleteV1(sharedById: "3", sharedWithId: "4"),
|
data: SyncPartnerDeleteV1(sharedById: "3", sharedWithId: "4"),
|
||||||
ack: "4",
|
ack: "4",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static final memoryV1 = SyncEvent(
|
||||||
|
type: SyncEntityType.memoryV1,
|
||||||
|
data: SyncMemoryV1(
|
||||||
|
createdAt: DateTime(2023, 1, 1),
|
||||||
|
data: {"year": 2023, "title": "Test Memory"},
|
||||||
|
deletedAt: null,
|
||||||
|
hideAt: null,
|
||||||
|
id: "memory-1",
|
||||||
|
isSaved: false,
|
||||||
|
memoryAt: DateTime(2023, 1, 1),
|
||||||
|
ownerId: "user-1",
|
||||||
|
seenAt: null,
|
||||||
|
showAt: DateTime(2023, 1, 1),
|
||||||
|
type: MemoryType.onThisDay,
|
||||||
|
updatedAt: DateTime(2023, 1, 1),
|
||||||
|
),
|
||||||
|
ack: "5",
|
||||||
|
);
|
||||||
|
|
||||||
|
static final memoryDeleteV1 = SyncEvent(
|
||||||
|
type: SyncEntityType.memoryDeleteV1,
|
||||||
|
data: SyncMemoryDeleteV1(memoryId: "memory-2"),
|
||||||
|
ack: "6",
|
||||||
|
);
|
||||||
|
|
||||||
|
static final memoryToAssetV1 = SyncEvent(
|
||||||
|
type: SyncEntityType.memoryToAssetV1,
|
||||||
|
data: SyncMemoryAssetV1(
|
||||||
|
assetId: "asset-1",
|
||||||
|
memoryId: "memory-1",
|
||||||
|
),
|
||||||
|
ack: "7",
|
||||||
|
);
|
||||||
|
|
||||||
|
static final memoryToAssetDeleteV1 = SyncEvent(
|
||||||
|
type: SyncEntityType.memoryToAssetDeleteV1,
|
||||||
|
data: SyncMemoryAssetDeleteV1(
|
||||||
|
assetId: "asset-2",
|
||||||
|
memoryId: "memory-1",
|
||||||
|
),
|
||||||
|
ack: "8",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -14654,6 +14654,9 @@
|
|||||||
"profileSigningAlgorithm": {
|
"profileSigningAlgorithm": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"roleClaim": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"scope": {
|
"scope": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@ -14690,6 +14693,7 @@
|
|||||||
"mobileOverrideEnabled",
|
"mobileOverrideEnabled",
|
||||||
"mobileRedirectUri",
|
"mobileRedirectUri",
|
||||||
"profileSigningAlgorithm",
|
"profileSigningAlgorithm",
|
||||||
|
"roleClaim",
|
||||||
"scope",
|
"scope",
|
||||||
"signingAlgorithm",
|
"signingAlgorithm",
|
||||||
"storageLabelClaim",
|
"storageLabelClaim",
|
||||||
|
@ -1398,6 +1398,7 @@ export type SystemConfigOAuthDto = {
|
|||||||
mobileOverrideEnabled: boolean;
|
mobileOverrideEnabled: boolean;
|
||||||
mobileRedirectUri: string;
|
mobileRedirectUri: string;
|
||||||
profileSigningAlgorithm: string;
|
profileSigningAlgorithm: string;
|
||||||
|
roleClaim: string;
|
||||||
scope: string;
|
scope: string;
|
||||||
signingAlgorithm: string;
|
signingAlgorithm: string;
|
||||||
storageLabelClaim: string;
|
storageLabelClaim: string;
|
||||||
|
@ -101,6 +101,7 @@ export interface SystemConfig {
|
|||||||
timeout: number;
|
timeout: number;
|
||||||
storageLabelClaim: string;
|
storageLabelClaim: string;
|
||||||
storageQuotaClaim: string;
|
storageQuotaClaim: string;
|
||||||
|
roleClaim: string;
|
||||||
};
|
};
|
||||||
passwordLogin: {
|
passwordLogin: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
@ -263,6 +264,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
profileSigningAlgorithm: 'none',
|
profileSigningAlgorithm: 'none',
|
||||||
storageLabelClaim: 'preferred_username',
|
storageLabelClaim: 'preferred_username',
|
||||||
storageQuotaClaim: 'immich_quota',
|
storageQuotaClaim: 'immich_quota',
|
||||||
|
roleClaim: 'immich_role',
|
||||||
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
|
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST,
|
||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
},
|
},
|
||||||
|
@ -395,6 +395,9 @@ class SystemConfigOAuthDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
storageQuotaClaim!: string;
|
storageQuotaClaim!: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
roleClaim!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SystemConfigPasswordLoginDto {
|
class SystemConfigPasswordLoginDto {
|
||||||
|
@ -711,6 +711,7 @@ describe(AuthService.name, () => {
|
|||||||
|
|
||||||
expect(mocks.user.create).toHaveBeenCalledWith({
|
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
isAdmin: false,
|
||||||
name: ' ',
|
name: ' ',
|
||||||
oauthId: user.oauthId,
|
oauthId: user.oauthId,
|
||||||
quotaSizeInBytes: 0,
|
quotaSizeInBytes: 0,
|
||||||
@ -739,6 +740,7 @@ describe(AuthService.name, () => {
|
|||||||
|
|
||||||
expect(mocks.user.create).toHaveBeenCalledWith({
|
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
isAdmin: false,
|
||||||
name: ' ',
|
name: ' ',
|
||||||
oauthId: user.oauthId,
|
oauthId: user.oauthId,
|
||||||
quotaSizeInBytes: 5_368_709_120,
|
quotaSizeInBytes: 5_368_709_120,
|
||||||
@ -805,6 +807,93 @@ describe(AuthService.name, () => {
|
|||||||
expect(mocks.user.update).not.toHaveBeenCalled();
|
expect(mocks.user.update).not.toHaveBeenCalled();
|
||||||
expect(mocks.oauth.getProfilePicture).not.toHaveBeenCalled();
|
expect(mocks.oauth.getProfilePicture).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should only allow "admin" and "user" for the role claim', async () => {
|
||||||
|
const user = factory.userAdmin({ oauthId: 'oauth-id' });
|
||||||
|
|
||||||
|
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister);
|
||||||
|
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_role: 'foo' });
|
||||||
|
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||||
|
mocks.user.getAdmin.mockResolvedValue(factory.userAdmin({ isAdmin: true }));
|
||||||
|
mocks.user.getByOAuthId.mockResolvedValue(void 0);
|
||||||
|
mocks.user.create.mockResolvedValue(user);
|
||||||
|
mocks.session.create.mockResolvedValue(factory.session());
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.callback(
|
||||||
|
{ url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' },
|
||||||
|
{},
|
||||||
|
loginDetails,
|
||||||
|
),
|
||||||
|
).resolves.toEqual(oauthResponse(user));
|
||||||
|
|
||||||
|
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||||
|
email: user.email,
|
||||||
|
name: ' ',
|
||||||
|
oauthId: user.oauthId,
|
||||||
|
quotaSizeInBytes: null,
|
||||||
|
storageLabel: null,
|
||||||
|
isAdmin: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an admin user if the role claim is set to admin', async () => {
|
||||||
|
const user = factory.userAdmin({ oauthId: 'oauth-id' });
|
||||||
|
|
||||||
|
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.oauthWithAutoRegister);
|
||||||
|
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, immich_role: 'admin' });
|
||||||
|
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||||
|
mocks.user.getByOAuthId.mockResolvedValue(void 0);
|
||||||
|
mocks.user.create.mockResolvedValue(user);
|
||||||
|
mocks.session.create.mockResolvedValue(factory.session());
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.callback(
|
||||||
|
{ url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' },
|
||||||
|
{},
|
||||||
|
loginDetails,
|
||||||
|
),
|
||||||
|
).resolves.toEqual(oauthResponse(user));
|
||||||
|
|
||||||
|
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||||
|
email: user.email,
|
||||||
|
name: ' ',
|
||||||
|
oauthId: user.oauthId,
|
||||||
|
quotaSizeInBytes: null,
|
||||||
|
storageLabel: null,
|
||||||
|
isAdmin: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should accept a custom role claim', async () => {
|
||||||
|
const user = factory.userAdmin({ oauthId: 'oauth-id' });
|
||||||
|
|
||||||
|
mocks.systemMetadata.get.mockResolvedValue({
|
||||||
|
oauth: { ...systemConfigStub.oauthWithAutoRegister, roleClaim: 'my_role' },
|
||||||
|
});
|
||||||
|
mocks.oauth.getProfile.mockResolvedValue({ sub: user.oauthId, email: user.email, my_role: 'admin' });
|
||||||
|
mocks.user.getByEmail.mockResolvedValue(void 0);
|
||||||
|
mocks.user.getByOAuthId.mockResolvedValue(void 0);
|
||||||
|
mocks.user.create.mockResolvedValue(user);
|
||||||
|
mocks.session.create.mockResolvedValue(factory.session());
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.callback(
|
||||||
|
{ url: 'http://immich/auth/login?code=abc123', state: 'xyz789', codeVerifier: 'foo' },
|
||||||
|
{},
|
||||||
|
loginDetails,
|
||||||
|
),
|
||||||
|
).resolves.toEqual(oauthResponse(user));
|
||||||
|
|
||||||
|
expect(mocks.user.create).toHaveBeenCalledWith({
|
||||||
|
email: user.email,
|
||||||
|
name: ' ',
|
||||||
|
oauthId: user.oauthId,
|
||||||
|
quotaSizeInBytes: null,
|
||||||
|
storageLabel: null,
|
||||||
|
isAdmin: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('link', () => {
|
describe('link', () => {
|
||||||
|
@ -250,7 +250,7 @@ export class AuthService extends BaseService {
|
|||||||
const { oauth } = await this.getConfig({ withCache: false });
|
const { oauth } = await this.getConfig({ withCache: false });
|
||||||
const url = this.resolveRedirectUri(oauth, dto.url);
|
const url = this.resolveRedirectUri(oauth, dto.url);
|
||||||
const profile = await this.oauthRepository.getProfile(oauth, url, expectedState, codeVerifier);
|
const profile = await this.oauthRepository.getProfile(oauth, url, expectedState, codeVerifier);
|
||||||
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim } = oauth;
|
const { autoRegister, defaultStorageQuota, storageLabelClaim, storageQuotaClaim, roleClaim } = oauth;
|
||||||
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
||||||
let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub);
|
let user: UserAdmin | undefined = await this.userRepository.getByOAuthId(profile.sub);
|
||||||
|
|
||||||
@ -290,6 +290,11 @@ export class AuthService extends BaseService {
|
|||||||
default: defaultStorageQuota,
|
default: defaultStorageQuota,
|
||||||
isValid: (value: unknown) => Number(value) >= 0,
|
isValid: (value: unknown) => Number(value) >= 0,
|
||||||
});
|
});
|
||||||
|
const role = this.getClaim<'admin' | 'user'>(profile, {
|
||||||
|
key: roleClaim,
|
||||||
|
default: 'user',
|
||||||
|
isValid: (value: unknown) => isString(value) && ['admin', 'user'].includes(value),
|
||||||
|
});
|
||||||
|
|
||||||
const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`;
|
const userName = profile.name ?? `${profile.given_name || ''} ${profile.family_name || ''}`;
|
||||||
user = await this.createUser({
|
user = await this.createUser({
|
||||||
@ -298,6 +303,7 @@ export class AuthService extends BaseService {
|
|||||||
oauthId: profile.sub,
|
oauthId: profile.sub,
|
||||||
quotaSizeInBytes: storageQuota === null ? null : storageQuota * HumanReadableSize.GiB,
|
quotaSizeInBytes: storageQuota === null ? null : storageQuota * HumanReadableSize.GiB,
|
||||||
storageLabel: storageLabel || null,
|
storageLabel: storageLabel || null,
|
||||||
|
isAdmin: role === 'admin',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
import { serverVersion } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnEvent, OnJob } from 'src/decorators';
|
import { OnEvent, OnJob } from 'src/decorators';
|
||||||
import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder } from 'src/enum';
|
import { DatabaseLock, ImmichWorker, JobName, JobStatus, QueueName, StorageFolder } from 'src/enum';
|
||||||
@ -88,13 +90,11 @@ export class BackupService extends BaseService {
|
|||||||
];
|
];
|
||||||
|
|
||||||
databaseParams.push('--clean', '--if-exists');
|
databaseParams.push('--clean', '--if-exists');
|
||||||
|
const databaseVersion = await this.databaseRepository.getPostgresVersion();
|
||||||
const backupFilePath = path.join(
|
const backupFilePath = path.join(
|
||||||
StorageCore.getBaseFolder(StorageFolder.BACKUPS),
|
StorageCore.getBaseFolder(StorageFolder.BACKUPS),
|
||||||
`immich-db-backup-${Date.now()}.sql.gz.tmp`,
|
`immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const databaseVersion = await this.databaseRepository.getPostgresVersion();
|
|
||||||
const databaseSemver = semver.coerce(databaseVersion);
|
const databaseSemver = semver.coerce(databaseVersion);
|
||||||
const databaseMajorVersion = databaseSemver?.major;
|
const databaseMajorVersion = databaseSemver?.major;
|
||||||
|
|
||||||
|
@ -124,6 +124,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
timeout: 30_000,
|
timeout: 30_000,
|
||||||
storageLabelClaim: 'preferred_username',
|
storageLabelClaim: 'preferred_username',
|
||||||
storageQuotaClaim: 'immich_quota',
|
storageQuotaClaim: 'immich_quota',
|
||||||
|
roleClaim: 'immich_role',
|
||||||
},
|
},
|
||||||
passwordLogin: {
|
passwordLogin: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { compareColumns } from 'src/sql-tools/diff/comparers/column.comparer';
|
import { compareColumns } from 'src/sql-tools/comparers/column.comparer';
|
||||||
import { DatabaseColumn, Reason } from 'src/sql-tools/types';
|
import { DatabaseColumn, Reason } from 'src/sql-tools/types';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@ -18,7 +18,7 @@ describe('compareColumns', () => {
|
|||||||
{
|
{
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
columnName: 'test',
|
columnName: 'test',
|
||||||
type: 'column.drop',
|
type: 'ColumnDrop',
|
||||||
reason: Reason.MissingInSource,
|
reason: Reason.MissingInSource,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -29,7 +29,7 @@ describe('compareColumns', () => {
|
|||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(compareColumns.onMissing(testColumn)).toEqual([
|
expect(compareColumns.onMissing(testColumn)).toEqual([
|
||||||
{
|
{
|
||||||
type: 'column.add',
|
type: 'ColumnAdd',
|
||||||
column: testColumn,
|
column: testColumn,
|
||||||
reason: Reason.MissingInTarget,
|
reason: Reason.MissingInTarget,
|
||||||
},
|
},
|
||||||
@ -50,11 +50,11 @@ describe('compareColumns', () => {
|
|||||||
{
|
{
|
||||||
columnName: 'test',
|
columnName: 'test',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
type: 'column.drop',
|
type: 'ColumnDrop',
|
||||||
reason,
|
reason,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'column.add',
|
type: 'ColumnAdd',
|
||||||
column: source,
|
column: source,
|
||||||
reason,
|
reason,
|
||||||
},
|
},
|
||||||
@ -69,7 +69,7 @@ describe('compareColumns', () => {
|
|||||||
{
|
{
|
||||||
columnName: 'test',
|
columnName: 'test',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
type: 'column.alter',
|
type: 'ColumnAlter',
|
||||||
changes: {
|
changes: {
|
||||||
comment: 'new comment',
|
comment: 'new comment',
|
||||||
},
|
},
|
@ -4,14 +4,14 @@ import { Comparer, DatabaseColumn, Reason, SchemaDiff } from 'src/sql-tools/type
|
|||||||
export const compareColumns: Comparer<DatabaseColumn> = {
|
export const compareColumns: Comparer<DatabaseColumn> = {
|
||||||
onMissing: (source) => [
|
onMissing: (source) => [
|
||||||
{
|
{
|
||||||
type: 'column.add',
|
type: 'ColumnAdd',
|
||||||
column: source,
|
column: source,
|
||||||
reason: Reason.MissingInTarget,
|
reason: Reason.MissingInTarget,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onExtra: (target) => [
|
onExtra: (target) => [
|
||||||
{
|
{
|
||||||
type: 'column.drop',
|
type: 'ColumnDrop',
|
||||||
tableName: target.tableName,
|
tableName: target.tableName,
|
||||||
columnName: target.name,
|
columnName: target.name,
|
||||||
reason: Reason.MissingInSource,
|
reason: Reason.MissingInSource,
|
||||||
@ -31,7 +31,7 @@ export const compareColumns: Comparer<DatabaseColumn> = {
|
|||||||
const items: SchemaDiff[] = [];
|
const items: SchemaDiff[] = [];
|
||||||
if (source.nullable !== target.nullable) {
|
if (source.nullable !== target.nullable) {
|
||||||
items.push({
|
items.push({
|
||||||
type: 'column.alter',
|
type: 'ColumnAlter',
|
||||||
tableName: source.tableName,
|
tableName: source.tableName,
|
||||||
columnName: source.name,
|
columnName: source.name,
|
||||||
changes: {
|
changes: {
|
||||||
@ -43,7 +43,7 @@ export const compareColumns: Comparer<DatabaseColumn> = {
|
|||||||
|
|
||||||
if (!isDefaultEqual(source, target)) {
|
if (!isDefaultEqual(source, target)) {
|
||||||
items.push({
|
items.push({
|
||||||
type: 'column.alter',
|
type: 'ColumnAlter',
|
||||||
tableName: source.tableName,
|
tableName: source.tableName,
|
||||||
columnName: source.name,
|
columnName: source.name,
|
||||||
changes: {
|
changes: {
|
||||||
@ -55,7 +55,7 @@ export const compareColumns: Comparer<DatabaseColumn> = {
|
|||||||
|
|
||||||
if (source.comment !== target.comment) {
|
if (source.comment !== target.comment) {
|
||||||
items.push({
|
items.push({
|
||||||
type: 'column.alter',
|
type: 'ColumnAlter',
|
||||||
tableName: source.tableName,
|
tableName: source.tableName,
|
||||||
columnName: source.name,
|
columnName: source.name,
|
||||||
changes: {
|
changes: {
|
||||||
@ -72,11 +72,11 @@ export const compareColumns: Comparer<DatabaseColumn> = {
|
|||||||
const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => {
|
const dropAndRecreateColumn = (source: DatabaseColumn, target: DatabaseColumn, reason: string): SchemaDiff[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: 'column.drop',
|
type: 'ColumnDrop',
|
||||||
tableName: target.tableName,
|
tableName: target.tableName,
|
||||||
columnName: target.name,
|
columnName: target.name,
|
||||||
reason,
|
reason,
|
||||||
},
|
},
|
||||||
{ type: 'column.add', column: source, reason },
|
{ type: 'ColumnAdd', column: source, reason },
|
||||||
];
|
];
|
||||||
};
|
};
|
@ -1,9 +1,9 @@
|
|||||||
import { compareConstraints } from 'src/sql-tools/diff/comparers/constraint.comparer';
|
import { compareConstraints } from 'src/sql-tools/comparers/constraint.comparer';
|
||||||
import { DatabaseConstraint, DatabaseConstraintType, Reason } from 'src/sql-tools/types';
|
import { ConstraintType, DatabaseConstraint, Reason } from 'src/sql-tools/types';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const testConstraint: DatabaseConstraint = {
|
const testConstraint: DatabaseConstraint = {
|
||||||
type: DatabaseConstraintType.PRIMARY_KEY,
|
type: ConstraintType.PRIMARY_KEY,
|
||||||
name: 'test',
|
name: 'test',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
columnNames: ['column1'],
|
columnNames: ['column1'],
|
||||||
@ -15,7 +15,7 @@ describe('compareConstraints', () => {
|
|||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(compareConstraints.onExtra(testConstraint)).toEqual([
|
expect(compareConstraints.onExtra(testConstraint)).toEqual([
|
||||||
{
|
{
|
||||||
type: 'constraint.drop',
|
type: 'ConstraintDrop',
|
||||||
constraintName: 'test',
|
constraintName: 'test',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
reason: Reason.MissingInSource,
|
reason: Reason.MissingInSource,
|
||||||
@ -28,7 +28,7 @@ describe('compareConstraints', () => {
|
|||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(compareConstraints.onMissing(testConstraint)).toEqual([
|
expect(compareConstraints.onMissing(testConstraint)).toEqual([
|
||||||
{
|
{
|
||||||
type: 'constraint.add',
|
type: 'ConstraintAdd',
|
||||||
constraint: testConstraint,
|
constraint: testConstraint,
|
||||||
reason: Reason.MissingInTarget,
|
reason: Reason.MissingInTarget,
|
||||||
},
|
},
|
||||||
@ -49,11 +49,11 @@ describe('compareConstraints', () => {
|
|||||||
{
|
{
|
||||||
constraintName: 'test',
|
constraintName: 'test',
|
||||||
tableName: 'table1',
|
tableName: 'table1',
|
||||||
type: 'constraint.drop',
|
type: 'ConstraintDrop',
|
||||||
reason,
|
reason,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'constraint.add',
|
type: 'ConstraintAdd',
|
||||||
constraint: source,
|
constraint: source,
|
||||||
reason,
|
reason,
|
||||||
},
|
},
|
@ -2,9 +2,9 @@ import { haveEqualColumns } from 'src/sql-tools/helpers';
|
|||||||
import {
|
import {
|
||||||
CompareFunction,
|
CompareFunction,
|
||||||
Comparer,
|
Comparer,
|
||||||
|
ConstraintType,
|
||||||
DatabaseCheckConstraint,
|
DatabaseCheckConstraint,
|
||||||
DatabaseConstraint,
|
DatabaseConstraint,
|
||||||
DatabaseConstraintType,
|
|
||||||
DatabaseForeignKeyConstraint,
|
DatabaseForeignKeyConstraint,
|
||||||
DatabasePrimaryKeyConstraint,
|
DatabasePrimaryKeyConstraint,
|
||||||
DatabaseUniqueConstraint,
|
DatabaseUniqueConstraint,
|
||||||
@ -15,14 +15,14 @@ import {
|
|||||||
export const compareConstraints: Comparer<DatabaseConstraint> = {
|
export const compareConstraints: Comparer<DatabaseConstraint> = {
|
||||||
onMissing: (source) => [
|
onMissing: (source) => [
|
||||||
{
|
{
|
||||||
type: 'constraint.add',
|
type: 'ConstraintAdd',
|
||||||
constraint: source,
|
constraint: source,
|
||||||
reason: Reason.MissingInTarget,
|
reason: Reason.MissingInTarget,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onExtra: (target) => [
|
onExtra: (target) => [
|
||||||
{
|
{
|
||||||
type: 'constraint.drop',
|
type: 'ConstraintDrop',
|
||||||
tableName: target.tableName,
|
tableName: target.tableName,
|
||||||
constraintName: target.name,
|
constraintName: target.name,
|
||||||
reason: Reason.MissingInSource,
|
reason: Reason.MissingInSource,
|
||||||
@ -30,19 +30,19 @@ export const compareConstraints: Comparer<DatabaseConstraint> = {
|
|||||||
],
|
],
|
||||||
onCompare: (source, target) => {
|
onCompare: (source, target) => {
|
||||||
switch (source.type) {
|
switch (source.type) {
|
||||||
case DatabaseConstraintType.PRIMARY_KEY: {
|
case ConstraintType.PRIMARY_KEY: {
|
||||||
return comparePrimaryKeyConstraint(source, target as DatabasePrimaryKeyConstraint);
|
return comparePrimaryKeyConstraint(source, target as DatabasePrimaryKeyConstraint);
|
||||||
}
|
}
|
||||||
|
|
||||||
case DatabaseConstraintType.FOREIGN_KEY: {
|
case ConstraintType.FOREIGN_KEY: {
|
||||||
return compareForeignKeyConstraint(source, target as DatabaseForeignKeyConstraint);
|
return compareForeignKeyConstraint(source, target as DatabaseForeignKeyConstraint);
|
||||||
}
|
}
|
||||||
|
|
||||||
case DatabaseConstraintType.UNIQUE: {
|
case ConstraintType.UNIQUE: {
|
||||||
return compareUniqueConstraint(source, target as DatabaseUniqueConstraint);
|
return compareUniqueConstraint(source, target as DatabaseUniqueConstraint);
|
||||||
}
|
}
|
||||||
|
|
||||||
case DatabaseConstraintType.CHECK: {
|
case ConstraintType.CHECK: {
|
||||||
return compareCheckConstraint(source, target as DatabaseCheckConstraint);
|
return compareCheckConstraint(source, target as DatabaseCheckConstraint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,11 +123,11 @@ const dropAndRecreateConstraint = (
|
|||||||
): SchemaDiff[] => {
|
): SchemaDiff[] => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: 'constraint.drop',
|
type: 'ConstraintDrop',
|
||||||
tableName: target.tableName,
|
tableName: target.tableName,
|
||||||
constraintName: target.name,
|
constraintName: target.name,
|
||||||
reason,
|
reason,
|
||||||
},
|
},
|
||||||
{ type: 'constraint.add', constraint: source, reason },
|
{ type: 'ConstraintAdd', constraint: source, reason },
|
||||||
];
|
];
|
||||||
};
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { compareEnums } from 'src/sql-tools/diff/comparers/enum.comparer';
|
import { compareEnums } from 'src/sql-tools/comparers/enum.comparer';
|
||||||
import { DatabaseEnum, Reason } from 'src/sql-tools/types';
|
import { DatabaseEnum, Reason } from 'src/sql-tools/types';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ describe('compareEnums', () => {
|
|||||||
expect(compareEnums.onExtra(testEnum)).toEqual([
|
expect(compareEnums.onExtra(testEnum)).toEqual([
|
||||||
{
|
{
|
||||||
enumName: 'test',
|
enumName: 'test',
|
||||||
type: 'enum.drop',
|
type: 'EnumDrop',
|
||||||
reason: Reason.MissingInSource,
|
reason: Reason.MissingInSource,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -21,7 +21,7 @@ describe('compareEnums', () => {
|
|||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(compareEnums.onMissing(testEnum)).toEqual([
|
expect(compareEnums.onMissing(testEnum)).toEqual([
|
||||||
{
|
{
|
||||||
type: 'enum.create',
|
type: 'EnumCreate',
|
||||||
enum: testEnum,
|
enum: testEnum,
|
||||||
reason: Reason.MissingInTarget,
|
reason: Reason.MissingInTarget,
|
||||||
},
|
},
|
||||||
@ -40,11 +40,11 @@ describe('compareEnums', () => {
|
|||||||
expect(compareEnums.onCompare(source, target)).toEqual([
|
expect(compareEnums.onCompare(source, target)).toEqual([
|
||||||
{
|
{
|
||||||
enumName: 'test',
|
enumName: 'test',
|
||||||
type: 'enum.drop',
|
type: 'EnumDrop',
|
||||||
reason: 'enum values has changed (foo,bar vs foo,bar,world)',
|
reason: 'enum values has changed (foo,bar vs foo,bar,world)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'enum.create',
|
type: 'EnumCreate',
|
||||||
enum: source,
|
enum: source,
|
||||||
reason: 'enum values has changed (foo,bar vs foo,bar,world)',
|
reason: 'enum values has changed (foo,bar vs foo,bar,world)',
|
||||||
},
|
},
|
@ -3,14 +3,14 @@ import { Comparer, DatabaseEnum, Reason } from 'src/sql-tools/types';
|
|||||||
export const compareEnums: Comparer<DatabaseEnum> = {
|
export const compareEnums: Comparer<DatabaseEnum> = {
|
||||||
onMissing: (source) => [
|
onMissing: (source) => [
|
||||||
{
|
{
|
||||||
type: 'enum.create',
|
type: 'EnumCreate',
|
||||||
enum: source,
|
enum: source,
|
||||||
reason: Reason.MissingInTarget,
|
reason: Reason.MissingInTarget,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onExtra: (target) => [
|
onExtra: (target) => [
|
||||||
{
|
{
|
||||||
type: 'enum.drop',
|
type: 'EnumDrop',
|
||||||
enumName: target.name,
|
enumName: target.name,
|
||||||
reason: Reason.MissingInSource,
|
reason: Reason.MissingInSource,
|
||||||
},
|
},
|
||||||
@ -21,12 +21,12 @@ export const compareEnums: Comparer<DatabaseEnum> = {
|
|||||||
const reason = `enum values has changed (${source.values} vs ${target.values})`;
|
const reason = `enum values has changed (${source.values} vs ${target.values})`;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
type: 'enum.drop',
|
type: 'EnumDrop',
|
||||||
enumName: source.name,
|
enumName: source.name,
|
||||||
reason,
|
reason,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'enum.create',
|
type: 'EnumCreate',
|
||||||
enum: source,
|
enum: source,
|
||||||
reason,
|
reason,
|
||||||
},
|
},
|
@ -1,4 +1,4 @@
|
|||||||
import { compareExtensions } from 'src/sql-tools/diff/comparers/extension.comparer';
|
import { compareExtensions } from 'src/sql-tools/comparers/extension.comparer';
|
||||||
import { Reason } from 'src/sql-tools/types';
|
import { Reason } from 'src/sql-tools/types';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ describe('compareExtensions', () => {
|
|||||||
expect(compareExtensions.onExtra(testExtension)).toEqual([
|
expect(compareExtensions.onExtra(testExtension)).toEqual([
|
||||||
{
|
{
|
||||||
extensionName: 'test',
|
extensionName: 'test',
|
||||||
type: 'extension.drop',
|
type: 'ExtensionDrop',
|
||||||
reason: Reason.MissingInSource,
|
reason: Reason.MissingInSource,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -21,7 +21,7 @@ describe('compareExtensions', () => {
|
|||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(compareExtensions.onMissing(testExtension)).toEqual([
|
expect(compareExtensions.onMissing(testExtension)).toEqual([
|
||||||
{
|
{
|
||||||
type: 'extension.create',
|
type: 'ExtensionCreate',
|
||||||
extension: testExtension,
|
extension: testExtension,
|
||||||
reason: Reason.MissingInTarget,
|
reason: Reason.MissingInTarget,
|
||||||
},
|
},
|
@ -3,14 +3,14 @@ import { Comparer, DatabaseExtension, Reason } from 'src/sql-tools/types';
|
|||||||
export const compareExtensions: Comparer<DatabaseExtension> = {
|
export const compareExtensions: Comparer<DatabaseExtension> = {
|
||||||
onMissing: (source) => [
|
onMissing: (source) => [
|
||||||
{
|
{
|
||||||
type: 'extension.create',
|
type: 'ExtensionCreate',
|
||||||
extension: source,
|
extension: source,
|
||||||
reason: Reason.MissingInTarget,
|
reason: Reason.MissingInTarget,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
onExtra: (target) => [
|
onExtra: (target) => [
|
||||||
{
|
{
|
||||||
type: 'extension.drop',
|
type: 'ExtensionDrop',
|
||||||
extensionName: target.name,
|
extensionName: target.name,
|
||||||
reason: Reason.MissingInSource,
|
reason: Reason.MissingInSource,
|
||||||
},
|
},
|
@ -1,4 +1,4 @@
|
|||||||
import { compareFunctions } from 'src/sql-tools/diff/comparers/function.comparer';
|
import { compareFunctions } from 'src/sql-tools/comparers/function.comparer';
|
||||||
import { DatabaseFunction, Reason } from 'src/sql-tools/types';
|
import { DatabaseFunction, Reason } from 'src/sql-tools/types';
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ describe('compareFunctions', () => {
|
|||||||
expect(compareFunctions.onExtra(testFunction)).toEqual([
|
expect(compareFunctions.onExtra(testFunction)).toEqual([
|
||||||
{
|
{
|
||||||
functionName: 'test',
|
functionName: 'test',
|
||||||
type: 'function.drop',
|
type: 'FunctionDrop',
|
||||||
reason: Reason.MissingInSource,
|
reason: Reason.MissingInSource,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@ -25,7 +25,7 @@ describe('compareFunctions', () => {
|
|||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
expect(compareFunctions.onMissing(testFunction)).toEqual([
|
expect(compareFunctions.onMissing(testFunction)).toEqual([
|
||||||
{
|
{
|
||||||
type: 'function.create',
|
type: 'FunctionCreate',
|
||||||
function: testFunction,
|
function: testFunction,
|
||||||
reason: Reason.MissingInTarget,
|
reason: Reason.MissingInTarget,
|
||||||
},
|
},
|
||||||
@ -43,7 +43,7 @@ describe('compareFunctions', () => {
|
|||||||
const target: DatabaseFunction = { ...testFunction, expression: 'SELECT 2' };
|
const target: DatabaseFunction = { ...testFunction, expression: 'SELECT 2' };
|
||||||
expect(compareFunctions.onCompare(source, target)).toEqual([
|
expect(compareFunctions.onCompare(source, target)).toEqual([
|
||||||
{
|
{
|
||||||
type: 'function.create',
|
type: 'FunctionCreate',
|
||||||
reason: 'function expression has changed (SELECT 1 vs SELECT 2)',
|
reason: 'function expression has changed (SELECT 1 vs SELECT 2)',
|
||||||
function: source,
|
function: source,
|
||||||
},
|
},
|
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