Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46f348825d | |||
| 17e6ca962a | |||
| 750d21aeba | |||
| 990d9ba9a8 | |||
| b05e931ed8 | |||
| 4d0c9172e5 | |||
| fab63e6b2a | |||
| 094e3a2757 | |||
| 278668b8c5 | |||
| 3ef9e36f38 | |||
| 10141504a2 | |||
| 67736c8fce | |||
| b56a272f64 | |||
| 5901c2e963 | |||
| be85832b20 | |||
| c8f9a72d3e | |||
| 3d633a81c4 | |||
| 4efbf36d82 | |||
| e2c3c39597 | |||
| 007ba1d9ef | |||
| 4d5cd1a6b5 | |||
| 8108f50c4e | |||
| 1b8354ed36 | |||
| 9242afb4b0 | |||
| c5f14adff0 | |||
| 1378f22368 | |||
| 4bd465e752 |
@@ -0,0 +1,96 @@
|
|||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened]
|
||||||
|
discussion:
|
||||||
|
types: [created]
|
||||||
|
|
||||||
|
name: Close likely duplicates
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get_body:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
EVENT: ${{ toJSON(github.event) }}
|
||||||
|
outputs:
|
||||||
|
body: ${{ steps.get_body.outputs.body }}
|
||||||
|
steps:
|
||||||
|
- id: get_body
|
||||||
|
run: |
|
||||||
|
BODY=$(echo """$EVENT""" | jq -r '.issue // .discussion | .body' | base64 -w 0)
|
||||||
|
echo "body=$BODY" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
get_checkbox_json:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: get_body
|
||||||
|
container:
|
||||||
|
image: yshavit/mdq:0.7.2
|
||||||
|
outputs:
|
||||||
|
json: ${{ steps.get_checkbox.outputs.json }}
|
||||||
|
steps:
|
||||||
|
- id: get_checkbox
|
||||||
|
env:
|
||||||
|
BODY: ${{ needs.get_body.outputs.body }}
|
||||||
|
run: |
|
||||||
|
JSON=$(echo "$BODY" | base64 -d | /mdq --output json '# I have searched | - [?] Yes')
|
||||||
|
echo "json=$JSON" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
close_and_comment:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: get_checkbox_json
|
||||||
|
if: ${{ !fromJSON(needs.get_checkbox_json.outputs.json).items[0].list[0].checked }}
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
discussions: write
|
||||||
|
steps:
|
||||||
|
- name: Close issue
|
||||||
|
if: ${{ github.event_name == 'issues' }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
NODE_ID: ${{ github.event.issue.node_id }}
|
||||||
|
run: |
|
||||||
|
gh api graphql \
|
||||||
|
-f issueId="$NODE_ID" \
|
||||||
|
-f body="This issue has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one." \
|
||||||
|
-f query='
|
||||||
|
mutation CommentAndCloseIssue($issueId: ID!, $body: String!) {
|
||||||
|
addComment(input: {
|
||||||
|
subjectId: $issueId,
|
||||||
|
body: $body
|
||||||
|
}) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
|
||||||
|
closeIssue(input: {
|
||||||
|
issueId: $issueId,
|
||||||
|
stateReason: DUPLICATE
|
||||||
|
}) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
- name: Close discussion
|
||||||
|
if: ${{ github.event_name == 'discussion' && github.event.discussion.category.name == 'Feature Request' }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
NODE_ID: ${{ github.event.discussion.node_id }}
|
||||||
|
run: |
|
||||||
|
gh api graphql \
|
||||||
|
-f discussionId="$NODE_ID" \
|
||||||
|
-f body="This discussion has automatically been closed as it is likely a duplicate. We get a lot of duplicate threads each day, which is why we ask you in the template to confirm that you searched for duplicates before opening one." \
|
||||||
|
-f query='
|
||||||
|
mutation CommentAndCloseDiscussion($discussionId: ID!, $body: String!) {
|
||||||
|
addDiscussionComment(input: {
|
||||||
|
discussionId: $discussionId,
|
||||||
|
body: $body
|
||||||
|
}) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
|
||||||
|
closeDiscussion(input: {
|
||||||
|
discussionId: $discussionId,
|
||||||
|
reason: DUPLICATE
|
||||||
|
}) {
|
||||||
|
__typename
|
||||||
|
}
|
||||||
|
}'
|
||||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.75",
|
"version": "2.2.77",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.75",
|
"version": "2.2.77",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.75",
|
"version": "2.2.77",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
|
|||||||
Vendored
+8
@@ -1,4 +1,12 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.137.3",
|
||||||
|
"url": "https://v1.137.3.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.137.2",
|
||||||
|
"url": "https://v1.137.2.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.137.1",
|
"label": "v1.137.1",
|
||||||
"url": "https://v1.137.1.archive.immich.app"
|
"url": "https://v1.137.1.archive.immich.app"
|
||||||
|
|||||||
Generated
+4
-4
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.1.0",
|
"@eslint/eslintrc": "^3.1.0",
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
},
|
},
|
||||||
"../cli": {
|
"../cli": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.75",
|
"version": "2.2.77",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+1
-2
@@ -1252,7 +1252,7 @@
|
|||||||
"manage_your_devices": "Manage your logged-in devices",
|
"manage_your_devices": "Manage your logged-in devices",
|
||||||
"manage_your_oauth_connection": "Manage your OAuth connection",
|
"manage_your_oauth_connection": "Manage your OAuth connection",
|
||||||
"map": "Map",
|
"map": "Map",
|
||||||
"map_assets_in_bounds": "{count, plural, one {# photo} other {# photos}}",
|
"map_assets_in_bounds": "{count, plural, =0 {No photos in this area} one {# photo} other {# photos}}",
|
||||||
"map_cannot_get_user_location": "Cannot get user's location",
|
"map_cannot_get_user_location": "Cannot get user's location",
|
||||||
"map_location_dialog_yes": "Yes",
|
"map_location_dialog_yes": "Yes",
|
||||||
"map_location_picker_page_use_location": "Use this location",
|
"map_location_picker_page_use_location": "Use this location",
|
||||||
@@ -1260,7 +1260,6 @@
|
|||||||
"map_location_service_disabled_title": "Location Service disabled",
|
"map_location_service_disabled_title": "Location Service disabled",
|
||||||
"map_marker_for_images": "Map marker for images taken in {city}, {country}",
|
"map_marker_for_images": "Map marker for images taken in {city}, {country}",
|
||||||
"map_marker_with_image": "Map marker with image",
|
"map_marker_with_image": "Map marker with image",
|
||||||
"map_no_assets_in_bounds": "No photos in this area",
|
|
||||||
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
|
||||||
"map_no_location_permission_title": "Location Permission denied",
|
"map_no_location_permission_title": "Location Permission denied",
|
||||||
"map_settings": "Map settings",
|
"map_settings": "Map settings",
|
||||||
|
|||||||
@@ -29,21 +29,24 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
|
MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO.toString()
|
||||||
)
|
)
|
||||||
const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)"
|
const val BUCKET_SELECTION = "(${MediaStore.Files.FileColumns.BUCKET_ID} = ?)"
|
||||||
val ASSET_PROJECTION = arrayOf(
|
val ASSET_PROJECTION = buildList {
|
||||||
MediaStore.MediaColumns._ID,
|
add(MediaStore.MediaColumns._ID)
|
||||||
MediaStore.MediaColumns.DATA,
|
add(MediaStore.MediaColumns.DATA)
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
add(MediaStore.MediaColumns.DISPLAY_NAME)
|
||||||
MediaStore.MediaColumns.DATE_TAKEN,
|
add(MediaStore.MediaColumns.DATE_TAKEN)
|
||||||
MediaStore.MediaColumns.DATE_ADDED,
|
add(MediaStore.MediaColumns.DATE_ADDED)
|
||||||
MediaStore.MediaColumns.DATE_MODIFIED,
|
add(MediaStore.MediaColumns.DATE_MODIFIED)
|
||||||
MediaStore.Files.FileColumns.MEDIA_TYPE,
|
add(MediaStore.Files.FileColumns.MEDIA_TYPE)
|
||||||
MediaStore.MediaColumns.BUCKET_ID,
|
add(MediaStore.MediaColumns.BUCKET_ID)
|
||||||
MediaStore.MediaColumns.WIDTH,
|
add(MediaStore.MediaColumns.WIDTH)
|
||||||
MediaStore.MediaColumns.HEIGHT,
|
add(MediaStore.MediaColumns.HEIGHT)
|
||||||
MediaStore.MediaColumns.DURATION,
|
add(MediaStore.MediaColumns.DURATION)
|
||||||
MediaStore.MediaColumns.ORIENTATION,
|
add(MediaStore.MediaColumns.ORIENTATION)
|
||||||
MediaStore.MediaColumns.IS_FAVORITE,
|
// IS_FAVORITE is only available on Android 11 and above
|
||||||
)
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
|
||||||
|
add(MediaStore.MediaColumns.IS_FAVORITE)
|
||||||
|
}
|
||||||
|
}.toTypedArray()
|
||||||
|
|
||||||
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
|
const val HASH_BUFFER_SIZE = 2 * 1024 * 1024
|
||||||
}
|
}
|
||||||
@@ -78,7 +81,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
|
val durationColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.DURATION)
|
||||||
val orientationColumn =
|
val orientationColumn =
|
||||||
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
c.getColumnIndexOrThrow(MediaStore.MediaColumns.ORIENTATION)
|
||||||
val favoriteColumn = c.getColumnIndexOrThrow(MediaStore.MediaColumns.IS_FAVORITE)
|
val favoriteColumn = c.getColumnIndex(MediaStore.MediaColumns.IS_FAVORITE)
|
||||||
|
|
||||||
while (c.moveToNext()) {
|
while (c.moveToNext()) {
|
||||||
val id = c.getLong(idColumn).toString()
|
val id = c.getLong(idColumn).toString()
|
||||||
@@ -107,7 +110,7 @@ open class NativeSyncApiImplBase(context: Context) {
|
|||||||
else c.getLong(durationColumn) / 1000
|
else c.getLong(durationColumn) / 1000
|
||||||
val bucketId = c.getString(bucketIdColumn)
|
val bucketId = c.getString(bucketIdColumn)
|
||||||
val orientation = c.getInt(orientationColumn)
|
val orientation = c.getInt(orientationColumn)
|
||||||
val isFavorite = c.getInt(favoriteColumn) != 0;
|
val isFavorite = if (favoriteColumn == -1) false else c.getInt(favoriteColumn) != 0
|
||||||
|
|
||||||
val asset = PlatformAsset(
|
val asset = PlatformAsset(
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 205,
|
"android.injected.version.code" => 3002,
|
||||||
"android.injected.version.name" => "1.137.1",
|
"android.injected.version.name" => "1.137.3",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -649,7 +649,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -793,7 +793,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -823,7 +823,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
@@ -857,7 +857,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -900,7 +900,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -940,7 +940,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||||
@@ -979,7 +979,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -1023,7 +1023,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
@@ -1064,7 +1064,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 210;
|
CURRENT_PROJECT_VERSION = 213;
|
||||||
CUSTOM_GROUP_ID = group.app.immich.share;
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.135.1</string>
|
<string>1.137.2</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleURLTypes</key>
|
<key>CFBundleURLTypes</key>
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>210</string>
|
<string>213</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ platform :ios do
|
|||||||
path: "./Runner.xcodeproj",
|
path: "./Runner.xcodeproj",
|
||||||
)
|
)
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.137.1"
|
version_number: "1.137.3"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ class HashService {
|
|||||||
if (hash?.length == 20) {
|
if (hash?.length == 20) {
|
||||||
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
hashed.add(asset.copyWith(checksum: base64.encode(hash!)));
|
||||||
} else {
|
} else {
|
||||||
_log.warning("Failed to hash file for ${asset.id}");
|
_log.warning("Failed to hash file for ${asset.id}: ${asset.name} created at ${asset.createdAt}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class BackgroundSyncManager {
|
|||||||
this.onHashingError,
|
this.onHashingError,
|
||||||
});
|
});
|
||||||
|
|
||||||
Future<void> cancel() {
|
Future<void> cancel() async {
|
||||||
final futures = <Future>[];
|
final futures = <Future>[];
|
||||||
|
|
||||||
if (_syncTask != null) {
|
if (_syncTask != null) {
|
||||||
@@ -52,7 +52,11 @@ class BackgroundSyncManager {
|
|||||||
_syncWebsocketTask?.cancel();
|
_syncWebsocketTask?.cancel();
|
||||||
_syncWebsocketTask = null;
|
_syncWebsocketTask = null;
|
||||||
|
|
||||||
return Future.wait(futures);
|
try {
|
||||||
|
await Future.wait(futures);
|
||||||
|
} on CanceledError {
|
||||||
|
// Ignore cancellation errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to cancel the task, as it can also be run when the user logs out
|
// No need to cancel the task, as it can also be run when the user logs out
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ Future<void> initApp() async {
|
|||||||
};
|
};
|
||||||
|
|
||||||
PlatformDispatcher.instance.onError = (error, stack) {
|
PlatformDispatcher.instance.onError = (error, stack) {
|
||||||
debugPrint("FlutterError - Catch all: $error \n $stack");
|
|
||||||
log.severe('PlatformDispatcher - Catch all', error, stack);
|
log.severe('PlatformDispatcher - Catch all', error, stack);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||||
import 'package:immich_mobile/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/providers/album/album.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
import 'package:immich_mobile/providers/backup/backup_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
@@ -14,7 +13,6 @@ import 'package:immich_mobile/providers/user.provider.dart';
|
|||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
import 'package:immich_mobile/widgets/backup/drift_album_info_list_tile.dart';
|
||||||
import 'package:immich_mobile/widgets/common/search_field.dart';
|
import 'package:immich_mobile/widgets/common/search_field.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget {
|
class DriftBackupAlbumSelectionPage extends ConsumerStatefulWidget {
|
||||||
@@ -67,14 +65,14 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||||||
final selectedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.selected).toList();
|
final selectedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.selected).toList();
|
||||||
final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList();
|
final excludedBackupAlbums = albums.where((album) => album.backupSelection == BackupSelection.excluded).toList();
|
||||||
|
|
||||||
handleSyncAlbumToggle(bool isEnable) async {
|
// handleSyncAlbumToggle(bool isEnable) async {
|
||||||
if (isEnable) {
|
// if (isEnable) {
|
||||||
await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
// await ref.read(albumProvider.notifier).refreshRemoteAlbums();
|
||||||
for (final album in selectedBackupAlbums) {
|
// for (final album in selectedBackupAlbums) {
|
||||||
await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
// await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
return PopScope(
|
return PopScope(
|
||||||
onPopInvokedWithResult: (didPop, result) async {
|
onPopInvokedWithResult: (didPop, result) async {
|
||||||
@@ -167,16 +165,15 @@ class _DriftBackupAlbumSelectionPageState extends ConsumerState<DriftBackupAlbum
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
SettingsSwitchListTile(
|
// SettingsSwitchListTile(
|
||||||
valueNotifier: _enableSyncUploadAlbum,
|
// valueNotifier: _enableSyncUploadAlbum,
|
||||||
title: "sync_albums".t(context: context),
|
// title: "sync_albums".t(context: context),
|
||||||
subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
// subtitle: "sync_upload_album_setting_subtitle".t(context: context),
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
// contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold),
|
// titleStyle: context.textTheme.bodyLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||||
subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary),
|
// subtitleStyle: context.textTheme.labelLarge?.copyWith(color: context.colorScheme.primary),
|
||||||
onChanged: handleSyncAlbumToggle,
|
// onChanged: handleSyncAlbumToggle,
|
||||||
),
|
// ),
|
||||||
|
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
"albums_on_device_count".t(context: context, args: {'count': albumCount.toString()}),
|
"albums_on_device_count".t(context: context, args: {'count': albumCount.toString()}),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -9,6 +11,7 @@ import 'package:immich_mobile/providers/app_settings.provider.dart';
|
|||||||
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
import 'package:immich_mobile/providers/backup/drift_backup.provider.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
import 'package:immich_mobile/providers/tab.provider.dart';
|
import 'package:immich_mobile/providers/tab.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
@@ -122,6 +125,11 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
|||||||
EventStream.shared.emit(const ScrollToTopEvent());
|
EventStream.shared.emit(const ScrollToTopEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On Photos page navigation, invalidate memories provider to get the most up-to-date data
|
||||||
|
if (router.activeIndex == 0) {
|
||||||
|
ref.invalidate(driftMemoryFutureProvider);
|
||||||
|
}
|
||||||
|
|
||||||
// On Search page tapped
|
// On Search page tapped
|
||||||
if (router.activeIndex == 1 && index == 1) {
|
if (router.activeIndex == 1 && index == 1) {
|
||||||
ref.read(searchInputFocusProvider).requestFocus();
|
ref.read(searchInputFocusProvider).requestFocus();
|
||||||
@@ -137,25 +145,50 @@ void _onNavigationSelected(TabsRouter router, int index, WidgetRef ref) {
|
|||||||
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
ref.read(tabProvider.notifier).state = TabEnum.values[index];
|
||||||
}
|
}
|
||||||
|
|
||||||
class _BottomNavigationBar extends ConsumerWidget {
|
class _BottomNavigationBar extends ConsumerStatefulWidget {
|
||||||
const _BottomNavigationBar({required this.tabsRouter, required this.destinations});
|
const _BottomNavigationBar({required this.tabsRouter, required this.destinations});
|
||||||
|
|
||||||
final List<Widget> destinations;
|
final List<Widget> destinations;
|
||||||
final TabsRouter tabsRouter;
|
final TabsRouter tabsRouter;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState createState() => _BottomNavigationBarState();
|
||||||
final isScreenLandscape = context.orientation == Orientation.landscape;
|
}
|
||||||
final isMultiselectEnabled = ref.watch(multiSelectProvider.select((s) => s.isEnabled));
|
|
||||||
|
|
||||||
if (isScreenLandscape || isMultiselectEnabled) {
|
class _BottomNavigationBarState extends ConsumerState<_BottomNavigationBar> {
|
||||||
|
bool hideNavigationBar = false;
|
||||||
|
StreamSubscription? _eventSubscription;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_eventSubscription = EventStream.shared.listen<MultiSelectToggleEvent>(_onEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onEvent(MultiSelectToggleEvent event) {
|
||||||
|
setState(() {
|
||||||
|
hideNavigationBar = event.isEnabled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_eventSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isScreenLandscape = context.orientation == Orientation.landscape;
|
||||||
|
|
||||||
|
if (isScreenLandscape || hideNavigationBar) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
return NavigationBar(
|
return NavigationBar(
|
||||||
selectedIndex: tabsRouter.activeIndex,
|
selectedIndex: widget.tabsRouter.activeIndex,
|
||||||
onDestinationSelected: (index) => _onNavigationSelected(tabsRouter, index, ref),
|
onDestinationSelected: (index) => _onNavigationSelected(widget.tabsRouter, index, ref),
|
||||||
destinations: destinations,
|
destinations: widget.destinations,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -264,11 +264,15 @@ class SharedLinkEditPage extends HookConsumerWidget {
|
|||||||
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
|
||||||
);
|
);
|
||||||
ref.invalidate(sharedLinksStateProvider);
|
ref.invalidate(sharedLinksStateProvider);
|
||||||
|
|
||||||
|
await ref.read(serverInfoProvider.notifier).getServerConfig();
|
||||||
final externalDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
final externalDomain = ref.read(serverInfoProvider.select((s) => s.serverConfig.externalDomain));
|
||||||
|
|
||||||
var serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
var serverUrl = externalDomain.isNotEmpty ? externalDomain : getServerUrl();
|
||||||
if (serverUrl != null && !serverUrl.endsWith('/')) {
|
if (serverUrl != null && !serverUrl.endsWith('/')) {
|
||||||
serverUrl += '/';
|
serverUrl += '/';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newLink != null && serverUrl != null) {
|
if (newLink != null && serverUrl != null) {
|
||||||
newShareLink.value = "${serverUrl}share/${newLink.key}";
|
newShareLink.value = "${serverUrl}share/${newLink.key}";
|
||||||
copyLinkToClipboard();
|
copyLinkToClipboard();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/memory/memory_lane.widget.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';
|
import 'package:immich_mobile/providers/infrastructure/memory.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
|
||||||
@RoutePage()
|
@RoutePage()
|
||||||
class MainTimelinePage extends ConsumerWidget {
|
class MainTimelinePage extends ConsumerWidget {
|
||||||
@@ -12,21 +13,24 @@ class MainTimelinePage extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
final memoryLaneProvider = ref.watch(driftMemoryFutureProvider);
|
||||||
|
final memoriesEnabled = ref.watch(currentUserProvider.select((user) => user?.memoryEnabled ?? true));
|
||||||
|
|
||||||
|
// TODO: the user preferences need to be updated
|
||||||
|
// from the server to get live hiding/showing of memory lane
|
||||||
|
|
||||||
return memoryLaneProvider.maybeWhen(
|
return memoryLaneProvider.maybeWhen(
|
||||||
data: (memories) {
|
data: (memories) {
|
||||||
return memories.isEmpty
|
return memories.isEmpty || !memoriesEnabled
|
||||||
? const Timeline(showStorageIndicator: true)
|
? const Timeline()
|
||||||
: Timeline(
|
: Timeline(
|
||||||
topSliverWidget: SliverToBoxAdapter(
|
topSliverWidget: SliverToBoxAdapter(
|
||||||
key: Key('memory-lane-${memories.first.assets.first.id}'),
|
key: Key('memory-lane-${memories.first.assets.first.id}'),
|
||||||
child: DriftMemoryLane(memories: memories),
|
child: DriftMemoryLane(memories: memories),
|
||||||
),
|
),
|
||||||
topSliverWidgetHeight: 200,
|
topSliverWidgetHeight: 200,
|
||||||
showStorageIndicator: true,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
orElse: () => const Timeline(showStorageIndicator: true),
|
orElse: () => const Timeline(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,15 @@ class RemoteAlbumPage extends ConsumerStatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
||||||
|
late RemoteAlbum _album;
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_album = widget.album;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addAssets(BuildContext context) async {
|
Future<void> addAssets(BuildContext context) async {
|
||||||
final albumAssets = await ref.read(remoteAlbumProvider.notifier).getAssets(widget.album.id);
|
final albumAssets = await ref.read(remoteAlbumProvider.notifier).getAssets(_album.id);
|
||||||
|
|
||||||
final newAssets = await context.pushRoute<Set<BaseAsset>>(
|
final newAssets = await context.pushRoute<Set<BaseAsset>>(
|
||||||
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()),
|
DriftAssetSelectionTimelineRoute(lockedSelectionAssets: albumAssets.toSet()),
|
||||||
@@ -47,7 +49,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
final added = await ref
|
final added = await ref
|
||||||
.read(remoteAlbumProvider.notifier)
|
.read(remoteAlbumProvider.notifier)
|
||||||
.addAssets(
|
.addAssets(
|
||||||
widget.album.id,
|
_album.id,
|
||||||
newAssets.map((asset) {
|
newAssets.map((asset) {
|
||||||
final remoteAsset = asset as RemoteAsset;
|
final remoteAsset = asset as RemoteAsset;
|
||||||
return remoteAsset.id;
|
return remoteAsset.id;
|
||||||
@@ -64,14 +66,14 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addUsers(BuildContext context) async {
|
Future<void> addUsers(BuildContext context) async {
|
||||||
final newUsers = await context.pushRoute<List<String>>(DriftUserSelectionRoute(album: widget.album));
|
final newUsers = await context.pushRoute<List<String>>(DriftUserSelectionRoute(album: _album));
|
||||||
|
|
||||||
if (newUsers == null || newUsers.isEmpty) {
|
if (newUsers == null || newUsers.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ref.read(remoteAlbumProvider.notifier).addUsers(widget.album.id, newUsers);
|
await ref.read(remoteAlbumProvider.notifier).addUsers(_album.id, newUsers);
|
||||||
|
|
||||||
if (newUsers.isNotEmpty) {
|
if (newUsers.isNotEmpty) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
@@ -81,7 +83,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ref.invalidate(remoteAlbumSharedUsersProvider(widget.album.id));
|
ref.invalidate(remoteAlbumSharedUsersProvider(_album.id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -92,7 +94,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toggleAlbumOrder() async {
|
Future<void> toggleAlbumOrder() async {
|
||||||
await ref.read(remoteAlbumProvider.notifier).toggleAlbumOrder(widget.album.id);
|
await ref.read(remoteAlbumProvider.notifier).toggleAlbumOrder(_album.id);
|
||||||
|
|
||||||
ref.invalidate(timelineServiceProvider);
|
ref.invalidate(timelineServiceProvider);
|
||||||
}
|
}
|
||||||
@@ -106,7 +108,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
content: Column(
|
content: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text('album_delete_confirmation'.t(context: context, args: {'album': widget.album.name})),
|
Text('album_delete_confirmation'.t(context: context, args: {'album': _album.name})),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text('album_delete_confirmation_description'.t(context: context)),
|
Text('album_delete_confirmation_description'.t(context: context)),
|
||||||
],
|
],
|
||||||
@@ -128,7 +130,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
|
|
||||||
if (confirmed == true) {
|
if (confirmed == true) {
|
||||||
try {
|
try {
|
||||||
await ref.read(remoteAlbumProvider.notifier).deleteAlbum(widget.album.id);
|
await ref.read(remoteAlbumProvider.notifier).deleteAlbum(_album.id);
|
||||||
|
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -151,17 +153,20 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
final result = await showDialog<_EditAlbumData?>(
|
final result = await showDialog<_EditAlbumData?>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: true,
|
barrierDismissible: true,
|
||||||
builder: (context) => _EditAlbumDialog(album: widget.album),
|
builder: (context) => _EditAlbumDialog(album: _album),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result != null && context.mounted) {
|
if (result != null && context.mounted) {
|
||||||
|
setState(() {
|
||||||
|
_album = _album.copyWith(name: result.name, description: result.description ?? '');
|
||||||
|
});
|
||||||
HapticFeedback.mediumImpact();
|
HapticFeedback.mediumImpact();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void showOptionSheet(BuildContext context) {
|
void showOptionSheet(BuildContext context) {
|
||||||
final user = ref.watch(currentUserProvider);
|
final user = ref.watch(currentUserProvider);
|
||||||
final isOwner = user != null ? user.id == widget.album.ownerId : false;
|
final isOwner = user != null ? user.id == _album.ownerId : false;
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -205,7 +210,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
return ProviderScope(
|
return ProviderScope(
|
||||||
overrides: [
|
overrides: [
|
||||||
timelineServiceProvider.overrideWith((ref) {
|
timelineServiceProvider.overrideWith((ref) {
|
||||||
final timelineService = ref.watch(timelineFactoryProvider).remoteAlbum(albumId: widget.album.id);
|
final timelineService = ref.watch(timelineFactoryProvider).remoteAlbum(albumId: _album.id);
|
||||||
ref.onDispose(timelineService.dispose);
|
ref.onDispose(timelineService.dispose);
|
||||||
return timelineService;
|
return timelineService;
|
||||||
}),
|
}),
|
||||||
@@ -217,7 +222,7 @@ class _RemoteAlbumPageState extends ConsumerState<RemoteAlbumPage> {
|
|||||||
onToggleAlbumOrder: () => toggleAlbumOrder(),
|
onToggleAlbumOrder: () => toggleAlbumOrder(),
|
||||||
onEditTitle: () => showEditTitleAndDescription(context),
|
onEditTitle: () => showEditTitleAndDescription(context),
|
||||||
),
|
),
|
||||||
bottomSheet: RemoteAlbumBottomSheet(album: widget.album),
|
bottomSheet: RemoteAlbumBottomSheet(album: _album),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ class DriftTrashPage extends StatelessWidget {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
child: Timeline(
|
child: Timeline(
|
||||||
showStorageIndicator: true,
|
|
||||||
appBar: SliverAppBar(
|
appBar: SliverAppBar(
|
||||||
title: Text('trash'.t(context: context)),
|
title: Text('trash'.t(context: context)),
|
||||||
floating: true,
|
floating: true,
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ class LocalTimelinePage extends StatelessWidget {
|
|||||||
child: Timeline(
|
child: Timeline(
|
||||||
appBar: MesmerizingSliverAppBar(title: album.name),
|
appBar: MesmerizingSliverAppBar(title: album.name),
|
||||||
bottomSheet: const LocalAlbumBottomSheet(),
|
bottomSheet: const LocalAlbumBottomSheet(),
|
||||||
showStorageIndicator: true,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ 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/models/search/search_filter.model.dart';
|
import 'package:immich_mobile/models/search/search_filter.model.dart';
|
||||||
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
import 'package:immich_mobile/presentation/pages/search/paginated_search.provider.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_sheet.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/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
import 'package:immich_mobile/providers/search/search_input_focus.provider.dart';
|
||||||
@@ -627,7 +628,12 @@ class _SearchResultGrid extends ConsumerWidget {
|
|||||||
return timelineService;
|
return timelineService;
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
child: Timeline(key: ValueKey(searchResult.totalAssets), appBar: null, groupBy: GroupAssetsBy.none),
|
child: Timeline(
|
||||||
|
key: ValueKey(searchResult.totalAssets),
|
||||||
|
groupBy: GroupAssetsBy.none,
|
||||||
|
appBar: null,
|
||||||
|
bottomSheet: const GeneralBottomSheet(minChildSize: 0.20),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import 'package:immich_mobile/models/albums/album_search.model.dart';
|
|||||||
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
|
import 'package:immich_mobile/pages/common/large_leading_tile.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/album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/current_album.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/routing/router.dart';
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
@@ -578,6 +579,8 @@ class AddToAlbumHeader extends ConsumerWidget {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ref.read(currentRemoteAlbumProvider.notifier).setAlbum(newAlbum);
|
||||||
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
context.pushRoute(RemoteAlbumRoute(album: newAlbum));
|
context.pushRoute(RemoteAlbumRoute(album: newAlbum));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,13 +22,13 @@ class BaseBottomSheet extends ConsumerStatefulWidget {
|
|||||||
this.slivers,
|
this.slivers,
|
||||||
this.controller,
|
this.controller,
|
||||||
this.initialChildSize = 0.35,
|
this.initialChildSize = 0.35,
|
||||||
this.minChildSize = 0.15,
|
double? minChildSize,
|
||||||
this.maxChildSize = 0.65,
|
this.maxChildSize = 0.65,
|
||||||
this.expand = true,
|
this.expand = true,
|
||||||
this.shouldCloseOnMinExtent = true,
|
this.shouldCloseOnMinExtent = true,
|
||||||
this.resizeOnScroll = true,
|
this.resizeOnScroll = true,
|
||||||
this.backgroundColor,
|
this.backgroundColor,
|
||||||
});
|
}) : minChildSize = minChildSize ?? 0.15;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<BaseBottomSheet> createState() => _BaseDraggableScrollableSheetState();
|
ConsumerState<BaseBottomSheet> createState() => _BaseDraggableScrollableSheetState();
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ 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/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_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/edit_date_time_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_date_time_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_location_action_button.widget.dart';
|
||||||
@@ -26,7 +26,8 @@ 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 GeneralBottomSheet extends ConsumerWidget {
|
class GeneralBottomSheet extends ConsumerWidget {
|
||||||
const GeneralBottomSheet({super.key});
|
final double? minChildSize;
|
||||||
|
const GeneralBottomSheet({super.key, this.minChildSize});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -60,6 +61,7 @@ class GeneralBottomSheet extends ConsumerWidget {
|
|||||||
|
|
||||||
return BaseBottomSheet(
|
return BaseBottomSheet(
|
||||||
initialChildSize: 0.45,
|
initialChildSize: 0.45,
|
||||||
|
minChildSize: minChildSize,
|
||||||
maxChildSize: 0.85,
|
maxChildSize: 0.85,
|
||||||
shouldCloseOnMinExtent: false,
|
shouldCloseOnMinExtent: false,
|
||||||
actions: [
|
actions: [
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ 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/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/setting.model.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
import 'package:immich_mobile/extensions/duration_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.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/setting.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
|
||||||
class ThumbnailTile extends ConsumerWidget {
|
class ThumbnailTile extends ConsumerWidget {
|
||||||
@@ -13,7 +15,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
this.asset, {
|
this.asset, {
|
||||||
this.size = const Size.square(256),
|
this.size = const Size.square(256),
|
||||||
this.fit = BoxFit.cover,
|
this.fit = BoxFit.cover,
|
||||||
this.showStorageIndicator = true,
|
this.showStorageIndicator,
|
||||||
this.lockSelection = false,
|
this.lockSelection = false,
|
||||||
this.heroOffset,
|
this.heroOffset,
|
||||||
super.key,
|
super.key,
|
||||||
@@ -22,7 +24,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
final BaseAsset asset;
|
final BaseAsset asset;
|
||||||
final Size size;
|
final Size size;
|
||||||
final BoxFit fit;
|
final BoxFit fit;
|
||||||
final bool showStorageIndicator;
|
final bool? showStorageIndicator;
|
||||||
final bool lockSelection;
|
final bool lockSelection;
|
||||||
final int? heroOffset;
|
final int? heroOffset;
|
||||||
|
|
||||||
@@ -52,6 +54,9 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
|
|
||||||
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
|
final hasStack = asset is RemoteAsset && (asset as RemoteAsset).stackId != null;
|
||||||
|
|
||||||
|
final bool storageIndicator =
|
||||||
|
showStorageIndicator ?? ref.watch(settingsProvider.select((s) => s.get(Setting.showStorageIndicator)));
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
AnimatedContainer(
|
AnimatedContainer(
|
||||||
@@ -86,7 +91,7 @@ class ThumbnailTile extends ConsumerWidget {
|
|||||||
child: _VideoIndicator(asset.duration),
|
child: _VideoIndicator(asset.duration),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (showStorageIndicator)
|
if (storageIndicator)
|
||||||
switch (asset.storage) {
|
switch (asset.storage) {
|
||||||
AssetState.local => const Align(
|
AssetState.local => const Align(
|
||||||
alignment: Alignment.bottomRight,
|
alignment: Alignment.bottomRight,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:auto_route/auto_route.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
import 'package:flutter/widgets.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/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/presentation/widgets/timeline/header.widget.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment_builder.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
|
||||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
@@ -125,10 +126,14 @@ class _FixedSegmentRow extends ConsumerWidget {
|
|||||||
textDirection: Directionality.of(context),
|
textDirection: Directionality.of(context),
|
||||||
children: [
|
children: [
|
||||||
for (int i = 0; i < assets.length; i++)
|
for (int i = 0; i < assets.length; i++)
|
||||||
_AssetTileWidget(
|
TimelineAssetIndexWrapper(
|
||||||
key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)),
|
|
||||||
asset: assets[i],
|
|
||||||
assetIndex: assetIndex + i,
|
assetIndex: assetIndex + i,
|
||||||
|
segmentIndex: 0, // For simplicity, using 0 for now
|
||||||
|
child: _AssetTileWidget(
|
||||||
|
key: ValueKey(Object.hash(assets[i].heroTag, assetIndex + i, timelineService.hashCode)),
|
||||||
|
asset: assets[i],
|
||||||
|
assetIndex: assetIndex + i,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ class TimelineArgs {
|
|||||||
final double maxHeight;
|
final double maxHeight;
|
||||||
final double spacing;
|
final double spacing;
|
||||||
final int columnCount;
|
final int columnCount;
|
||||||
final bool showStorageIndicator;
|
final bool? showStorageIndicator;
|
||||||
final bool withStack;
|
final bool withStack;
|
||||||
final GroupAssetsBy? groupBy;
|
final GroupAssetsBy? groupBy;
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ class TimelineArgs {
|
|||||||
required this.maxHeight,
|
required this.maxHeight,
|
||||||
this.spacing = kTimelineSpacing,
|
this.spacing = kTimelineSpacing,
|
||||||
this.columnCount = kTimelineColumnCount,
|
this.columnCount = kTimelineColumnCount,
|
||||||
this.showStorageIndicator = false,
|
this.showStorageIndicator,
|
||||||
this.withStack = false,
|
this.withStack = false,
|
||||||
this.groupBy,
|
this.groupBy,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
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/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/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
@@ -15,6 +18,7 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/general_bottom_s
|
|||||||
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/scrubber.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/segment.model.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/timeline/timeline_drag_region.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
@@ -27,7 +31,7 @@ class Timeline extends StatelessWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.topSliverWidget,
|
this.topSliverWidget,
|
||||||
this.topSliverWidgetHeight,
|
this.topSliverWidgetHeight,
|
||||||
this.showStorageIndicator = false,
|
this.showStorageIndicator,
|
||||||
this.withStack = false,
|
this.withStack = false,
|
||||||
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
|
this.appBar = const ImmichSliverAppBar(floating: true, pinned: false, snap: false),
|
||||||
this.bottomSheet = const GeneralBottomSheet(),
|
this.bottomSheet = const GeneralBottomSheet(),
|
||||||
@@ -36,7 +40,7 @@ class Timeline extends StatelessWidget {
|
|||||||
|
|
||||||
final Widget? topSliverWidget;
|
final Widget? topSliverWidget;
|
||||||
final double? topSliverWidgetHeight;
|
final double? topSliverWidgetHeight;
|
||||||
final bool showStorageIndicator;
|
final bool? showStorageIndicator;
|
||||||
final Widget? appBar;
|
final Widget? appBar;
|
||||||
final Widget? bottomSheet;
|
final Widget? bottomSheet;
|
||||||
final bool withStack;
|
final bool withStack;
|
||||||
@@ -88,10 +92,31 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
final _scrollController = ScrollController();
|
final _scrollController = ScrollController();
|
||||||
StreamSubscription? _eventSubscription;
|
StreamSubscription? _eventSubscription;
|
||||||
|
|
||||||
|
// Drag selection state
|
||||||
|
bool _dragging = false;
|
||||||
|
TimelineAssetIndex? _dragAnchorIndex;
|
||||||
|
final Set<BaseAsset> _draggedAssets = HashSet();
|
||||||
|
ScrollPhysics? _scrollPhysics;
|
||||||
|
|
||||||
|
int _perRow = 4;
|
||||||
|
double _scaleFactor = 3.0;
|
||||||
|
double _baseScaleFactor = 3.0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_eventSubscription = EventStream.shared.listen(_onEvent);
|
_eventSubscription = EventStream.shared.listen(_onEvent);
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final currentTilesPerRow = ref.read(settingsProvider).get(Setting.tilesPerRow);
|
||||||
|
setState(() {
|
||||||
|
_perRow = currentTilesPerRow;
|
||||||
|
_scaleFactor = 7.0 - _perRow;
|
||||||
|
_baseScaleFactor = _scaleFactor;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
ref.listenManual(multiSelectProvider.select((s) => s.isEnabled), _onMultiSelectionToggled);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onEvent(Event event) {
|
void _onEvent(Event event) {
|
||||||
@@ -107,6 +132,10 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onMultiSelectionToggled(_, bool isEnabled) {
|
||||||
|
EventStream.shared.emit(MultiSelectToggleEvent(isEnabled));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
@@ -150,6 +179,71 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drag selection methods
|
||||||
|
void _setDragStartIndex(TimelineAssetIndex index) {
|
||||||
|
setState(() {
|
||||||
|
_scrollPhysics = const ClampingScrollPhysics();
|
||||||
|
_dragAnchorIndex = index;
|
||||||
|
_dragging = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _stopDrag() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
// Update the physics post frame to prevent sudden change in physics on iOS.
|
||||||
|
setState(() {
|
||||||
|
_scrollPhysics = null;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setState(() {
|
||||||
|
_dragging = false;
|
||||||
|
_draggedAssets.clear();
|
||||||
|
});
|
||||||
|
// Reset the scrolling state after a small delay to allow bottom sheet to expand again
|
||||||
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
if (mounted) {
|
||||||
|
ref.read(timelineStateProvider.notifier).setScrolling(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _dragScroll(ScrollDirection direction) {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
_scrollController.offset + (direction == ScrollDirection.forward ? 175 : -175),
|
||||||
|
duration: const Duration(milliseconds: 125),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragAssetEnter(TimelineAssetIndex index) {
|
||||||
|
if (_dragAnchorIndex == null || !_dragging) return;
|
||||||
|
|
||||||
|
final timelineService = ref.read(timelineServiceProvider);
|
||||||
|
final dragAnchorIndex = _dragAnchorIndex!;
|
||||||
|
|
||||||
|
// Calculate the range of assets to select
|
||||||
|
final startIndex = math.min(dragAnchorIndex.assetIndex, index.assetIndex);
|
||||||
|
final endIndex = math.max(dragAnchorIndex.assetIndex, index.assetIndex);
|
||||||
|
final count = endIndex - startIndex + 1;
|
||||||
|
|
||||||
|
// Load the assets in the range
|
||||||
|
if (timelineService.hasRange(startIndex, count)) {
|
||||||
|
final selectedAssets = timelineService.getAssets(startIndex, count);
|
||||||
|
|
||||||
|
// Clear previous drag selection and add new range
|
||||||
|
final multiSelectNotifier = ref.read(multiSelectProvider.notifier);
|
||||||
|
for (final asset in _draggedAssets) {
|
||||||
|
multiSelectNotifier.deselectAsset(asset);
|
||||||
|
}
|
||||||
|
_draggedAssets.clear();
|
||||||
|
|
||||||
|
for (final asset in selectedAssets) {
|
||||||
|
multiSelectNotifier.selectAsset(asset);
|
||||||
|
_draggedAssets.add(asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext _) {
|
Widget build(BuildContext _) {
|
||||||
final asyncSegments = ref.watch(timelineSegmentProvider);
|
final asyncSegments = ref.watch(timelineSegmentProvider);
|
||||||
@@ -177,43 +271,83 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
|
|
||||||
return PrimaryScrollController(
|
return PrimaryScrollController(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
child: Stack(
|
child: RawGestureDetector(
|
||||||
children: [
|
gestures: {
|
||||||
Scrubber(
|
CustomScaleGestureRecognizer: GestureRecognizerFactoryWithHandlers<CustomScaleGestureRecognizer>(
|
||||||
layoutSegments: segments,
|
() => CustomScaleGestureRecognizer(),
|
||||||
timelineHeight: maxHeight,
|
(CustomScaleGestureRecognizer scale) {
|
||||||
topPadding: topPadding,
|
scale.onStart = (details) {
|
||||||
bottomPadding: bottomPadding,
|
_baseScaleFactor = _scaleFactor;
|
||||||
monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight,
|
};
|
||||||
child: CustomScrollView(
|
|
||||||
primary: true,
|
scale.onUpdate = (details) {
|
||||||
cacheExtent: maxHeight * 2,
|
final newScaleFactor = math.max(math.min(5.0, _baseScaleFactor * details.scale), 1.0);
|
||||||
slivers: [
|
final newPerRow = 7 - newScaleFactor.toInt();
|
||||||
if (isSelectionMode) const SelectionSliverAppBar() else if (widget.appBar != null) widget.appBar!,
|
|
||||||
if (widget.topSliverWidget != null) widget.topSliverWidget!,
|
if (newPerRow != _perRow) {
|
||||||
_SliverSegmentedList(
|
setState(() {
|
||||||
segments: segments,
|
_scaleFactor = newScaleFactor;
|
||||||
delegate: SliverChildBuilderDelegate(
|
_perRow = newPerRow;
|
||||||
(ctx, index) {
|
});
|
||||||
if (index >= childCount) return null;
|
|
||||||
final segment = segments.findByIndex(index);
|
ref.read(settingsProvider.notifier).set(Setting.tilesPerRow, _perRow);
|
||||||
return segment?.builder(ctx, index) ?? const SizedBox.shrink();
|
}
|
||||||
},
|
};
|
||||||
childCount: childCount,
|
},
|
||||||
addAutomaticKeepAlives: false,
|
|
||||||
// We add repaint boundary around tiles, so skip the auto boundaries
|
|
||||||
addRepaintBoundaries: false,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (!isSelectionMode && isMultiSelectEnabled) ...[
|
},
|
||||||
const Positioned(top: 60, left: 25, child: _MultiSelectStatusButton()),
|
child: TimelineDragRegion(
|
||||||
if (widget.bottomSheet != null) widget.bottomSheet!,
|
onStart: _setDragStartIndex,
|
||||||
],
|
onAssetEnter: _handleDragAssetEnter,
|
||||||
],
|
onEnd: _stopDrag,
|
||||||
|
onScroll: _dragScroll,
|
||||||
|
onScrollStart: () {
|
||||||
|
// Minimize the bottom sheet when drag selection starts
|
||||||
|
ref.read(timelineStateProvider.notifier).setScrolling(true);
|
||||||
|
},
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Scrubber(
|
||||||
|
layoutSegments: segments,
|
||||||
|
timelineHeight: maxHeight,
|
||||||
|
topPadding: topPadding,
|
||||||
|
bottomPadding: bottomPadding,
|
||||||
|
monthSegmentSnappingOffset: widget.topSliverWidgetHeight ?? 0 + appBarExpandedHeight,
|
||||||
|
child: CustomScrollView(
|
||||||
|
primary: true,
|
||||||
|
physics: _scrollPhysics,
|
||||||
|
cacheExtent: maxHeight * 2,
|
||||||
|
slivers: [
|
||||||
|
if (isSelectionMode)
|
||||||
|
const SelectionSliverAppBar()
|
||||||
|
else if (widget.appBar != null)
|
||||||
|
widget.appBar!,
|
||||||
|
if (widget.topSliverWidget != null) widget.topSliverWidget!,
|
||||||
|
_SliverSegmentedList(
|
||||||
|
segments: segments,
|
||||||
|
delegate: SliverChildBuilderDelegate(
|
||||||
|
(ctx, index) {
|
||||||
|
if (index >= childCount) return null;
|
||||||
|
final segment = segments.findByIndex(index);
|
||||||
|
return segment?.builder(ctx, index) ?? const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
childCount: childCount,
|
||||||
|
addAutomaticKeepAlives: false,
|
||||||
|
// We add repaint boundary around tiles, so skip the auto boundaries
|
||||||
|
addRepaintBoundaries: false,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (!isSelectionMode && isMultiSelectEnabled) ...[
|
||||||
|
const Positioned(top: 60, left: 25, child: _MultiSelectStatusButton()),
|
||||||
|
if (widget.bottomSheet != null) widget.bottomSheet!,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -443,3 +577,11 @@ class _MultiSelectStatusButton extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// accepts a gesture even though it should reject it (because child won)
|
||||||
|
class CustomScaleGestureRecognizer extends ScaleGestureRecognizer {
|
||||||
|
@override
|
||||||
|
void rejectGesture(int pointer) {
|
||||||
|
acceptGesture(pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
|
class TimelineDragRegion extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
final void Function(TimelineAssetIndex valueKey)? onStart;
|
||||||
|
final void Function(TimelineAssetIndex valueKey)? onAssetEnter;
|
||||||
|
final void Function()? onEnd;
|
||||||
|
final void Function()? onScrollStart;
|
||||||
|
final void Function(ScrollDirection direction)? onScroll;
|
||||||
|
|
||||||
|
const TimelineDragRegion({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.onStart,
|
||||||
|
this.onAssetEnter,
|
||||||
|
this.onEnd,
|
||||||
|
this.onScrollStart,
|
||||||
|
this.onScroll,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State createState() => _TimelineDragRegionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineDragRegionState extends State<TimelineDragRegion> {
|
||||||
|
late TimelineAssetIndex? assetUnderPointer;
|
||||||
|
late TimelineAssetIndex? anchorAsset;
|
||||||
|
|
||||||
|
// Scroll related state
|
||||||
|
static const double scrollOffset = 0.10;
|
||||||
|
double? topScrollOffset;
|
||||||
|
double? bottomScrollOffset;
|
||||||
|
Timer? scrollTimer;
|
||||||
|
late bool scrollNotified;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
assetUnderPointer = null;
|
||||||
|
anchorAsset = null;
|
||||||
|
scrollNotified = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
topScrollOffset = null;
|
||||||
|
bottomScrollOffset = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
scrollTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return RawGestureDetector(
|
||||||
|
gestures: {
|
||||||
|
_CustomLongPressGestureRecognizer: GestureRecognizerFactoryWithHandlers<_CustomLongPressGestureRecognizer>(
|
||||||
|
() => _CustomLongPressGestureRecognizer(),
|
||||||
|
_registerCallbacks,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerCallbacks(_CustomLongPressGestureRecognizer recognizer) {
|
||||||
|
recognizer.onLongPressMoveUpdate = (details) => _onLongPressMove(details);
|
||||||
|
recognizer.onLongPressStart = (details) => _onLongPressStart(details);
|
||||||
|
recognizer.onLongPressUp = _onLongPressEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
TimelineAssetIndex? _getValueKeyAtPosition(Offset position) {
|
||||||
|
final box = context.findAncestorRenderObjectOfType<RenderBox>();
|
||||||
|
if (box == null) return null;
|
||||||
|
|
||||||
|
final hitTestResult = BoxHitTestResult();
|
||||||
|
final local = box.globalToLocal(position);
|
||||||
|
if (!box.hitTest(hitTestResult, position: local)) return null;
|
||||||
|
|
||||||
|
return (hitTestResult.path.firstWhereOrNull((hit) => hit.target is _TimelineAssetIndexProxy)?.target
|
||||||
|
as _TimelineAssetIndexProxy?)
|
||||||
|
?.index;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressStart(LongPressStartDetails event) {
|
||||||
|
/// Calculate widget height and scroll offset when long press starting instead of in [initState]
|
||||||
|
/// or [didChangeDependencies] as the grid might still be rendering into view to get the actual size
|
||||||
|
final height = context.size?.height;
|
||||||
|
if (height != null && (topScrollOffset == null || bottomScrollOffset == null)) {
|
||||||
|
topScrollOffset = height * scrollOffset;
|
||||||
|
bottomScrollOffset = height - topScrollOffset!;
|
||||||
|
}
|
||||||
|
|
||||||
|
final initialHit = _getValueKeyAtPosition(event.globalPosition);
|
||||||
|
anchorAsset = initialHit;
|
||||||
|
if (initialHit == null) return;
|
||||||
|
|
||||||
|
if (anchorAsset != null) {
|
||||||
|
widget.onStart?.call(anchorAsset!);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressEnd() {
|
||||||
|
scrollNotified = false;
|
||||||
|
scrollTimer?.cancel();
|
||||||
|
widget.onEnd?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressMove(LongPressMoveUpdateDetails event) {
|
||||||
|
if (anchorAsset == null) return;
|
||||||
|
if (topScrollOffset == null || bottomScrollOffset == null) return;
|
||||||
|
|
||||||
|
final currentDy = event.localPosition.dy;
|
||||||
|
|
||||||
|
if (currentDy > bottomScrollOffset!) {
|
||||||
|
scrollTimer ??= Timer.periodic(
|
||||||
|
const Duration(milliseconds: 50),
|
||||||
|
(_) => widget.onScroll?.call(ScrollDirection.forward),
|
||||||
|
);
|
||||||
|
} else if (currentDy < topScrollOffset!) {
|
||||||
|
scrollTimer ??= Timer.periodic(
|
||||||
|
const Duration(milliseconds: 50),
|
||||||
|
(_) => widget.onScroll?.call(ScrollDirection.reverse),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
scrollTimer?.cancel();
|
||||||
|
scrollTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentlyTouchingAsset = _getValueKeyAtPosition(event.globalPosition);
|
||||||
|
if (currentlyTouchingAsset == null) return;
|
||||||
|
|
||||||
|
if (assetUnderPointer != currentlyTouchingAsset) {
|
||||||
|
if (!scrollNotified) {
|
||||||
|
scrollNotified = true;
|
||||||
|
widget.onScrollStart?.call();
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.onAssetEnter?.call(currentlyTouchingAsset);
|
||||||
|
assetUnderPointer = currentlyTouchingAsset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomLongPressGestureRecognizer extends LongPressGestureRecognizer {
|
||||||
|
@override
|
||||||
|
void rejectGesture(int pointer) {
|
||||||
|
acceptGesture(pointer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineAssetIndexWrapper extends SingleChildRenderObjectWidget {
|
||||||
|
final int assetIndex;
|
||||||
|
final int segmentIndex;
|
||||||
|
|
||||||
|
const TimelineAssetIndexWrapper({
|
||||||
|
required Widget super.child,
|
||||||
|
required this.assetIndex,
|
||||||
|
required this.segmentIndex,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_TimelineAssetIndexProxy createRenderObject(BuildContext context) {
|
||||||
|
return _TimelineAssetIndexProxy(
|
||||||
|
index: TimelineAssetIndex(assetIndex: assetIndex, segmentIndex: segmentIndex),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void updateRenderObject(
|
||||||
|
BuildContext context,
|
||||||
|
// ignore: library_private_types_in_public_api
|
||||||
|
_TimelineAssetIndexProxy renderObject,
|
||||||
|
) {
|
||||||
|
renderObject.index = TimelineAssetIndex(assetIndex: assetIndex, segmentIndex: segmentIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TimelineAssetIndexProxy extends RenderProxyBox {
|
||||||
|
TimelineAssetIndex index;
|
||||||
|
|
||||||
|
_TimelineAssetIndexProxy({required this.index});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TimelineAssetIndex {
|
||||||
|
final int assetIndex;
|
||||||
|
final int segmentIndex;
|
||||||
|
|
||||||
|
const TimelineAssetIndex({required this.assetIndex, required this.segmentIndex});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(covariant TimelineAssetIndex other) {
|
||||||
|
if (identical(this, other)) return true;
|
||||||
|
|
||||||
|
return other.assetIndex == assetIndex && other.segmentIndex == segmentIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => assetIndex.hashCode ^ segmentIndex.hashCode;
|
||||||
|
}
|
||||||
@@ -86,11 +86,12 @@ class AppLifeCycleNotifier extends StateNotifier<AppLifeCycleEnum> {
|
|||||||
// Ensure proper cleanup before starting new background tasks
|
// Ensure proper cleanup before starting new background tasks
|
||||||
try {
|
try {
|
||||||
await Future.wait([
|
await Future.wait([
|
||||||
backgroundManager.syncLocal().then((_) {
|
Future(() async {
|
||||||
|
await backgroundManager.syncLocal();
|
||||||
Logger("AppLifeCycleNotifier").fine("Hashing assets after syncLocal");
|
Logger("AppLifeCycleNotifier").fine("Hashing assets after syncLocal");
|
||||||
// Check if app is still active before hashing
|
// Check if app is still active before hashing
|
||||||
if (state == AppLifeCycleEnum.resumed) {
|
if ([AppLifeCycleEnum.resumed, AppLifeCycleEnum.active].contains(state)) {
|
||||||
backgroundManager.hashAssets();
|
await backgroundManager.hashAssets();
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
backgroundManager.syncRemote(),
|
backgroundManager.syncRemote(),
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/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/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
|
||||||
final multiSelectProvider = NotifierProvider<MultiSelectNotifier, MultiSelectState>(
|
final multiSelectProvider = NotifierProvider<MultiSelectNotifier, MultiSelectState>(
|
||||||
@@ -10,6 +10,11 @@ final multiSelectProvider = NotifierProvider<MultiSelectNotifier, MultiSelectSta
|
|||||||
dependencies: [timelineServiceProvider],
|
dependencies: [timelineServiceProvider],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
class MultiSelectToggleEvent extends Event {
|
||||||
|
final bool isEnabled;
|
||||||
|
const MultiSelectToggleEvent(this.isEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
class MultiSelectState {
|
class MultiSelectState {
|
||||||
final Set<BaseAsset> selectedAssets;
|
final Set<BaseAsset> selectedAssets;
|
||||||
final Set<BaseAsset> lockedSelectionAssets;
|
final Set<BaseAsset> lockedSelectionAssets;
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class AuthApiRepository extends ApiRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> logout() async {
|
Future<void> logout() async {
|
||||||
|
if (_apiService.apiClient.basePath.isEmpty) return;
|
||||||
|
|
||||||
await _apiService.authenticationApi.logout().timeout(const Duration(seconds: 7));
|
await _apiService.authenticationApi.logout().timeout(const Duration(seconds: 7));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:auto_route/auto_route.dart';
|
||||||
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';
|
||||||
@@ -18,6 +19,7 @@ import 'package:immich_mobile/providers/infrastructure/current_album.provider.da
|
|||||||
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/remote_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
import 'package:immich_mobile/providers/timeline/multiselect.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
import 'package:immich_mobile/widgets/album/remote_album_shared_user_icons.dart';
|
||||||
|
|
||||||
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
class RemoteAlbumSliverAppBar extends ConsumerStatefulWidget {
|
||||||
@@ -93,7 +95,7 @@ class _MesmerizingSliverAppBarState extends ConsumerState<RemoteAlbumSliverAppBa
|
|||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
ref.read(remoteAlbumProvider.notifier).refresh();
|
ref.read(remoteAlbumProvider.notifier).refresh();
|
||||||
context.pop();
|
context.navigateTo(const TabShellRoute(children: [DriftAlbumsRoute()]));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
@@ -26,7 +25,7 @@ class _SelectionSliverAppBarState extends ConsumerState<SelectionSliverAppBar> {
|
|||||||
|
|
||||||
onDone(Set<BaseAsset> selected) {
|
onDone(Set<BaseAsset> selected) {
|
||||||
ref.read(multiSelectProvider.notifier).reset();
|
ref.read(multiSelectProvider.notifier).reset();
|
||||||
context.maybePop<Set<BaseAsset>>(selected);
|
context.pop<Set<BaseAsset>>(selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SliverAppBar(
|
return SliverAppBar(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/models/map/map_event.model.dart';
|
import 'package:immich_mobile/models/map/map_event.model.dart';
|
||||||
import 'package:immich_mobile/providers/db.provider.dart';
|
import 'package:immich_mobile/providers/db.provider.dart';
|
||||||
import 'package:immich_mobile/providers/timeline.provider.dart';
|
import 'package:immich_mobile/providers/timeline.provider.dart';
|
||||||
@@ -229,9 +230,7 @@ class _MapSheetDragRegion extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final assetsInBoundsText = assetsInBoundCount > 0
|
final assetsInBoundsText = "map_assets_in_bounds".t(context: context, args: {'count': assetsInBoundCount});
|
||||||
? "map_assets_in_bounds".tr(namedArgs: {'count': assetsInBoundCount.toString()})
|
|
||||||
: "map_no_assets_in_bounds".tr();
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
|
|||||||
@@ -2,11 +2,13 @@ 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/providers/app_settings.provider.dart';
|
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||||
|
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart';
|
import 'package:immich_mobile/widgets/settings/asset_list_settings/asset_list_group_settings.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
import 'package:immich_mobile/widgets/settings/settings_sub_page_scaffold.dart';
|
||||||
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
|
||||||
import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
|
|
||||||
import 'asset_list_layout_settings.dart';
|
import 'asset_list_layout_settings.dart';
|
||||||
|
|
||||||
class AssetListSettings extends HookConsumerWidget {
|
class AssetListSettings extends HookConsumerWidget {
|
||||||
@@ -20,7 +22,10 @@ class AssetListSettings extends HookConsumerWidget {
|
|||||||
SettingsSwitchListTile(
|
SettingsSwitchListTile(
|
||||||
valueNotifier: showStorageIndicator,
|
valueNotifier: showStorageIndicator,
|
||||||
title: 'theme_setting_asset_list_storage_indicator_title'.tr(),
|
title: 'theme_setting_asset_list_storage_indicator_title'.tr(),
|
||||||
onChanged: (_) => ref.invalidate(appSettingsServiceProvider),
|
onChanged: (_) {
|
||||||
|
ref.invalidate(appSettingsServiceProvider);
|
||||||
|
ref.invalidate(settingsProvider);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const LayoutSettings(),
|
const LayoutSettings(),
|
||||||
const GroupSettings(),
|
const GroupSettings(),
|
||||||
|
|||||||
Generated
+1
-1
@@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.137.1
|
- API version: 1.137.3
|
||||||
- Generator version: 7.8.0
|
- Generator version: 7.8.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.137.1+3001
|
version: 1.137.3+3002
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.8.0 <4.0.0'
|
sdk: '>=3.8.0 <4.0.0'
|
||||||
|
|||||||
@@ -9469,7 +9469,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
|||||||
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.137.1
|
* 1.137.3
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bullmq": "^11.0.1",
|
"@nestjs/bullmq": "^11.0.1",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return val;
|
return Math.round(val);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLensModel = (exifTags: ImmichTags): string | null => {
|
const getLensModel = (exifTags: ImmichTags): string | null => {
|
||||||
|
|||||||
@@ -101,6 +101,10 @@ export class StorageService extends BaseService {
|
|||||||
const savedValue = await this.systemMetadataRepository.get(SystemMetadataKey.MediaLocation);
|
const savedValue = await this.systemMetadataRepository.get(SystemMetadataKey.MediaLocation);
|
||||||
let previous = savedValue?.location || '';
|
let previous = savedValue?.location || '';
|
||||||
|
|
||||||
|
if (!previous && this.configRepository.getEnv().storage.mediaLocation) {
|
||||||
|
previous = current;
|
||||||
|
}
|
||||||
|
|
||||||
if (!previous) {
|
if (!previous) {
|
||||||
previous = path.startsWith('upload/') ? 'upload' : '/usr/src/app/upload';
|
previous = path.startsWith('upload/') ? 'upload' : '/usr/src/app/upload';
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+3
-3
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.137.1",
|
"version": "1.137.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -63,8 +63,9 @@
|
|||||||
touchmoveTwoFingers: false,
|
touchmoveTwoFingers: false,
|
||||||
mousewheelCtrlKey: false,
|
mousewheelCtrlKey: false,
|
||||||
navbar,
|
navbar,
|
||||||
minFov: 10,
|
minFov: 15,
|
||||||
maxFov: 120,
|
maxFov: 90,
|
||||||
|
zoomSpeed: 0.5,
|
||||||
fisheye: false,
|
fisheye: false,
|
||||||
});
|
});
|
||||||
const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin;
|
const resolutionPlugin = viewer.getPlugin(ResolutionPlugin) as ResolutionPlugin;
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="me-4 flex place-items-center gap-1 justify-self-end">
|
<div class="max-[350px]:me-0 max-[350px]:gap-0 me-4 flex place-items-center gap-1 justify-self-end">
|
||||||
{@render trailing?.()}
|
{@render trailing?.()}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -230,7 +230,8 @@
|
|||||||
type="text"
|
type="text"
|
||||||
name="q"
|
name="q"
|
||||||
id="main-search-bar"
|
id="main-search-bar"
|
||||||
class="w-full transition-all border-2 px-14 py-4 max-md:py-2 text-immich-fg/75 dark:text-immich-dark-fg
|
class="w-full transition-all border-2 ps-14 py-4 max-md:py-2 text-immich-fg/75 dark:text-immich-dark-fg
|
||||||
|
{showClearIcon ? 'pe-[90px]' : 'pe-14'}
|
||||||
{grayTheme ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
|
{grayTheme ? 'dark:bg-immich-dark-gray' : 'dark:bg-immich-dark-bg'}
|
||||||
{showSuggestions && isSearchSuggestions ? 'rounded-t-3xl' : 'rounded-3xl bg-gray-200'}
|
{showSuggestions && isSearchSuggestions ? 'rounded-t-3xl' : 'rounded-3xl bg-gray-200'}
|
||||||
{searchStore.isSearchEnabled ? 'border-gray-200 dark:border-gray-700 bg-white' : 'border-transparent'}"
|
{searchStore.isSearchEnabled ? 'border-gray-200 dark:border-gray-700 bg-white' : 'border-transparent'}"
|
||||||
@@ -285,6 +286,7 @@
|
|||||||
{#if isFocus}
|
{#if isFocus}
|
||||||
<div
|
<div
|
||||||
class="absolute inset-y-0 flex items-center"
|
class="absolute inset-y-0 flex items-center"
|
||||||
|
class:max-md:hidden={value}
|
||||||
class:end-16={isFocus}
|
class:end-16={isFocus}
|
||||||
class:end-28={isFocus && value.length > 0}
|
class:end-28={isFocus && value.length > 0}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user