mirror of
https://github.com/immich-app/immich.git
synced 2025-12-13 09:13:17 +03:00
Compare commits
34 Commits
fix/album-
...
feature/re
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c3f1753f1 | ||
|
|
185134953d | ||
|
|
c136b5f6ee | ||
|
|
a604a0ad6d | ||
|
|
a84f4fc1bd | ||
|
|
f91d5d7da8 | ||
|
|
1517385704 | ||
|
|
598e856322 | ||
|
|
6d499c782a | ||
|
|
7af99b8606 | ||
|
|
01e39277e0 | ||
|
|
17361d189c | ||
|
|
7473b959dc | ||
|
|
06e79703da | ||
|
|
c360781565 | ||
|
|
f874c12bee | ||
|
|
2d4e901c55 | ||
|
|
1e5c3d7d37 | ||
|
|
3700f9980f | ||
|
|
287f6d5c94 | ||
|
|
fe9125a3d1 | ||
|
|
8b31936bb6 | ||
|
|
19958dfd83 | ||
|
|
7f3386c8d0 | ||
|
|
be9e632efb | ||
|
|
80c1771cb2 | ||
|
|
c7c929b3b5 | ||
|
|
72f18183a1 | ||
|
|
7f9ba91c8d | ||
|
|
6c07915f84 | ||
|
|
d4df41dc38 | ||
|
|
0eae657611 | ||
|
|
74f90fe944 | ||
|
|
eb7813047b |
@@ -652,6 +652,7 @@
|
|||||||
"backup_options_page_title": "Backup options",
|
"backup_options_page_title": "Backup options",
|
||||||
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
||||||
"backup_settings_subtitle": "Manage upload settings",
|
"backup_settings_subtitle": "Manage upload settings",
|
||||||
|
"backup_upload_details_page_more_details": "Tap for more details",
|
||||||
"backward": "Backward",
|
"backward": "Backward",
|
||||||
"biometric_auth_enabled": "Biometric authentication enabled",
|
"biometric_auth_enabled": "Biometric authentication enabled",
|
||||||
"biometric_locked_out": "You are locked out of biometric authentication",
|
"biometric_locked_out": "You are locked out of biometric authentication",
|
||||||
@@ -718,6 +719,7 @@
|
|||||||
"check_corrupt_asset_backup_button": "Perform check",
|
"check_corrupt_asset_backup_button": "Perform check",
|
||||||
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
|
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
|
||||||
"check_logs": "Check Logs",
|
"check_logs": "Check Logs",
|
||||||
|
"checksum": "Checksum",
|
||||||
"choose_matching_people_to_merge": "Choose matching people to merge",
|
"choose_matching_people_to_merge": "Choose matching people to merge",
|
||||||
"city": "City",
|
"city": "City",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
@@ -1166,6 +1168,7 @@
|
|||||||
"header_settings_header_name_input": "Header name",
|
"header_settings_header_name_input": "Header name",
|
||||||
"header_settings_header_value_input": "Header value",
|
"header_settings_header_value_input": "Header value",
|
||||||
"headers_settings_tile_title": "Custom proxy headers",
|
"headers_settings_tile_title": "Custom proxy headers",
|
||||||
|
"height": "Height",
|
||||||
"hi_user": "Hi {name} ({email})",
|
"hi_user": "Hi {name} ({email})",
|
||||||
"hide_all_people": "Hide all people",
|
"hide_all_people": "Hide all people",
|
||||||
"hide_gallery": "Hide gallery",
|
"hide_gallery": "Hide gallery",
|
||||||
@@ -1288,6 +1291,7 @@
|
|||||||
"local": "Local",
|
"local": "Local",
|
||||||
"local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server",
|
"local_asset_cast_failed": "Unable to cast an asset that is not uploaded to the server",
|
||||||
"local_assets": "Local Assets",
|
"local_assets": "Local Assets",
|
||||||
|
"local_id": "Local ID",
|
||||||
"local_media_summary": "Local Media Summary",
|
"local_media_summary": "Local Media Summary",
|
||||||
"local_network": "Local network",
|
"local_network": "Local network",
|
||||||
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
|
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
|
||||||
@@ -1663,6 +1667,8 @@
|
|||||||
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
||||||
"query_asset_id": "Query Asset ID",
|
"query_asset_id": "Query Asset ID",
|
||||||
"queue_status": "Queuing {count}/{total}",
|
"queue_status": "Queuing {count}/{total}",
|
||||||
|
"quick_actions_settings_description": "Drag to rearrange buttons. Up to {count} available buttons are displayed in order.",
|
||||||
|
"quick_actions_settings_title": "Button order settings",
|
||||||
"rating": "Star rating",
|
"rating": "Star rating",
|
||||||
"rating_clear": "Clear rating",
|
"rating_clear": "Clear rating",
|
||||||
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
"rating_count": "{count, plural, one {# star} other {# stars}}",
|
||||||
@@ -1723,6 +1729,7 @@
|
|||||||
"removed_photo_from_memory": "Removed photo from memory",
|
"removed_photo_from_memory": "Removed photo from memory",
|
||||||
"removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}",
|
"removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
|
"reorder_buttons": "Reorder buttons",
|
||||||
"repair": "Repair",
|
"repair": "Repair",
|
||||||
"repair_no_results_message": "Untracked and missing files will show up here",
|
"repair_no_results_message": "Untracked and missing files will show up here",
|
||||||
"replace_with_upload": "Replace with upload",
|
"replace_with_upload": "Replace with upload",
|
||||||
@@ -2218,6 +2225,7 @@
|
|||||||
"week": "Week",
|
"week": "Week",
|
||||||
"welcome": "Welcome",
|
"welcome": "Welcome",
|
||||||
"welcome_to_immich": "Welcome to Immich",
|
"welcome_to_immich": "Welcome to Immich",
|
||||||
|
"width": "Width",
|
||||||
"wifi_name": "Wi-Fi Name",
|
"wifi_name": "Wi-Fi Name",
|
||||||
"workflow": "Workflow",
|
"workflow": "Workflow",
|
||||||
"wrong_pin_code": "Wrong PIN code",
|
"wrong_pin_code": "Wrong PIN code",
|
||||||
|
|||||||
@@ -32,9 +32,6 @@
|
|||||||
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */; };
|
FEAFA8732E4D42F4001E47FE /* Thumbhash.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEAFA8722E4D42F4001E47FE /* Thumbhash.swift */; };
|
||||||
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */; };
|
FED3B1962E253E9B0030FD97 /* ThumbnailsImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1942E253E9B0030FD97 /* ThumbnailsImpl.swift */; };
|
||||||
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */; };
|
FED3B1972E253E9B0030FD97 /* Thumbnails.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = FED3B1932E253E9B0030FD97 /* Thumbnails.g.swift */; };
|
||||||
FEE084F82EC172460045228E /* SQLiteData in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084F72EC172460045228E /* SQLiteData */; };
|
|
||||||
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */; };
|
|
||||||
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */ = {isa = PBXBuildFile; productRef = FEE084FC2EC1725A0045228E /* StructuredFieldValues */; };
|
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -156,13 +153,6 @@
|
|||||||
path = WidgetExtension;
|
path = WidgetExtension;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
FEE084F22EC172080045228E /* Schemas */ = {
|
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
|
||||||
exceptions = (
|
|
||||||
);
|
|
||||||
path = Schemas;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -170,9 +160,6 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
FEE084F82EC172460045228E /* SQLiteData in Frameworks */,
|
|
||||||
FEE084FB2EC1725A0045228E /* RawStructuredFieldValues in Frameworks */,
|
|
||||||
FEE084FD2EC1725A0045228E /* StructuredFieldValues in Frameworks */,
|
|
||||||
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */,
|
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -267,7 +254,6 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */ = {
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
FEE084F22EC172080045228E /* Schemas */,
|
|
||||||
B231F52D2E93A44A00BC45D1 /* Core */,
|
B231F52D2E93A44A00BC45D1 /* Core */,
|
||||||
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
B25D37792E72CA15008B6CA7 /* Connectivity */,
|
||||||
B21E34A62E5AF9760031FDB9 /* Background */,
|
B21E34A62E5AF9760031FDB9 /* Background */,
|
||||||
@@ -355,7 +341,6 @@
|
|||||||
fileSystemSynchronizedGroups = (
|
fileSystemSynchronizedGroups = (
|
||||||
B231F52D2E93A44A00BC45D1 /* Core */,
|
B231F52D2E93A44A00BC45D1 /* Core */,
|
||||||
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
B2CF7F8C2DDE4EBB00744BF6 /* Sync */,
|
||||||
FEE084F22EC172080045228E /* Schemas */,
|
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
@@ -434,10 +419,6 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 97C146E51CF9000F007C117D;
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
packageReferences = (
|
|
||||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */,
|
|
||||||
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */,
|
|
||||||
);
|
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
@@ -1220,43 +1201,6 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
|
||||||
FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/pointfreeco/sqlite-data";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 1.3.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */ = {
|
|
||||||
isa = XCRemoteSwiftPackageReference;
|
|
||||||
repositoryURL = "https://github.com/apple/swift-http-structured-headers.git";
|
|
||||||
requirement = {
|
|
||||||
kind = upToNextMajorVersion;
|
|
||||||
minimumVersion = 1.5.0;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
/* End XCRemoteSwiftPackageReference section */
|
|
||||||
|
|
||||||
/* Begin XCSwiftPackageProductDependency section */
|
|
||||||
FEE084F72EC172460045228E /* SQLiteData */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = FEE084F62EC172460045228E /* XCRemoteSwiftPackageReference "sqlite-data" */;
|
|
||||||
productName = SQLiteData;
|
|
||||||
};
|
|
||||||
FEE084FA2EC1725A0045228E /* RawStructuredFieldValues */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */;
|
|
||||||
productName = RawStructuredFieldValues;
|
|
||||||
};
|
|
||||||
FEE084FC2EC1725A0045228E /* StructuredFieldValues */ = {
|
|
||||||
isa = XCSwiftPackageProductDependency;
|
|
||||||
package = FEE084F92EC1725A0045228E /* XCRemoteSwiftPackageReference "swift-http-structured-headers" */;
|
|
||||||
productName = StructuredFieldValues;
|
|
||||||
};
|
|
||||||
/* End XCSwiftPackageProductDependency section */
|
|
||||||
};
|
};
|
||||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
{
|
|
||||||
"originHash" : "9be33bfaa68721646604aefff3cabbdaf9a193da192aae024c265065671f6c49",
|
|
||||||
"pins" : [
|
|
||||||
{
|
|
||||||
"identity" : "combine-schedulers",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/combine-schedulers",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "5928286acce13def418ec36d05a001a9641086f2",
|
|
||||||
"version" : "1.0.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "grdb.swift",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/groue/GRDB.swift",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "18497b68fdbb3a09528d260a0a0e1e7e61c8c53d",
|
|
||||||
"version" : "7.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "sqlite-data",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/sqlite-data",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "b66b894b9a5710f1072c8eb6448a7edfc2d743d9",
|
|
||||||
"version" : "1.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-case-paths",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-case-paths",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "6989976265be3f8d2b5802c722f9ba168e227c71",
|
|
||||||
"version" : "1.7.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-clocks",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-clocks",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "cc46202b53476d64e824e0b6612da09d84ffde8e",
|
|
||||||
"version" : "1.0.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-collections",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-collections",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e",
|
|
||||||
"version" : "1.3.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-concurrency-extras",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "5a3825302b1a0d744183200915a47b508c828e6f",
|
|
||||||
"version" : "1.3.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-custom-dump",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-custom-dump",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
|
||||||
"version" : "1.3.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-dependencies",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-dependencies",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "a10f9feeb214bc72b5337b6ef6d5a029360db4cc",
|
|
||||||
"version" : "1.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-http-structured-headers",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/apple/swift-http-structured-headers.git",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "a9f3c352f4d46afd155e00b3c6e85decae6bcbeb",
|
|
||||||
"version" : "1.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-identified-collections",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-identified-collections",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "322d9ffeeba85c9f7c4984b39422ec7cc3c56597",
|
|
||||||
"version" : "1.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-perception",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-perception",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "4f47ebafed5f0b0172cf5c661454fa8e28fb2ac4",
|
|
||||||
"version" : "2.0.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-sharing",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-sharing",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "3bfc408cc2d0bee2287c174da6b1c76768377818",
|
|
||||||
"version" : "2.7.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-snapshot-testing",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-snapshot-testing",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "a8b7c5e0ed33d8ab8887d1654d9b59f2cbad529b",
|
|
||||||
"version" : "1.18.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-structured-queries",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-structured-queries",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "1447ea20550f6f02c4b48cc80931c3ed40a9c756",
|
|
||||||
"version" : "0.25.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-syntax",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/swiftlang/swift-syntax",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "4799286537280063c85a32f09884cfbca301b1a1",
|
|
||||||
"version" : "602.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "swift-tagged",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/swift-tagged",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "3907a9438f5b57d317001dc99f3f11b46882272b",
|
|
||||||
"version" : "0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"identity" : "xctest-dynamic-overlay",
|
|
||||||
"kind" : "remoteSourceControl",
|
|
||||||
"location" : "https://github.com/pointfreeco/xctest-dynamic-overlay",
|
|
||||||
"state" : {
|
|
||||||
"revision" : "4c27acf5394b645b70d8ba19dc249c0472d5f618",
|
|
||||||
"version" : "1.7.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"version" : 3
|
|
||||||
}
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
import SQLiteData
|
|
||||||
|
|
||||||
struct Endpoint: Codable {
|
|
||||||
let url: URL
|
|
||||||
let status: Status
|
|
||||||
|
|
||||||
enum Status: String, Codable {
|
|
||||||
case loading, valid, error, unknown
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum StoreKey: Int, CaseIterable, QueryBindable {
|
|
||||||
// MARK: - Int
|
|
||||||
case _version = 0
|
|
||||||
static let version = Typed<Int>(rawValue: ._version)
|
|
||||||
case _deviceIdHash = 3
|
|
||||||
static let deviceIdHash = Typed<Int>(rawValue: ._deviceIdHash)
|
|
||||||
case _backupTriggerDelay = 8
|
|
||||||
static let backupTriggerDelay = Typed<Int>(rawValue: ._backupTriggerDelay)
|
|
||||||
case _tilesPerRow = 103
|
|
||||||
static let tilesPerRow = Typed<Int>(rawValue: ._tilesPerRow)
|
|
||||||
case _groupAssetsBy = 105
|
|
||||||
static let groupAssetsBy = Typed<Int>(rawValue: ._groupAssetsBy)
|
|
||||||
case _uploadErrorNotificationGracePeriod = 106
|
|
||||||
static let uploadErrorNotificationGracePeriod = Typed<Int>(rawValue: ._uploadErrorNotificationGracePeriod)
|
|
||||||
case _thumbnailCacheSize = 110
|
|
||||||
static let thumbnailCacheSize = Typed<Int>(rawValue: ._thumbnailCacheSize)
|
|
||||||
case _imageCacheSize = 111
|
|
||||||
static let imageCacheSize = Typed<Int>(rawValue: ._imageCacheSize)
|
|
||||||
case _albumThumbnailCacheSize = 112
|
|
||||||
static let albumThumbnailCacheSize = Typed<Int>(rawValue: ._albumThumbnailCacheSize)
|
|
||||||
case _selectedAlbumSortOrder = 113
|
|
||||||
static let selectedAlbumSortOrder = Typed<Int>(rawValue: ._selectedAlbumSortOrder)
|
|
||||||
case _logLevel = 115
|
|
||||||
static let logLevel = Typed<Int>(rawValue: ._logLevel)
|
|
||||||
case _mapRelativeDate = 119
|
|
||||||
static let mapRelativeDate = Typed<Int>(rawValue: ._mapRelativeDate)
|
|
||||||
case _mapThemeMode = 124
|
|
||||||
static let mapThemeMode = Typed<Int>(rawValue: ._mapThemeMode)
|
|
||||||
|
|
||||||
// MARK: - String
|
|
||||||
case _assetETag = 1
|
|
||||||
static let assetETag = Typed<String>(rawValue: ._assetETag)
|
|
||||||
case _currentUser = 2
|
|
||||||
static let currentUser = Typed<String>(rawValue: ._currentUser)
|
|
||||||
case _deviceId = 4
|
|
||||||
static let deviceId = Typed<String>(rawValue: ._deviceId)
|
|
||||||
case _accessToken = 11
|
|
||||||
static let accessToken = Typed<String>(rawValue: ._accessToken)
|
|
||||||
case _serverEndpoint = 12
|
|
||||||
static let serverEndpoint = Typed<String>(rawValue: ._serverEndpoint)
|
|
||||||
case _sslClientCertData = 15
|
|
||||||
static let sslClientCertData = Typed<String>(rawValue: ._sslClientCertData)
|
|
||||||
case _sslClientPasswd = 16
|
|
||||||
static let sslClientPasswd = Typed<String>(rawValue: ._sslClientPasswd)
|
|
||||||
case _themeMode = 102
|
|
||||||
static let themeMode = Typed<String>(rawValue: ._themeMode)
|
|
||||||
case _customHeaders = 127
|
|
||||||
static let customHeaders = Typed<[String: String]>(rawValue: ._customHeaders)
|
|
||||||
case _primaryColor = 128
|
|
||||||
static let primaryColor = Typed<String>(rawValue: ._primaryColor)
|
|
||||||
case _preferredWifiName = 133
|
|
||||||
static let preferredWifiName = Typed<String>(rawValue: ._preferredWifiName)
|
|
||||||
|
|
||||||
// MARK: - Endpoint
|
|
||||||
case _externalEndpointList = 135
|
|
||||||
static let externalEndpointList = Typed<[Endpoint]>(rawValue: ._externalEndpointList)
|
|
||||||
|
|
||||||
// MARK: - URL
|
|
||||||
case _localEndpoint = 134
|
|
||||||
static let localEndpoint = Typed<URL>(rawValue: ._localEndpoint)
|
|
||||||
case _serverUrl = 10
|
|
||||||
static let serverUrl = Typed<URL>(rawValue: ._serverUrl)
|
|
||||||
|
|
||||||
// MARK: - Date
|
|
||||||
case _backupFailedSince = 5
|
|
||||||
static let backupFailedSince = Typed<Date>(rawValue: ._backupFailedSince)
|
|
||||||
|
|
||||||
// MARK: - Bool
|
|
||||||
case _backupRequireWifi = 6
|
|
||||||
static let backupRequireWifi = Typed<Bool>(rawValue: ._backupRequireWifi)
|
|
||||||
case _backupRequireCharging = 7
|
|
||||||
static let backupRequireCharging = Typed<Bool>(rawValue: ._backupRequireCharging)
|
|
||||||
case _autoBackup = 13
|
|
||||||
static let autoBackup = Typed<Bool>(rawValue: ._autoBackup)
|
|
||||||
case _backgroundBackup = 14
|
|
||||||
static let backgroundBackup = Typed<Bool>(rawValue: ._backgroundBackup)
|
|
||||||
case _loadPreview = 100
|
|
||||||
static let loadPreview = Typed<Bool>(rawValue: ._loadPreview)
|
|
||||||
case _loadOriginal = 101
|
|
||||||
static let loadOriginal = Typed<Bool>(rawValue: ._loadOriginal)
|
|
||||||
case _dynamicLayout = 104
|
|
||||||
static let dynamicLayout = Typed<Bool>(rawValue: ._dynamicLayout)
|
|
||||||
case _backgroundBackupTotalProgress = 107
|
|
||||||
static let backgroundBackupTotalProgress = Typed<Bool>(rawValue: ._backgroundBackupTotalProgress)
|
|
||||||
case _backgroundBackupSingleProgress = 108
|
|
||||||
static let backgroundBackupSingleProgress = Typed<Bool>(rawValue: ._backgroundBackupSingleProgress)
|
|
||||||
case _storageIndicator = 109
|
|
||||||
static let storageIndicator = Typed<Bool>(rawValue: ._storageIndicator)
|
|
||||||
case _advancedTroubleshooting = 114
|
|
||||||
static let advancedTroubleshooting = Typed<Bool>(rawValue: ._advancedTroubleshooting)
|
|
||||||
case _preferRemoteImage = 116
|
|
||||||
static let preferRemoteImage = Typed<Bool>(rawValue: ._preferRemoteImage)
|
|
||||||
case _loopVideo = 117
|
|
||||||
static let loopVideo = Typed<Bool>(rawValue: ._loopVideo)
|
|
||||||
case _mapShowFavoriteOnly = 118
|
|
||||||
static let mapShowFavoriteOnly = Typed<Bool>(rawValue: ._mapShowFavoriteOnly)
|
|
||||||
case _selfSignedCert = 120
|
|
||||||
static let selfSignedCert = Typed<Bool>(rawValue: ._selfSignedCert)
|
|
||||||
case _mapIncludeArchived = 121
|
|
||||||
static let mapIncludeArchived = Typed<Bool>(rawValue: ._mapIncludeArchived)
|
|
||||||
case _ignoreIcloudAssets = 122
|
|
||||||
static let ignoreIcloudAssets = Typed<Bool>(rawValue: ._ignoreIcloudAssets)
|
|
||||||
case _selectedAlbumSortReverse = 123
|
|
||||||
static let selectedAlbumSortReverse = Typed<Bool>(rawValue: ._selectedAlbumSortReverse)
|
|
||||||
case _mapwithPartners = 125
|
|
||||||
static let mapwithPartners = Typed<Bool>(rawValue: ._mapwithPartners)
|
|
||||||
case _enableHapticFeedback = 126
|
|
||||||
static let enableHapticFeedback = Typed<Bool>(rawValue: ._enableHapticFeedback)
|
|
||||||
case _dynamicTheme = 129
|
|
||||||
static let dynamicTheme = Typed<Bool>(rawValue: ._dynamicTheme)
|
|
||||||
case _colorfulInterface = 130
|
|
||||||
static let colorfulInterface = Typed<Bool>(rawValue: ._colorfulInterface)
|
|
||||||
case _syncAlbums = 131
|
|
||||||
static let syncAlbums = Typed<Bool>(rawValue: ._syncAlbums)
|
|
||||||
case _autoEndpointSwitching = 132
|
|
||||||
static let autoEndpointSwitching = Typed<Bool>(rawValue: ._autoEndpointSwitching)
|
|
||||||
case _loadOriginalVideo = 136
|
|
||||||
static let loadOriginalVideo = Typed<Bool>(rawValue: ._loadOriginalVideo)
|
|
||||||
case _manageLocalMediaAndroid = 137
|
|
||||||
static let manageLocalMediaAndroid = Typed<Bool>(rawValue: ._manageLocalMediaAndroid)
|
|
||||||
case _readonlyModeEnabled = 138
|
|
||||||
static let readonlyModeEnabled = Typed<Bool>(rawValue: ._readonlyModeEnabled)
|
|
||||||
case _autoPlayVideo = 139
|
|
||||||
static let autoPlayVideo = Typed<Bool>(rawValue: ._autoPlayVideo)
|
|
||||||
case _photoManagerCustomFilter = 1000
|
|
||||||
static let photoManagerCustomFilter = Typed<Bool>(rawValue: ._photoManagerCustomFilter)
|
|
||||||
case _betaPromptShown = 1001
|
|
||||||
static let betaPromptShown = Typed<Bool>(rawValue: ._betaPromptShown)
|
|
||||||
case _betaTimeline = 1002
|
|
||||||
static let betaTimeline = Typed<Bool>(rawValue: ._betaTimeline)
|
|
||||||
case _enableBackup = 1003
|
|
||||||
static let enableBackup = Typed<Bool>(rawValue: ._enableBackup)
|
|
||||||
case _useWifiForUploadVideos = 1004
|
|
||||||
static let useWifiForUploadVideos = Typed<Bool>(rawValue: ._useWifiForUploadVideos)
|
|
||||||
case _useWifiForUploadPhotos = 1005
|
|
||||||
static let useWifiForUploadPhotos = Typed<Bool>(rawValue: ._useWifiForUploadPhotos)
|
|
||||||
case _needBetaMigration = 1006
|
|
||||||
static let needBetaMigration = Typed<Bool>(rawValue: ._needBetaMigration)
|
|
||||||
case _shouldResetSync = 1007
|
|
||||||
static let shouldResetSync = Typed<Bool>(rawValue: ._shouldResetSync)
|
|
||||||
|
|
||||||
struct Typed<T>: RawRepresentable {
|
|
||||||
let rawValue: StoreKey
|
|
||||||
|
|
||||||
@_transparent
|
|
||||||
init(rawValue value: StoreKey) {
|
|
||||||
self.rawValue = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum BackupSelection: Int, QueryBindable {
|
|
||||||
case selected, none, excluded
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AvatarColor: Int, QueryBindable {
|
|
||||||
case primary, pink, red, yellow, blue, green, purple, orange, gray, amber
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AlbumUserRole: Int, QueryBindable {
|
|
||||||
case editor, viewer
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MemoryType: Int, QueryBindable {
|
|
||||||
case onThisDay
|
|
||||||
}
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
import SQLiteData
|
|
||||||
|
|
||||||
enum StoreError: Error {
|
|
||||||
case invalidJSON(String)
|
|
||||||
case invalidURL(String)
|
|
||||||
case encodingFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
protocol StoreConvertible {
|
|
||||||
associatedtype StorageType
|
|
||||||
static func fromValue(_ value: StorageType) throws(StoreError) -> Self
|
|
||||||
static func toValue(_ value: Self) throws(StoreError) -> StorageType
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Int: StoreConvertible {
|
|
||||||
static func fromValue(_ value: Int) -> Int { value }
|
|
||||||
static func toValue(_ value: Int) -> Int { value }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Bool: StoreConvertible {
|
|
||||||
static func fromValue(_ value: Int) -> Bool { value == 1 }
|
|
||||||
static func toValue(_ value: Bool) -> Int { value ? 1 : 0 }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Date: StoreConvertible {
|
|
||||||
static func fromValue(_ value: Int) -> Date { Date(timeIntervalSince1970: TimeInterval(value) / 1000) }
|
|
||||||
static func toValue(_ value: Date) -> Int { Int(value.timeIntervalSince1970 * 1000) }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension String: StoreConvertible {
|
|
||||||
static func fromValue(_ value: String) -> String { value }
|
|
||||||
static func toValue(_ value: String) -> String { value }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension URL: StoreConvertible {
|
|
||||||
static func fromValue(_ value: String) throws(StoreError) -> URL {
|
|
||||||
guard let url = URL(string: value) else {
|
|
||||||
throw StoreError.invalidURL(value)
|
|
||||||
}
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
static func toValue(_ value: URL) -> String { value.absoluteString }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension StoreConvertible where Self: Codable, StorageType == String {
|
|
||||||
static var jsonDecoder: JSONDecoder { JSONDecoder() }
|
|
||||||
static var jsonEncoder: JSONEncoder { JSONEncoder() }
|
|
||||||
|
|
||||||
static func fromValue(_ value: String) throws(StoreError) -> Self {
|
|
||||||
do {
|
|
||||||
return try jsonDecoder.decode(Self.self, from: Data(value.utf8))
|
|
||||||
} catch {
|
|
||||||
throw StoreError.invalidJSON(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func toValue(_ value: Self) throws(StoreError) -> String {
|
|
||||||
let encoded: Data
|
|
||||||
do {
|
|
||||||
encoded = try jsonEncoder.encode(value)
|
|
||||||
} catch {
|
|
||||||
throw StoreError.encodingFailed
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let string = String(data: encoded, encoding: .utf8) else {
|
|
||||||
throw StoreError.encodingFailed
|
|
||||||
}
|
|
||||||
return string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Array: StoreConvertible where Element: Codable {
|
|
||||||
typealias StorageType = String
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Dictionary: StoreConvertible where Key == String, Value: Codable {
|
|
||||||
typealias StorageType = String
|
|
||||||
}
|
|
||||||
|
|
||||||
class StoreRepository {
|
|
||||||
private let db: DatabasePool
|
|
||||||
|
|
||||||
init(db: DatabasePool) {
|
|
||||||
self.db = db
|
|
||||||
}
|
|
||||||
|
|
||||||
func get<T: StoreConvertible>(_ key: StoreKey.Typed<T>) throws -> T? where T.StorageType == Int {
|
|
||||||
let query = Store.select(\.intValue).where { $0.id.eq(key.rawValue) }
|
|
||||||
if let value = try db.read({ conn in try query.fetchOne(conn) }) ?? nil {
|
|
||||||
return try T.fromValue(value)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func get<T: StoreConvertible>(_ key: StoreKey.Typed<T>) throws -> T? where T.StorageType == String {
|
|
||||||
let query = Store.select(\.stringValue).where { $0.id.eq(key.rawValue) }
|
|
||||||
if let value = try db.read({ conn in try query.fetchOne(conn) }) ?? nil {
|
|
||||||
return try T.fromValue(value)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func get<T: StoreConvertible>(_ key: StoreKey.Typed<T>) async throws -> T? where T.StorageType == Int {
|
|
||||||
let query = Store.select(\.intValue).where { $0.id.eq(key.rawValue) }
|
|
||||||
if let value = try await db.read({ conn in try query.fetchOne(conn) }) ?? nil {
|
|
||||||
return try T.fromValue(value)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func get<T: StoreConvertible>(_ key: StoreKey.Typed<T>) async throws -> T? where T.StorageType == String {
|
|
||||||
let query = Store.select(\.stringValue).where { $0.id.eq(key.rawValue) }
|
|
||||||
if let value = try await db.read({ conn in try query.fetchOne(conn) }) ?? nil {
|
|
||||||
return try T.fromValue(value)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func set<T: StoreConvertible>(_ key: StoreKey.Typed<T>, value: T) throws where T.StorageType == Int {
|
|
||||||
let value = try T.toValue(value)
|
|
||||||
try db.write { conn in
|
|
||||||
try Store.upsert { Store(id: key.rawValue, stringValue: nil, intValue: value) }.execute(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func set<T: StoreConvertible>(_ key: StoreKey.Typed<T>, value: T) throws where T.StorageType == String {
|
|
||||||
let value = try T.toValue(value)
|
|
||||||
try db.write { conn in
|
|
||||||
try Store.upsert { Store(id: key.rawValue, stringValue: value, intValue: nil) }.execute(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func set<T: StoreConvertible>(_ key: StoreKey.Typed<T>, value: T) async throws where T.StorageType == Int {
|
|
||||||
let value = try T.toValue(value)
|
|
||||||
try await db.write { conn in
|
|
||||||
try Store.upsert { Store(id: key.rawValue, stringValue: nil, intValue: value) }.execute(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func set<T: StoreConvertible>(_ key: StoreKey.Typed<T>, value: T) async throws where T.StorageType == String {
|
|
||||||
let value = try T.toValue(value)
|
|
||||||
try await db.write { conn in
|
|
||||||
try Store.upsert { Store(id: key.rawValue, stringValue: value, intValue: nil) }.execute(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
import GRDB
|
|
||||||
import SQLiteData
|
|
||||||
|
|
||||||
@Table("asset_face_entity")
|
|
||||||
struct AssetFace {
|
|
||||||
let id: String
|
|
||||||
let assetId: String
|
|
||||||
let personId: String?
|
|
||||||
let imageWidth: Int
|
|
||||||
let imageHeight: Int
|
|
||||||
let boundingBoxX1: Int
|
|
||||||
let boundingBoxY1: Int
|
|
||||||
let boundingBoxX2: Int
|
|
||||||
let boundingBoxY2: Int
|
|
||||||
let sourceType: String
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("auth_user_entity")
|
|
||||||
struct AuthUser {
|
|
||||||
let id: String
|
|
||||||
let name: String
|
|
||||||
let email: String
|
|
||||||
let isAdmin: Bool
|
|
||||||
let hasProfileImage: Bool
|
|
||||||
let profileChangedAt: Date
|
|
||||||
let avatarColor: AvatarColor
|
|
||||||
let quotaSizeInBytes: Int
|
|
||||||
let quotaUsageInBytes: Int
|
|
||||||
let pinCode: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("local_album_entity")
|
|
||||||
struct LocalAlbum {
|
|
||||||
let id: String
|
|
||||||
let backupSelection: BackupSelection
|
|
||||||
let linkedRemoteAlbumId: String?
|
|
||||||
let marker_: Bool?
|
|
||||||
let name: String
|
|
||||||
let isIosSharedAlbum: Bool
|
|
||||||
let updatedAt: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("local_album_asset_entity")
|
|
||||||
struct LocalAlbumAsset {
|
|
||||||
let id: ID
|
|
||||||
let marker_: String?
|
|
||||||
|
|
||||||
@Selection
|
|
||||||
struct ID {
|
|
||||||
let assetId: String
|
|
||||||
let albumId: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("local_asset_entity")
|
|
||||||
struct LocalAsset {
|
|
||||||
let id: String
|
|
||||||
let checksum: String?
|
|
||||||
let createdAt: Date
|
|
||||||
let durationInSeconds: Int?
|
|
||||||
let height: Int?
|
|
||||||
let isFavorite: Bool
|
|
||||||
let name: String
|
|
||||||
let orientation: String
|
|
||||||
let type: Int
|
|
||||||
let updatedAt: Date
|
|
||||||
let width: Int?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("memory_asset_entity")
|
|
||||||
struct MemoryAsset {
|
|
||||||
let id: ID
|
|
||||||
|
|
||||||
@Selection
|
|
||||||
struct ID {
|
|
||||||
let assetId: String
|
|
||||||
let albumId: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("memory_entity")
|
|
||||||
struct Memory {
|
|
||||||
let id: String
|
|
||||||
let createdAt: Date
|
|
||||||
let updatedAt: Date
|
|
||||||
let deletedAt: Date?
|
|
||||||
let ownerId: String
|
|
||||||
let type: MemoryType
|
|
||||||
let data: String
|
|
||||||
let isSaved: Bool
|
|
||||||
let memoryAt: Date
|
|
||||||
let seenAt: Date?
|
|
||||||
let showAt: Date?
|
|
||||||
let hideAt: Date?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("partner_entity")
|
|
||||||
struct Partner {
|
|
||||||
let id: ID
|
|
||||||
let inTimeline: Bool
|
|
||||||
|
|
||||||
@Selection
|
|
||||||
struct ID {
|
|
||||||
let sharedById: String
|
|
||||||
let sharedWithId: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("person_entity")
|
|
||||||
struct Person {
|
|
||||||
let id: String
|
|
||||||
let createdAt: Date
|
|
||||||
let updatedAt: Date
|
|
||||||
let ownerId: String
|
|
||||||
let name: String
|
|
||||||
let faceAssetId: String?
|
|
||||||
let isFavorite: Bool
|
|
||||||
let isHidden: Bool
|
|
||||||
let color: String?
|
|
||||||
let birthDate: Date?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("remote_album_entity")
|
|
||||||
struct RemoteAlbum {
|
|
||||||
let id: String
|
|
||||||
let createdAt: Date
|
|
||||||
let description: String?
|
|
||||||
let isActivityEnabled: Bool
|
|
||||||
let name: String
|
|
||||||
let order: Int
|
|
||||||
let ownerId: String
|
|
||||||
let thumbnailAssetId: String?
|
|
||||||
let updatedAt: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("remote_album_asset_entity")
|
|
||||||
struct RemoteAlbumAsset {
|
|
||||||
let id: ID
|
|
||||||
|
|
||||||
@Selection
|
|
||||||
struct ID {
|
|
||||||
let assetId: String
|
|
||||||
let albumId: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("remote_album_user_entity")
|
|
||||||
struct RemoteAlbumUser {
|
|
||||||
let id: ID
|
|
||||||
let role: AlbumUserRole
|
|
||||||
|
|
||||||
@Selection
|
|
||||||
struct ID {
|
|
||||||
let albumId: String
|
|
||||||
let userId: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("remote_asset_entity")
|
|
||||||
struct RemoteAsset {
|
|
||||||
let id: String
|
|
||||||
let checksum: String?
|
|
||||||
let deletedAt: Date?
|
|
||||||
let isFavorite: Int
|
|
||||||
let libraryId: String?
|
|
||||||
let livePhotoVideoId: String?
|
|
||||||
let localDateTime: Date?
|
|
||||||
let orientation: String
|
|
||||||
let ownerId: String
|
|
||||||
let stackId: String?
|
|
||||||
let visibility: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("remote_exif_entity")
|
|
||||||
struct RemoteExif {
|
|
||||||
@Column(primaryKey: true)
|
|
||||||
let assetId: String
|
|
||||||
let city: String?
|
|
||||||
let state: String?
|
|
||||||
let country: String?
|
|
||||||
let dateTimeOriginal: Date?
|
|
||||||
let description: String?
|
|
||||||
let height: Int?
|
|
||||||
let width: Int?
|
|
||||||
let exposureTime: String?
|
|
||||||
let fNumber: Double?
|
|
||||||
let fileSize: Int?
|
|
||||||
let focalLength: Double?
|
|
||||||
let latitude: Double?
|
|
||||||
let longitude: Double?
|
|
||||||
let iso: Int?
|
|
||||||
let make: String?
|
|
||||||
let model: String?
|
|
||||||
let lens: String?
|
|
||||||
let orientation: String?
|
|
||||||
let timeZone: String?
|
|
||||||
let rating: Int?
|
|
||||||
let projectionType: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("stack_entity")
|
|
||||||
struct Stack {
|
|
||||||
let id: String
|
|
||||||
let createdAt: Date
|
|
||||||
let updatedAt: Date
|
|
||||||
let ownerId: String
|
|
||||||
let primaryAssetId: String
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("store_entity")
|
|
||||||
struct Store {
|
|
||||||
let id: StoreKey
|
|
||||||
let stringValue: String?
|
|
||||||
let intValue: Int?
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("user_entity")
|
|
||||||
struct User {
|
|
||||||
let id: String
|
|
||||||
let name: String
|
|
||||||
let email: String
|
|
||||||
let hasProfileImage: Bool
|
|
||||||
let profileChangedAt: Date
|
|
||||||
let avatarColor: AvatarColor
|
|
||||||
}
|
|
||||||
|
|
||||||
@Table("user_metadata_entity")
|
|
||||||
struct UserMetadata {
|
|
||||||
let id: ID
|
|
||||||
let value: Data
|
|
||||||
|
|
||||||
@Selection
|
|
||||||
struct ID {
|
|
||||||
let userId: String
|
|
||||||
let key: Date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -72,6 +72,7 @@ enum StoreKey<T> {
|
|||||||
|
|
||||||
autoPlayVideo<bool>._(139),
|
autoPlayVideo<bool>._(139),
|
||||||
albumGridView<bool>._(140),
|
albumGridView<bool>._(140),
|
||||||
|
viewerQuickActionOrder<String>._(141),
|
||||||
|
|
||||||
// Experimental stuff
|
// Experimental stuff
|
||||||
photoManagerCustomFilter<bool>._(1000),
|
photoManagerCustomFilter<bool>._(1000),
|
||||||
|
|||||||
85
mobile/lib/domain/services/quick_action.service.dart
Normal file
85
mobile/lib/domain/services/quick_action.service.dart
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||||
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
|
||||||
|
class QuickActionService {
|
||||||
|
final ActionButtonOrderRepository _repository;
|
||||||
|
|
||||||
|
const QuickActionService(this._repository);
|
||||||
|
|
||||||
|
static final Set<ActionButtonType> _quickActionSet = Set<ActionButtonType>.unmodifiable(
|
||||||
|
ActionButtonBuilder.defaultQuickActionOrder,
|
||||||
|
);
|
||||||
|
|
||||||
|
List<ActionButtonType> get() {
|
||||||
|
return _repository.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> set(List<ActionButtonType> order) async {
|
||||||
|
final normalized = _normalizeQuickActionOrder(order);
|
||||||
|
await _repository.set(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<ActionButtonType>> watch() {
|
||||||
|
return _repository.watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ActionButtonType> _normalizeQuickActionOrder(List<ActionButtonType> order) {
|
||||||
|
final ordered = <ActionButtonType>{};
|
||||||
|
|
||||||
|
for (final type in order) {
|
||||||
|
if (_quickActionSet.contains(type)) {
|
||||||
|
ordered.add(type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ordered.addAll(ActionButtonBuilder.defaultQuickActionOrder);
|
||||||
|
|
||||||
|
return ordered.toList(growable: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ActionButtonType> buildQuickActionTypes(
|
||||||
|
ActionButtonContext context, {
|
||||||
|
List<ActionButtonType>? quickActionOrder,
|
||||||
|
int limit = ActionButtonBuilder.defaultQuickActionLimit,
|
||||||
|
}) {
|
||||||
|
final normalized = _normalizeQuickActionOrder(
|
||||||
|
quickActionOrder == null || quickActionOrder.isEmpty
|
||||||
|
? ActionButtonBuilder.defaultQuickActionOrder
|
||||||
|
: quickActionOrder,
|
||||||
|
);
|
||||||
|
|
||||||
|
final seen = <ActionButtonType>{};
|
||||||
|
final result = <ActionButtonType>[];
|
||||||
|
|
||||||
|
for (final type in normalized) {
|
||||||
|
if (!_quickActionSet.contains(type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final resolved = _resolveQuickActionType(type, context);
|
||||||
|
if (!seen.add(resolved) || !resolved.shouldShow(context)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.add(resolved);
|
||||||
|
if (result.length >= limit) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve quick action type based on context (e.g., archive -> unarchive)
|
||||||
|
ActionButtonType _resolveQuickActionType(ActionButtonType type, ActionButtonContext context) {
|
||||||
|
if (type == ActionButtonType.archive && context.isArchived) {
|
||||||
|
return ActionButtonType.unarchive;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == ActionButtonType.delete && context.asset.isLocalOnly) {
|
||||||
|
return ActionButtonType.deleteLocal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||||
|
import 'package:immich_mobile/entities/store.entity.dart';
|
||||||
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
|
||||||
|
class ActionButtonOrderRepository {
|
||||||
|
const ActionButtonOrderRepository();
|
||||||
|
|
||||||
|
static const storeKey = StoreKey.viewerQuickActionOrder;
|
||||||
|
|
||||||
|
List<ActionButtonType> get() {
|
||||||
|
final json = Store.tryGet(storeKey);
|
||||||
|
if (json == null || json.isEmpty) {
|
||||||
|
return ActionButtonBuilder.defaultQuickActionOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
final deserialized = _deserialize(json);
|
||||||
|
return deserialized.isEmpty ? ActionButtonBuilder.defaultQuickActionOrder : deserialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> set(List<ActionButtonType> order) async {
|
||||||
|
final json = _serialize(order);
|
||||||
|
await Store.put(storeKey, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<ActionButtonType>> watch() {
|
||||||
|
return Store.watch(storeKey).map((json) {
|
||||||
|
if (json == null || json.isEmpty) {
|
||||||
|
return ActionButtonBuilder.defaultQuickActionOrder;
|
||||||
|
}
|
||||||
|
final deserialized = _deserialize(json);
|
||||||
|
return deserialized.isEmpty ? ActionButtonBuilder.defaultQuickActionOrder : deserialized;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
String _serialize(List<ActionButtonType> order) {
|
||||||
|
return jsonEncode(order.map((type) => type.name).toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ActionButtonType> _deserialize(String json) {
|
||||||
|
try {
|
||||||
|
final list = jsonDecode(json) as List<dynamic>;
|
||||||
|
return list
|
||||||
|
.whereType<String>()
|
||||||
|
.map((name) {
|
||||||
|
try {
|
||||||
|
return ActionButtonType.values.byName(name);
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.whereType<ActionButtonType>()
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -98,7 +98,7 @@ class DriftUploadDetailPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Tap for more details',
|
"backup_upload_details_page_more_details".t(context: context),
|
||||||
style: context.textTheme.bodySmall?.copyWith(
|
style: context.textTheme.bodySmall?.copyWith(
|
||||||
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
color: context.colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
@@ -239,14 +239,20 @@ class FileDetailDialog extends ConsumerWidget {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
if (asset != null) ...[
|
if (asset != null) ...[
|
||||||
_buildInfoSection(context, [
|
_buildInfoSection(context, [
|
||||||
_buildInfoRow(context, "Filename", path.basename(uploadStatus.filename)),
|
_buildInfoRow(context, "filename".t(context: context), path.basename(uploadStatus.filename)),
|
||||||
_buildInfoRow(context, "Local ID", asset.id),
|
_buildInfoRow(context, "local_id".t(context: context), asset.id),
|
||||||
_buildInfoRow(context, "File Size", formatHumanReadableBytes(uploadStatus.fileSize, 2)),
|
_buildInfoRow(
|
||||||
if (asset.width != null) _buildInfoRow(context, "Width", "${asset.width}px"),
|
context,
|
||||||
if (asset.height != null) _buildInfoRow(context, "Height", "${asset.height}px"),
|
"file_size".t(context: context),
|
||||||
_buildInfoRow(context, "Created At", asset.createdAt.toString()),
|
formatHumanReadableBytes(uploadStatus.fileSize, 2),
|
||||||
_buildInfoRow(context, "Updated At", asset.updatedAt.toString()),
|
),
|
||||||
if (asset.checksum != null) _buildInfoRow(context, "Checksum", asset.checksum!),
|
if (asset.width != null) _buildInfoRow(context, "width".t(context: context), "${asset.width}px"),
|
||||||
|
if (asset.height != null)
|
||||||
|
_buildInfoRow(context, "height".t(context: context), "${asset.height}px"),
|
||||||
|
_buildInfoRow(context, "created_at".t(context: context), asset.createdAt.toString()),
|
||||||
|
_buildInfoRow(context, "updated_at".t(context: context), asset.updatedAt.toString()),
|
||||||
|
if (asset.checksum != null)
|
||||||
|
_buildInfoRow(context, "checksum".t(context: context), asset.checksum!),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -27,8 +27,19 @@ class _DriftCreateAlbumPageState extends ConsumerState<DriftCreateAlbumPage> {
|
|||||||
bool isAlbumTitleTextFieldFocus = false;
|
bool isAlbumTitleTextFieldFocus = false;
|
||||||
Set<BaseAsset> selectedAssets = {};
|
Set<BaseAsset> selectedAssets = {};
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
albumTitleController.addListener(_onTitleChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTitleChanged() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
albumTitleController.removeListener(_onTitleChanged);
|
||||||
albumTitleController.dispose();
|
albumTitleController.dispose();
|
||||||
albumDescriptionController.dispose();
|
albumDescriptionController.dispose();
|
||||||
albumTitleTextFieldFocusNode.dispose();
|
albumTitleTextFieldFocusNode.dispose();
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_shee
|
|||||||
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
|
enum AddToMenuItem { album, archive, unarchive, lockedFolder }
|
||||||
|
|
||||||
class AddActionButton extends ConsumerStatefulWidget {
|
class AddActionButton extends ConsumerStatefulWidget {
|
||||||
const AddActionButton({super.key});
|
const AddActionButton({super.key, this.originalTheme});
|
||||||
|
|
||||||
|
final ThemeData? originalTheme;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<AddActionButton> createState() => _AddActionButtonState();
|
ConsumerState<AddActionButton> createState() => _AddActionButtonState();
|
||||||
@@ -71,7 +73,7 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
|||||||
),
|
),
|
||||||
|
|
||||||
if (isOwner) ...[
|
if (isOwner) ...[
|
||||||
const PopupMenuDivider(),
|
const Divider(),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
|
child: Text("move_to".tr(), style: context.textTheme.labelMedium),
|
||||||
@@ -166,16 +168,27 @@ class _AddActionButtonState extends ConsumerState<AddActionButton> {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final themeData = widget.originalTheme ?? context.themeData;
|
||||||
|
|
||||||
return MenuAnchor(
|
return MenuAnchor(
|
||||||
consumeOutsideTap: true,
|
consumeOutsideTap: true,
|
||||||
style: MenuStyle(
|
style: MenuStyle(
|
||||||
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
|
backgroundColor: WidgetStatePropertyAll(themeData.scaffoldBackgroundColor),
|
||||||
|
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
|
||||||
elevation: const WidgetStatePropertyAll(4),
|
elevation: const WidgetStatePropertyAll(4),
|
||||||
shape: const WidgetStatePropertyAll(
|
shape: const WidgetStatePropertyAll(
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||||
),
|
),
|
||||||
|
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
|
||||||
),
|
),
|
||||||
menuChildren: _buildMenuChildren(),
|
menuChildren: widget.originalTheme != null
|
||||||
|
? [
|
||||||
|
Theme(
|
||||||
|
data: widget.originalTheme!,
|
||||||
|
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: _buildMenuChildren()),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: _buildMenuChildren(),
|
||||||
builder: (context, controller, child) {
|
builder: (context, controller, child) {
|
||||||
return BaseActionButton(
|
return BaseActionButton(
|
||||||
iconData: Icons.add,
|
iconData: Icons.add,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
class BaseActionButton extends StatelessWidget {
|
class BaseActionButton extends ConsumerWidget {
|
||||||
const BaseActionButton({
|
const BaseActionButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.label,
|
required this.label,
|
||||||
@@ -30,7 +31,7 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
final void Function()? onLongPressed;
|
final void Function()? onLongPressed;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
|
final miniWidth = minWidth ?? (context.isMobile ? context.width / 4.5 : 75.0);
|
||||||
final iconTheme = IconTheme.of(context);
|
final iconTheme = IconTheme.of(context);
|
||||||
final iconSize = iconTheme.size ?? 24.0;
|
final iconSize = iconTheme.size ?? 24.0;
|
||||||
@@ -46,14 +47,13 @@ class BaseActionButton extends StatelessWidget {
|
|||||||
|
|
||||||
if (menuItem) {
|
if (menuItem) {
|
||||||
final theme = context.themeData;
|
final theme = context.themeData;
|
||||||
final effectiveStyle = theme.textTheme.labelLarge;
|
|
||||||
final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant;
|
final effectiveIconColor = iconColor ?? theme.iconTheme.color ?? theme.colorScheme.onSurfaceVariant;
|
||||||
|
|
||||||
return MenuItemButton(
|
return MenuItemButton(
|
||||||
style: MenuItemButton.styleFrom(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12)),
|
style: MenuItemButton.styleFrom(alignment: Alignment.centerLeft, padding: const EdgeInsets.all(16)),
|
||||||
leadingIcon: Icon(iconData, color: effectiveIconColor, size: 20),
|
leadingIcon: Icon(iconData, color: effectiveIconColor),
|
||||||
onPressed: onPressed,
|
onPressed: onPressed,
|
||||||
child: Text(label, style: effectiveStyle),
|
child: Text(label, style: theme.textTheme.labelLarge?.copyWith(fontSize: 16)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import 'package:immich_mobile/providers/cast.provider.dart';
|
|||||||
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/cast_dialog.dart';
|
||||||
|
|
||||||
class CastActionButton extends ConsumerWidget {
|
class CastActionButton extends ConsumerWidget {
|
||||||
const CastActionButton({super.key, this.iconOnly = true, this.menuItem = false});
|
const CastActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||||
|
|
||||||
final bool iconOnly;
|
final bool iconOnly;
|
||||||
final bool menuItem;
|
final bool menuItem;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_bu
|
|||||||
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart';
|
||||||
|
|
||||||
class MotionPhotoActionButton extends ConsumerWidget {
|
class MotionPhotoActionButton extends ConsumerWidget {
|
||||||
const MotionPhotoActionButton({super.key, this.iconOnly = true, this.menuItem = false});
|
const MotionPhotoActionButton({super.key, this.iconOnly = false, this.menuItem = false});
|
||||||
|
|
||||||
final bool iconOnly;
|
final bool iconOnly;
|
||||||
final bool menuItem;
|
final bool menuItem;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/quick_action_configurator.widget.dart';
|
||||||
|
|
||||||
|
class ReorderButtonsActionButton extends ConsumerWidget {
|
||||||
|
const ReorderButtonsActionButton({super.key, this.originalTheme});
|
||||||
|
|
||||||
|
final ThemeData? originalTheme;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return BaseActionButton(
|
||||||
|
label: 'reorder_buttons'.tr(),
|
||||||
|
iconData: Icons.swap_vert,
|
||||||
|
iconColor: originalTheme?.iconTheme.color,
|
||||||
|
menuItem: true,
|
||||||
|
onPressed: () async {
|
||||||
|
final viewerNotifier = ref.read(assetViewerProvider.notifier);
|
||||||
|
viewerNotifier.setBottomSheet(true);
|
||||||
|
|
||||||
|
await showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
enableDrag: false,
|
||||||
|
builder: (sheetContext) => const FractionallySizedBox(heightFactor: 0.75, child: QuickActionConfigurator()),
|
||||||
|
).whenComplete(() {
|
||||||
|
viewerNotifier.setBottomSheet(false);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,18 +2,19 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/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/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/quick_action_configurator.widget.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/viewer_quick_action_order.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/setting.provider.dart';
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
|
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
|
||||||
|
|
||||||
class ViewerBottomBar extends ConsumerWidget {
|
class ViewerBottomBar extends ConsumerWidget {
|
||||||
@@ -33,23 +34,53 @@ class ViewerBottomBar extends ConsumerWidget {
|
|||||||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||||
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
|
final isArchived = asset is RemoteAsset && asset.visibility == AssetVisibility.archive;
|
||||||
|
final isTrashEnabled = ref.watch(serverInfoProvider.select((state) => state.serverFeatures.trash));
|
||||||
|
final currentAlbum = ref.watch(currentRemoteAlbumProvider);
|
||||||
|
final advancedTroubleshooting = ref.watch(settingsProvider.notifier).get(Setting.advancedTroubleshooting);
|
||||||
|
final quickActionOrder = ref.watch(viewerQuickActionOrderProvider);
|
||||||
|
|
||||||
if (!showControls) {
|
if (!showControls) {
|
||||||
opacity = 0;
|
opacity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
final actions = <Widget>[
|
final buttonContext = ActionButtonContext(
|
||||||
const ShareActionButton(source: ActionSource.viewer),
|
asset: asset,
|
||||||
if (asset.isLocalOnly) const UploadActionButton(source: ActionSource.viewer),
|
isOwner: isOwner,
|
||||||
if (asset.type == AssetType.image) const EditImageActionButton(),
|
isArchived: isArchived,
|
||||||
if (asset.hasRemote) const AddActionButton(),
|
isTrashEnabled: isTrashEnabled,
|
||||||
|
isStacked: asset is RemoteAsset && asset.stackId != null,
|
||||||
|
isInLockedView: isInLockedView,
|
||||||
|
currentAlbum: currentAlbum,
|
||||||
|
advancedTroubleshooting: advancedTroubleshooting,
|
||||||
|
source: ActionSource.viewer,
|
||||||
|
);
|
||||||
|
|
||||||
if (isOwner) ...[
|
final quickActionService = ref.watch(quickActionServiceProvider);
|
||||||
asset.isLocalOnly
|
final quickActionTypes = quickActionService.buildQuickActionTypes(
|
||||||
? const DeleteLocalActionButton(source: ActionSource.viewer)
|
buttonContext,
|
||||||
: const DeleteActionButton(source: ActionSource.viewer, showConfirmation: true),
|
quickActionOrder: quickActionOrder,
|
||||||
],
|
);
|
||||||
];
|
|
||||||
|
Future<void> openConfigurator() async {
|
||||||
|
final viewerNotifier = ref.read(assetViewerProvider.notifier);
|
||||||
|
|
||||||
|
viewerNotifier.setBottomSheet(true);
|
||||||
|
|
||||||
|
await showModalBottomSheet<void>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
enableDrag: false,
|
||||||
|
builder: (sheetContext) => const FractionallySizedBox(heightFactor: 0.75, child: QuickActionConfigurator()),
|
||||||
|
).whenComplete(() {
|
||||||
|
viewerNotifier.setBottomSheet(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final actions = quickActionTypes
|
||||||
|
.map((type) => type.buildButton(buttonContext))
|
||||||
|
.map((widget) => GestureDetector(onLongPress: openConfigurator, child: widget))
|
||||||
|
.toList(growable: false);
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: opacity < 255,
|
ignoring: opacity < 255,
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/viewer_quick_action_order.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
import 'package:immich_mobile/utils/action_button_visuals.dart';
|
||||||
|
import 'package:immich_mobile/widgets/common/reorderable_drag_drop_grid.dart';
|
||||||
|
|
||||||
|
class QuickActionConfigurator extends ConsumerStatefulWidget {
|
||||||
|
const QuickActionConfigurator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<QuickActionConfigurator> createState() => _QuickActionConfiguratorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickActionConfiguratorState extends ConsumerState<QuickActionConfigurator> {
|
||||||
|
late List<ActionButtonType> _order;
|
||||||
|
late final ScrollController _scrollController;
|
||||||
|
bool _hasLocalChanges = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_order = List<ActionButtonType>.from(ref.read(viewerQuickActionOrderProvider));
|
||||||
|
_scrollController = ScrollController();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onReorder(int oldIndex, int newIndex) {
|
||||||
|
setState(() {
|
||||||
|
final item = _order.removeAt(oldIndex);
|
||||||
|
_order.insert(newIndex, item);
|
||||||
|
_hasLocalChanges = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetToDefault() {
|
||||||
|
setState(() {
|
||||||
|
_order = List<ActionButtonType>.from(ActionButtonBuilder.defaultQuickActionOrder);
|
||||||
|
_hasLocalChanges = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancel() => Navigator.of(context).pop();
|
||||||
|
|
||||||
|
Future<void> _save() async {
|
||||||
|
await ref.read(viewerQuickActionOrderProvider.notifier).setOrder(_order);
|
||||||
|
_hasLocalChanges = false;
|
||||||
|
if (mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
const crossAxisCount = 4;
|
||||||
|
const crossAxisSpacing = 12.0;
|
||||||
|
const mainAxisSpacing = 12.0;
|
||||||
|
const tileHeight = 130.0;
|
||||||
|
final currentOrder = ref.watch(viewerQuickActionOrderProvider);
|
||||||
|
if (!_hasLocalChanges && !listEquals(_order, currentOrder)) {
|
||||||
|
_order = List<ActionButtonType>.from(currentOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasChanges = !listEquals(currentOrder, _order);
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.only(left: 20, right: 20, bottom: MediaQuery.of(context).viewInsets.bottom + 20, top: 16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.max,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.onSurface.withValues(alpha: 0.25),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(2)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('quick_actions_settings_title'.tr(), style: theme.textTheme.titleMedium),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'quick_actions_settings_description'.tr(
|
||||||
|
namedArgs: {'count': ActionButtonBuilder.defaultQuickActionLimit.toString()},
|
||||||
|
),
|
||||||
|
style: theme.textTheme.bodyMedium,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Expanded(
|
||||||
|
child: LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final rows = (_order.length / crossAxisCount).ceil().clamp(1, 4);
|
||||||
|
final naturalHeight = rows * tileHeight + (rows - 1) * mainAxisSpacing;
|
||||||
|
final shouldScroll = naturalHeight > constraints.maxHeight;
|
||||||
|
final horizontalPadding = 8.0;
|
||||||
|
final tileWidth =
|
||||||
|
(constraints.maxWidth - horizontalPadding - (crossAxisSpacing * (crossAxisCount - 1))) /
|
||||||
|
crossAxisCount;
|
||||||
|
final childAspectRatio = tileWidth / tileHeight;
|
||||||
|
final gridController = shouldScroll ? _scrollController : null;
|
||||||
|
|
||||||
|
return ReorderableDragDropGrid(
|
||||||
|
scrollController: gridController,
|
||||||
|
itemCount: _order.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final type = _order[index];
|
||||||
|
return _QuickActionTile(index: index, type: type);
|
||||||
|
},
|
||||||
|
onReorder: _onReorder,
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
crossAxisSpacing: crossAxisSpacing,
|
||||||
|
mainAxisSpacing: mainAxisSpacing,
|
||||||
|
childAspectRatio: childAspectRatio,
|
||||||
|
shouldScroll: shouldScroll,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
TextButton(onPressed: _resetToDefault, child: const Text('reset').tr()),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
TextButton(onPressed: _cancel, child: const Text('cancel').tr()),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
FilledButton(onPressed: hasChanges ? _save : null, child: const Text('done').tr()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickActionTile extends StatelessWidget {
|
||||||
|
final int index;
|
||||||
|
final ActionButtonType type;
|
||||||
|
|
||||||
|
const _QuickActionTile({required this.index, required this.type});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final borderColor = theme.dividerColor;
|
||||||
|
final backgroundColor = theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.2);
|
||||||
|
final indicatorColor = theme.colorScheme.primary;
|
||||||
|
final accentColor = theme.colorScheme.onSurface.withValues(alpha: 0.7);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
border: Border.all(color: borderColor),
|
||||||
|
color: backgroundColor,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: indicatorColor.withValues(alpha: 0.15),
|
||||||
|
borderRadius: const BorderRadius.all(Radius.circular(12)),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'${index + 1}',
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(color: indicatorColor, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
Icon(Icons.drag_indicator_rounded, size: 18, color: indicatorColor),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: Icon(type.iconData, size: 28, color: theme.colorScheme.onSurface),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: Text(
|
||||||
|
type.localizedLabel(context),
|
||||||
|
style: theme.textTheme.labelSmall?.copyWith(color: accentColor),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,26 +4,19 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/events.model.dart';
|
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||||
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/download_action_button.widget.dart';
|
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/favorite_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/motion_photo_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unfavorite_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/asset_viewer/viewer_kebab_menu.widget.dart';
|
||||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||||
import 'package:immich_mobile/providers/cast.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/readonly_mode.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
|
||||||
import 'package:immich_mobile/providers/routes.provider.dart';
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
import 'package:immich_mobile/providers/user.provider.dart';
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
import 'package:immich_mobile/routing/router.dart';
|
|
||||||
|
|
||||||
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
const ViewerTopAppBar({super.key});
|
const ViewerTopAppBar({super.key});
|
||||||
@@ -42,15 +35,6 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
final isInLockedView = ref.watch(inLockedViewProvider);
|
final isInLockedView = ref.watch(inLockedViewProvider);
|
||||||
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
final isReadonlyModeEnabled = ref.watch(readonlyModeProvider);
|
||||||
|
|
||||||
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
|
||||||
final showViewInTimelineButton =
|
|
||||||
timelineOrigin != TimelineOrigin.main &&
|
|
||||||
timelineOrigin != TimelineOrigin.deepLink &&
|
|
||||||
timelineOrigin != TimelineOrigin.trash &&
|
|
||||||
timelineOrigin != TimelineOrigin.archive &&
|
|
||||||
timelineOrigin != TimelineOrigin.localAlbum &&
|
|
||||||
isOwner;
|
|
||||||
|
|
||||||
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
final isShowingSheet = ref.watch(assetViewerProvider.select((state) => state.showingBottomSheet));
|
||||||
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
int opacity = ref.watch(assetViewerProvider.select((state) => state.backgroundOpacity));
|
||||||
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
final showControls = ref.watch(assetViewerProvider.select((s) => s.showingControls));
|
||||||
@@ -63,11 +47,10 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
opacity = 0;
|
opacity = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
final originalTheme = context.themeData;
|
||||||
|
|
||||||
final actions = <Widget>[
|
final actions = <Widget>[
|
||||||
if (asset.isRemoteOnly) const DownloadActionButton(source: ActionSource.viewer, iconOnly: true),
|
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
||||||
if (isCasting || (asset.hasRemote)) const CastActionButton(iconOnly: true),
|
|
||||||
if (album != null && album.isActivityEnabled && album.isShared)
|
if (album != null && album.isActivityEnabled && album.isShared)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.chat_outlined),
|
icon: const Icon(Icons.chat_outlined),
|
||||||
@@ -75,28 +58,16 @@ class ViewerTopAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
|||||||
EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true));
|
EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (showViewInTimelineButton)
|
|
||||||
IconButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await context.maybePop();
|
|
||||||
await context.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
|
|
||||||
EventStream.shared.emit(ScrollToDateEvent(asset.createdAt));
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.image_search),
|
|
||||||
tooltip: 'view_in_timeline'.t(context: context),
|
|
||||||
),
|
|
||||||
if (asset.hasRemote && isOwner && !asset.isFavorite)
|
if (asset.hasRemote && isOwner && !asset.isFavorite)
|
||||||
const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
const FavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||||
if (asset.hasRemote && isOwner && asset.isFavorite)
|
if (asset.hasRemote && isOwner && asset.isFavorite)
|
||||||
const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
const UnFavoriteActionButton(source: ActionSource.viewer, iconOnly: true),
|
||||||
if (asset.isMotionPhoto) const MotionPhotoActionButton(iconOnly: true),
|
|
||||||
const ViewerKebabMenu(),
|
ViewerKebabMenu(originalTheme: originalTheme),
|
||||||
];
|
];
|
||||||
|
|
||||||
final lockedViewActions = <Widget>[
|
final lockedViewActions = <Widget>[ViewerKebabMenu(originalTheme: originalTheme)];
|
||||||
if (isCasting || (asset.hasRemote)) const CastActionButton(iconOnly: true),
|
|
||||||
const ViewerKebabMenu(),
|
|
||||||
];
|
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: opacity < 255,
|
ignoring: opacity < 255,
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
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/domain/models/events.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
import 'package:immich_mobile/providers/cast.provider.dart';
|
||||||
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/infrastructure/timeline.provider.dart';
|
||||||
|
import 'package:immich_mobile/providers/user.provider.dart';
|
||||||
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
|
||||||
class ViewerKebabMenu extends ConsumerWidget {
|
class ViewerKebabMenu extends ConsumerWidget {
|
||||||
const ViewerKebabMenu({super.key});
|
const ViewerKebabMenu({super.key, this.originalTheme});
|
||||||
|
|
||||||
|
final ThemeData? originalTheme;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -17,25 +20,42 @@ class ViewerKebabMenu extends ConsumerWidget {
|
|||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
|
||||||
final menuChildren = <Widget>[
|
final user = ref.watch(currentUserProvider);
|
||||||
BaseActionButton(
|
final isOwner = asset is RemoteAsset && asset.ownerId == user?.id;
|
||||||
label: 'about'.tr(),
|
final isCasting = ref.watch(castProvider.select((c) => c.isCasting));
|
||||||
iconData: Icons.info_outline,
|
final timelineOrigin = ref.read(timelineServiceProvider).origin;
|
||||||
menuItem: true,
|
|
||||||
onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()),
|
final kebabContext = ViewerKebabMenuButtonContext(
|
||||||
),
|
asset: asset,
|
||||||
];
|
isOwner: isOwner,
|
||||||
|
isCasting: isCasting,
|
||||||
|
timelineOrigin: timelineOrigin,
|
||||||
|
originalTheme: originalTheme,
|
||||||
|
);
|
||||||
|
|
||||||
|
final menuChildren = ViewerKebabMenuButtonBuilder.build(kebabContext, context, ref);
|
||||||
|
|
||||||
return MenuAnchor(
|
return MenuAnchor(
|
||||||
consumeOutsideTap: true,
|
consumeOutsideTap: true,
|
||||||
style: MenuStyle(
|
style: MenuStyle(
|
||||||
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
|
backgroundColor: WidgetStatePropertyAll(context.themeData.scaffoldBackgroundColor),
|
||||||
|
surfaceTintColor: const WidgetStatePropertyAll(Colors.grey),
|
||||||
elevation: const WidgetStatePropertyAll(4),
|
elevation: const WidgetStatePropertyAll(4),
|
||||||
shape: const WidgetStatePropertyAll(
|
shape: const WidgetStatePropertyAll(
|
||||||
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(12))),
|
||||||
),
|
),
|
||||||
|
padding: const WidgetStatePropertyAll(EdgeInsets.symmetric(vertical: 6)),
|
||||||
),
|
),
|
||||||
menuChildren: menuChildren,
|
menuChildren: [
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(minWidth: 150),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: menuChildren,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
builder: (context, controller, child) {
|
builder: (context, controller, child) {
|
||||||
return IconButton(
|
return IconButton(
|
||||||
icon: const Icon(Icons.more_vert_rounded),
|
icon: const Icon(Icons.more_vert_rounded),
|
||||||
|
|||||||
@@ -324,7 +324,11 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10;
|
final topPadding = context.padding.top + (widget.appBar == null ? 0 : kToolbarHeight) + 10;
|
||||||
|
|
||||||
const scrubberBottomPadding = 100.0;
|
const scrubberBottomPadding = 100.0;
|
||||||
final bottomPadding = context.padding.bottom + (widget.appBar == null ? 0 : scrubberBottomPadding);
|
const bottomSheetOpenModifier = 120.0;
|
||||||
|
final bottomPadding =
|
||||||
|
context.padding.bottom +
|
||||||
|
(widget.appBar == null ? 0 : scrubberBottomPadding) +
|
||||||
|
(isMultiSelectEnabled ? bottomSheetOpenModifier : 0);
|
||||||
|
|
||||||
final grid = CustomScrollView(
|
final grid = CustomScrollView(
|
||||||
primary: true,
|
primary: true,
|
||||||
@@ -347,7 +351,7 @@ class _SliverTimelineState extends ConsumerState<_SliverTimeline> {
|
|||||||
addRepaintBoundaries: false,
|
addRepaintBoundaries: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SliverPadding(padding: EdgeInsets.only(bottom: scrubberBottomPadding)),
|
SliverPadding(padding: EdgeInsets.only(bottom: bottomPadding)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/quick_action.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||||
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
|
||||||
|
final actionButtonOrderRepositoryProvider = Provider<ActionButtonOrderRepository>(
|
||||||
|
(ref) => const ActionButtonOrderRepository(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final quickActionServiceProvider = Provider<QuickActionService>(
|
||||||
|
(ref) => QuickActionService(ref.watch(actionButtonOrderRepositoryProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
final viewerQuickActionOrderProvider = StateNotifierProvider<ViewerQuickActionOrderNotifier, List<ActionButtonType>>(
|
||||||
|
(ref) => ViewerQuickActionOrderNotifier(ref.watch(quickActionServiceProvider)),
|
||||||
|
);
|
||||||
|
|
||||||
|
class ViewerQuickActionOrderNotifier extends StateNotifier<List<ActionButtonType>> {
|
||||||
|
final QuickActionService _service;
|
||||||
|
StreamSubscription<List<ActionButtonType>>? _subscription;
|
||||||
|
|
||||||
|
ViewerQuickActionOrderNotifier(this._service) : super(_service.get()) {
|
||||||
|
_subscription = _service.watch().listen((order) {
|
||||||
|
state = order;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_subscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setOrder(List<ActionButtonType> order) async {
|
||||||
|
if (listEquals(state, order)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final previous = state;
|
||||||
|
state = order;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _service.set(order);
|
||||||
|
} catch (error) {
|
||||||
|
state = previous;
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -65,6 +65,7 @@ enum AppSettingsEnum<T> {
|
|||||||
|
|
||||||
class AppSettingsService {
|
class AppSettingsService {
|
||||||
const AppSettingsService();
|
const AppSettingsService();
|
||||||
|
|
||||||
T getSetting<T>(AppSettingsEnum<T> setting) {
|
T getSetting<T>(AppSettingsEnum<T> setting) {
|
||||||
return Store.get(setting.storeKey, setting.defaultValue);
|
return Store.get(setting.storeKey, setting.defaultValue);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,24 @@
|
|||||||
import 'package:flutter/widgets.dart';
|
import 'package:auto_route/auto_route.dart';
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/add_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/events.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/timeline.service.dart';
|
||||||
|
import 'package:immich_mobile/domain/utils/event_stream.dart';
|
||||||
|
import 'package:immich_mobile/extensions/translate_extensions.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/advanced_info_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/cast_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_local_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/delete_permanent_action_button.widget.dart';
|
||||||
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_image_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/like_activity_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/move_to_lock_folder_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/remove_from_album_action_button.widget.dart';
|
||||||
@@ -19,6 +30,8 @@ import 'package:immich_mobile/presentation/widgets/action_buttons/trash_action_b
|
|||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unarchive_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/unstack_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/action_buttons/upload_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/reorder_buttons_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
|
|
||||||
class ActionButtonContext {
|
class ActionButtonContext {
|
||||||
final BaseAsset asset;
|
final BaseAsset asset;
|
||||||
@@ -47,6 +60,8 @@ class ActionButtonContext {
|
|||||||
enum ActionButtonType {
|
enum ActionButtonType {
|
||||||
advancedInfo,
|
advancedInfo,
|
||||||
share,
|
share,
|
||||||
|
edit,
|
||||||
|
add,
|
||||||
shareLink,
|
shareLink,
|
||||||
similarPhotos,
|
similarPhotos,
|
||||||
archive,
|
archive,
|
||||||
@@ -63,10 +78,16 @@ enum ActionButtonType {
|
|||||||
unstack,
|
unstack,
|
||||||
likeActivity;
|
likeActivity;
|
||||||
|
|
||||||
|
String toJson() => name;
|
||||||
|
|
||||||
bool shouldShow(ActionButtonContext context) {
|
bool shouldShow(ActionButtonContext context) {
|
||||||
return switch (this) {
|
return switch (this) {
|
||||||
ActionButtonType.advancedInfo => context.advancedTroubleshooting,
|
ActionButtonType.advancedInfo => context.advancedTroubleshooting,
|
||||||
ActionButtonType.share => true,
|
ActionButtonType.share => true,
|
||||||
|
ActionButtonType.edit =>
|
||||||
|
!context.isInLockedView && //
|
||||||
|
context.asset.isImage,
|
||||||
|
ActionButtonType.add => context.asset.hasRemote,
|
||||||
ActionButtonType.shareLink =>
|
ActionButtonType.shareLink =>
|
||||||
!context.isInLockedView && //
|
!context.isInLockedView && //
|
||||||
context.asset.hasRemote,
|
context.asset.hasRemote,
|
||||||
@@ -135,6 +156,8 @@ enum ActionButtonType {
|
|||||||
return switch (this) {
|
return switch (this) {
|
||||||
ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source),
|
ActionButtonType.advancedInfo => AdvancedInfoActionButton(source: context.source),
|
||||||
ActionButtonType.share => ShareActionButton(source: context.source),
|
ActionButtonType.share => ShareActionButton(source: context.source),
|
||||||
|
ActionButtonType.edit => const EditImageActionButton(),
|
||||||
|
ActionButtonType.add => const AddActionButton(),
|
||||||
ActionButtonType.shareLink => ShareLinkActionButton(source: context.source),
|
ActionButtonType.shareLink => ShareLinkActionButton(source: context.source),
|
||||||
ActionButtonType.archive => ArchiveActionButton(source: context.source),
|
ActionButtonType.archive => ArchiveActionButton(source: context.source),
|
||||||
ActionButtonType.unarchive => UnArchiveActionButton(source: context.source),
|
ActionButtonType.unarchive => UnArchiveActionButton(source: context.source),
|
||||||
@@ -160,7 +183,119 @@ enum ActionButtonType {
|
|||||||
class ActionButtonBuilder {
|
class ActionButtonBuilder {
|
||||||
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
|
static const List<ActionButtonType> _actionTypes = ActionButtonType.values;
|
||||||
|
|
||||||
|
static const int defaultQuickActionLimit = 4;
|
||||||
|
|
||||||
|
static const List<ActionButtonType> defaultQuickActionOrder = [
|
||||||
|
ActionButtonType.share,
|
||||||
|
ActionButtonType.upload,
|
||||||
|
ActionButtonType.edit,
|
||||||
|
ActionButtonType.add,
|
||||||
|
ActionButtonType.archive,
|
||||||
|
ActionButtonType.delete,
|
||||||
|
ActionButtonType.removeFromAlbum,
|
||||||
|
ActionButtonType.likeActivity,
|
||||||
|
];
|
||||||
|
|
||||||
static List<Widget> build(ActionButtonContext context) {
|
static List<Widget> build(ActionButtonContext context) {
|
||||||
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
return _actionTypes.where((type) => type.shouldShow(context)).map((type) => type.buildButton(context)).toList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ViewerKebabMenuButtonContext {
|
||||||
|
final BaseAsset asset;
|
||||||
|
final bool isOwner;
|
||||||
|
final bool isCasting;
|
||||||
|
final TimelineOrigin timelineOrigin;
|
||||||
|
final ThemeData? originalTheme;
|
||||||
|
|
||||||
|
const ViewerKebabMenuButtonContext({
|
||||||
|
required this.asset,
|
||||||
|
required this.isOwner,
|
||||||
|
required this.isCasting,
|
||||||
|
required this.timelineOrigin,
|
||||||
|
this.originalTheme,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ViewerKebabMenuButtonType {
|
||||||
|
openInfo,
|
||||||
|
viewInTimeline,
|
||||||
|
reorderButtons,
|
||||||
|
cast,
|
||||||
|
download;
|
||||||
|
|
||||||
|
/// Defines which group each button belongs to.
|
||||||
|
/// Buttons in the same group will be displayed together,
|
||||||
|
/// with dividers separating different groups.
|
||||||
|
int get group => switch (this) {
|
||||||
|
ViewerKebabMenuButtonType.openInfo => 0,
|
||||||
|
ViewerKebabMenuButtonType.viewInTimeline => 1,
|
||||||
|
ViewerKebabMenuButtonType.reorderButtons => 1,
|
||||||
|
ViewerKebabMenuButtonType.cast => 1,
|
||||||
|
ViewerKebabMenuButtonType.download => 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
bool shouldShow(ViewerKebabMenuButtonContext context) {
|
||||||
|
return switch (this) {
|
||||||
|
ViewerKebabMenuButtonType.openInfo => true,
|
||||||
|
ViewerKebabMenuButtonType.viewInTimeline =>
|
||||||
|
context.timelineOrigin != TimelineOrigin.main &&
|
||||||
|
context.timelineOrigin != TimelineOrigin.deepLink &&
|
||||||
|
context.timelineOrigin != TimelineOrigin.trash &&
|
||||||
|
context.timelineOrigin != TimelineOrigin.archive &&
|
||||||
|
context.timelineOrigin != TimelineOrigin.localAlbum &&
|
||||||
|
context.isOwner,
|
||||||
|
ViewerKebabMenuButtonType.cast => context.isCasting || context.asset.hasRemote,
|
||||||
|
ViewerKebabMenuButtonType.download => context.asset.isRemoteOnly,
|
||||||
|
ViewerKebabMenuButtonType.reorderButtons => true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsumerWidget buildButton(ViewerKebabMenuButtonContext context, BuildContext buildContext) {
|
||||||
|
return switch (this) {
|
||||||
|
ViewerKebabMenuButtonType.openInfo => BaseActionButton(
|
||||||
|
label: 'info'.tr(),
|
||||||
|
iconData: Icons.info_outline,
|
||||||
|
iconColor: context.originalTheme?.iconTheme.color,
|
||||||
|
menuItem: true,
|
||||||
|
onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent()),
|
||||||
|
),
|
||||||
|
|
||||||
|
ViewerKebabMenuButtonType.viewInTimeline => BaseActionButton(
|
||||||
|
label: 'view_in_timeline'.t(context: buildContext),
|
||||||
|
iconData: Icons.image_search,
|
||||||
|
iconColor: context.originalTheme?.iconTheme.color,
|
||||||
|
menuItem: true,
|
||||||
|
onPressed: () async {
|
||||||
|
await buildContext.maybePop();
|
||||||
|
await buildContext.navigateTo(const TabShellRoute(children: [MainTimelineRoute()]));
|
||||||
|
EventStream.shared.emit(ScrollToDateEvent(context.asset.createdAt));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ViewerKebabMenuButtonType.cast => const CastActionButton(menuItem: true),
|
||||||
|
ViewerKebabMenuButtonType.download => const DownloadActionButton(source: ActionSource.viewer, menuItem: true),
|
||||||
|
ViewerKebabMenuButtonType.reorderButtons => ReorderButtonsActionButton(originalTheme: context.originalTheme),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewerKebabMenuButtonBuilder {
|
||||||
|
static List<Widget> build(ViewerKebabMenuButtonContext context, BuildContext buildContext, WidgetRef ref) {
|
||||||
|
final visibleButtons = ViewerKebabMenuButtonType.values.where((type) => type.shouldShow(context)).toList();
|
||||||
|
|
||||||
|
if (visibleButtons.isEmpty) return [];
|
||||||
|
|
||||||
|
final List<Widget> result = [];
|
||||||
|
int? lastGroup;
|
||||||
|
|
||||||
|
for (final type in visibleButtons) {
|
||||||
|
if (lastGroup != null && type.group != lastGroup) {
|
||||||
|
result.add(const Divider(height: 1));
|
||||||
|
}
|
||||||
|
result.add(type.buildButton(context, buildContext).build(buildContext, ref));
|
||||||
|
lastGroup = type.group;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
55
mobile/lib/utils/action_button_visuals.dart
Normal file
55
mobile/lib/utils/action_button_visuals.dart
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
|
||||||
|
extension ActionButtonTypeVisuals on ActionButtonType {
|
||||||
|
IconData get iconData {
|
||||||
|
return switch (this) {
|
||||||
|
ActionButtonType.advancedInfo => Icons.help_outline_rounded,
|
||||||
|
ActionButtonType.share => Icons.share_rounded,
|
||||||
|
ActionButtonType.edit => Icons.tune,
|
||||||
|
ActionButtonType.add => Icons.add,
|
||||||
|
ActionButtonType.shareLink => Icons.link_rounded,
|
||||||
|
ActionButtonType.similarPhotos => Icons.compare,
|
||||||
|
ActionButtonType.archive => Icons.archive_outlined,
|
||||||
|
ActionButtonType.unarchive => Icons.unarchive_outlined,
|
||||||
|
ActionButtonType.download => Icons.download,
|
||||||
|
ActionButtonType.trash => Icons.delete_outline_rounded,
|
||||||
|
ActionButtonType.deletePermanent => Icons.delete_forever,
|
||||||
|
ActionButtonType.delete => Icons.delete_sweep_outlined,
|
||||||
|
ActionButtonType.moveToLockFolder => Icons.lock_outline_rounded,
|
||||||
|
ActionButtonType.removeFromLockFolder => Icons.lock_open_rounded,
|
||||||
|
ActionButtonType.deleteLocal => Icons.no_cell_outlined,
|
||||||
|
ActionButtonType.upload => Icons.backup_outlined,
|
||||||
|
ActionButtonType.removeFromAlbum => Icons.remove_circle_outline,
|
||||||
|
ActionButtonType.unstack => Icons.layers_clear_outlined,
|
||||||
|
ActionButtonType.likeActivity => Icons.favorite_border,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String get _labelKey {
|
||||||
|
return switch (this) {
|
||||||
|
ActionButtonType.advancedInfo => 'troubleshoot',
|
||||||
|
ActionButtonType.share => 'share',
|
||||||
|
ActionButtonType.edit => 'edit',
|
||||||
|
ActionButtonType.add => 'add_to_bottom_bar',
|
||||||
|
ActionButtonType.shareLink => 'share_link',
|
||||||
|
ActionButtonType.similarPhotos => 'view_similar_photos',
|
||||||
|
ActionButtonType.archive => 'to_archive',
|
||||||
|
ActionButtonType.unarchive => 'unarchive',
|
||||||
|
ActionButtonType.download => 'download',
|
||||||
|
ActionButtonType.trash => 'control_bottom_app_bar_trash_from_immich',
|
||||||
|
ActionButtonType.deletePermanent => 'delete_permanently',
|
||||||
|
ActionButtonType.delete => 'delete',
|
||||||
|
ActionButtonType.moveToLockFolder => 'move_to_locked_folder',
|
||||||
|
ActionButtonType.removeFromLockFolder => 'remove_from_locked_folder',
|
||||||
|
ActionButtonType.deleteLocal => 'control_bottom_app_bar_delete_from_local',
|
||||||
|
ActionButtonType.upload => 'upload',
|
||||||
|
ActionButtonType.removeFromAlbum => 'remove_from_album',
|
||||||
|
ActionButtonType.unstack => 'unstack',
|
||||||
|
ActionButtonType.likeActivity => 'like',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
String localizedLabel(BuildContext _) => _labelKey.tr();
|
||||||
|
}
|
||||||
288
mobile/lib/widgets/common/reorderable_drag_drop_grid.dart
Normal file
288
mobile/lib/widgets/common/reorderable_drag_drop_grid.dart
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A callback that is called when items are reordered.
|
||||||
|
/// [oldIndex] is the original index of the item being moved.
|
||||||
|
/// [newIndex] is the target index where the item should be moved to.
|
||||||
|
typedef ReorderCallback = void Function(int oldIndex, int newIndex);
|
||||||
|
|
||||||
|
/// A callback that is called during drag to update hover state.
|
||||||
|
/// [draggedIndex] is the index of the item being dragged.
|
||||||
|
/// [targetIndex] is the index of the item being hovered over.
|
||||||
|
typedef DragUpdateCallback = void Function(int draggedIndex, int targetIndex);
|
||||||
|
|
||||||
|
/// A reorderable grid that supports drag and drop reordering with smooth animations.
|
||||||
|
///
|
||||||
|
/// This widget provides a drag-and-drop interface for reordering items in a grid layout.
|
||||||
|
/// Items can be dragged to new positions, and the grid will animate smoothly to reflect
|
||||||
|
/// the new order.
|
||||||
|
///
|
||||||
|
/// Features:
|
||||||
|
/// - Smooth animations during drag and drop
|
||||||
|
/// - Instant snap animation on drop completion
|
||||||
|
/// - Visual feedback during dragging
|
||||||
|
/// - Customizable grid layout parameters
|
||||||
|
class ReorderableDragDropGrid extends StatefulWidget {
|
||||||
|
/// Controller for scrolling the grid.
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
|
/// The number of items to display.
|
||||||
|
final int itemCount;
|
||||||
|
|
||||||
|
/// Builder function to create each grid item.
|
||||||
|
final Widget Function(BuildContext context, int index) itemBuilder;
|
||||||
|
|
||||||
|
/// Callback when items are reordered.
|
||||||
|
final ReorderCallback onReorder;
|
||||||
|
|
||||||
|
/// Number of columns in the grid.
|
||||||
|
final int crossAxisCount;
|
||||||
|
|
||||||
|
/// Horizontal spacing between grid items.
|
||||||
|
final double crossAxisSpacing;
|
||||||
|
|
||||||
|
/// Vertical spacing between grid items.
|
||||||
|
final double mainAxisSpacing;
|
||||||
|
|
||||||
|
/// The ratio of width to height for each grid item.
|
||||||
|
final double childAspectRatio;
|
||||||
|
|
||||||
|
/// Whether the grid should be scrollable.
|
||||||
|
final bool shouldScroll;
|
||||||
|
|
||||||
|
/// Scale factor for the dragged item feedback widget.
|
||||||
|
final double feedbackScaleFactor;
|
||||||
|
|
||||||
|
/// Opacity for the dragged item feedback widget.
|
||||||
|
final double feedbackOpacity;
|
||||||
|
|
||||||
|
const ReorderableDragDropGrid({
|
||||||
|
super.key,
|
||||||
|
this.scrollController,
|
||||||
|
required this.itemCount,
|
||||||
|
required this.itemBuilder,
|
||||||
|
required this.onReorder,
|
||||||
|
required this.crossAxisCount,
|
||||||
|
required this.crossAxisSpacing,
|
||||||
|
required this.mainAxisSpacing,
|
||||||
|
required this.childAspectRatio,
|
||||||
|
this.shouldScroll = true,
|
||||||
|
this.feedbackScaleFactor = 1.05,
|
||||||
|
this.feedbackOpacity = 0.9,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ReorderableDragDropGrid> createState() => _ReorderableDragDropGridState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReorderableDragDropGridState extends State<ReorderableDragDropGrid> {
|
||||||
|
int? _draggingIndex;
|
||||||
|
late List<int> _itemOrder;
|
||||||
|
int? _lastHoveredIndex;
|
||||||
|
bool _snapNow = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_itemOrder = List.generate(widget.itemCount, (index) => index);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(ReorderableDragDropGrid oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (oldWidget.itemCount != widget.itemCount) {
|
||||||
|
_itemOrder = List.generate(widget.itemCount, (index) => index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _updateHover(int draggedIndex, int targetIndex) {
|
||||||
|
if (draggedIndex == targetIndex || _draggingIndex == null) return;
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_lastHoveredIndex = targetIndex;
|
||||||
|
final newOrder = List<int>.from(_itemOrder);
|
||||||
|
final draggedOrderIndex = newOrder.indexOf(draggedIndex);
|
||||||
|
final targetOrderIndex = newOrder.indexOf(targetIndex);
|
||||||
|
|
||||||
|
newOrder.removeAt(draggedOrderIndex);
|
||||||
|
newOrder.insert(targetOrderIndex, draggedIndex);
|
||||||
|
_itemOrder = newOrder;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleDragEnd(int draggedIndex, int? targetIndex) {
|
||||||
|
final effectiveTargetIndex =
|
||||||
|
targetIndex ??
|
||||||
|
(() {
|
||||||
|
final currentVisualIndex = _itemOrder.indexOf(draggedIndex);
|
||||||
|
if (currentVisualIndex != draggedIndex) {
|
||||||
|
return _itemOrder[currentVisualIndex];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (effectiveTargetIndex != null && draggedIndex != effectiveTargetIndex) {
|
||||||
|
widget.onReorder(draggedIndex, effectiveTargetIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
_armSnapNow();
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_draggingIndex = null;
|
||||||
|
_lastHoveredIndex = null;
|
||||||
|
_itemOrder = List.generate(widget.itemCount, (i) => i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _armSnapNow() {
|
||||||
|
setState(() => _snapNow = true);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() => _snapNow = false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final tileWidth =
|
||||||
|
(constraints.maxWidth - (widget.crossAxisSpacing * (widget.crossAxisCount - 1))) / widget.crossAxisCount;
|
||||||
|
final tileHeight = tileWidth / widget.childAspectRatio;
|
||||||
|
final rows = (_itemOrder.length / widget.crossAxisCount).ceil();
|
||||||
|
final totalHeight = rows * tileHeight + (rows - 1) * widget.mainAxisSpacing;
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
controller: widget.scrollController,
|
||||||
|
physics: widget.shouldScroll ? const BouncingScrollPhysics() : const NeverScrollableScrollPhysics(),
|
||||||
|
child: SizedBox(
|
||||||
|
width: constraints.maxWidth,
|
||||||
|
height: totalHeight,
|
||||||
|
child: Stack(
|
||||||
|
children: List.generate(widget.itemCount, (index) {
|
||||||
|
final visualIndex = _itemOrder.indexOf(index);
|
||||||
|
final isDragging = _draggingIndex == index;
|
||||||
|
|
||||||
|
final row = visualIndex ~/ widget.crossAxisCount;
|
||||||
|
final col = visualIndex % widget.crossAxisCount;
|
||||||
|
final left = col * (tileWidth + widget.crossAxisSpacing);
|
||||||
|
final top = row * (tileHeight + widget.mainAxisSpacing);
|
||||||
|
|
||||||
|
return _AnimatedGridItem(
|
||||||
|
key: ValueKey(index),
|
||||||
|
index: index,
|
||||||
|
isDragging: isDragging,
|
||||||
|
snapNow: _snapNow,
|
||||||
|
tileWidth: tileWidth,
|
||||||
|
tileHeight: tileHeight,
|
||||||
|
left: left,
|
||||||
|
top: top,
|
||||||
|
feedbackScaleFactor: widget.feedbackScaleFactor,
|
||||||
|
feedbackOpacity: widget.feedbackOpacity,
|
||||||
|
onDragStarted: () {
|
||||||
|
setState(() {
|
||||||
|
_draggingIndex = index;
|
||||||
|
_lastHoveredIndex = index;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onDragUpdate: (draggedIndex, targetIndex) {
|
||||||
|
_updateHover(draggedIndex, targetIndex);
|
||||||
|
},
|
||||||
|
onDragCompleted: (draggedIndex) {
|
||||||
|
_handleDragEnd(draggedIndex, _lastHoveredIndex);
|
||||||
|
},
|
||||||
|
child: widget.itemBuilder(context, index),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AnimatedGridItem extends StatelessWidget {
|
||||||
|
final int index;
|
||||||
|
final bool isDragging;
|
||||||
|
final bool snapNow;
|
||||||
|
final double tileWidth;
|
||||||
|
final double tileHeight;
|
||||||
|
final double left;
|
||||||
|
final double top;
|
||||||
|
final double feedbackScaleFactor;
|
||||||
|
final double feedbackOpacity;
|
||||||
|
final VoidCallback onDragStarted;
|
||||||
|
final DragUpdateCallback onDragUpdate;
|
||||||
|
final Function(int draggedIndex) onDragCompleted;
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _AnimatedGridItem({
|
||||||
|
super.key,
|
||||||
|
required this.index,
|
||||||
|
required this.isDragging,
|
||||||
|
required this.snapNow,
|
||||||
|
required this.tileWidth,
|
||||||
|
required this.tileHeight,
|
||||||
|
required this.left,
|
||||||
|
required this.top,
|
||||||
|
required this.feedbackScaleFactor,
|
||||||
|
required this.feedbackOpacity,
|
||||||
|
required this.onDragStarted,
|
||||||
|
required this.onDragUpdate,
|
||||||
|
required this.onDragCompleted,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Duration animDuration = snapNow ? Duration.zero : const Duration(milliseconds: 150);
|
||||||
|
|
||||||
|
return AnimatedPositioned(
|
||||||
|
duration: animDuration,
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
left: left,
|
||||||
|
top: top,
|
||||||
|
width: tileWidth,
|
||||||
|
height: tileHeight,
|
||||||
|
child: DragTarget<int>(
|
||||||
|
onWillAcceptWithDetails: (details) {
|
||||||
|
if (details.data != index) {
|
||||||
|
onDragUpdate(details.data, index);
|
||||||
|
}
|
||||||
|
return details.data != index;
|
||||||
|
},
|
||||||
|
builder: (context, candidateData, rejectedData) {
|
||||||
|
Widget displayChild = child;
|
||||||
|
|
||||||
|
if (isDragging) {
|
||||||
|
displayChild = Opacity(opacity: 0.0, child: child);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Draggable<int>(
|
||||||
|
data: index,
|
||||||
|
feedback: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: SizedBox(
|
||||||
|
width: tileWidth,
|
||||||
|
height: tileHeight,
|
||||||
|
child: Opacity(
|
||||||
|
opacity: feedbackOpacity,
|
||||||
|
child: Transform.scale(scale: feedbackScaleFactor, child: child),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
childWhenDragging: const SizedBox.shrink(),
|
||||||
|
onDragStarted: onDragStarted,
|
||||||
|
onDragCompleted: () {
|
||||||
|
onDragCompleted(index);
|
||||||
|
},
|
||||||
|
onDraggableCanceled: (_, __) {
|
||||||
|
onDragCompleted(index);
|
||||||
|
},
|
||||||
|
child: displayChild,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,26 +2,27 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||||
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
import 'package:immich_mobile/extensions/theme_extensions.dart';
|
||||||
|
|
||||||
class EntitiyCountTile extends StatelessWidget {
|
class EntityCountTile extends StatelessWidget {
|
||||||
final int count;
|
final int count;
|
||||||
final String label;
|
final String label;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
|
|
||||||
const EntitiyCountTile({super.key, required this.count, required this.label, required this.icon});
|
const EntityCountTile({super.key, required this.count, required this.label, required this.icon});
|
||||||
|
|
||||||
String zeroPadding(int number, int targetWidth) {
|
String zeroPadding(int number, int targetWidth) {
|
||||||
final numStr = number.toString();
|
final numStr = number.toString();
|
||||||
return numStr.length < targetWidth ? "0" * (targetWidth - numStr.length) : "";
|
return numStr.length < targetWidth ? "0" * (targetWidth - numStr.length) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
int calculateMaxDigits(double availableWidth) {
|
|
||||||
const double charWidth = 11.0;
|
|
||||||
return (availableWidth / charWidth).floor().clamp(1, 8);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
final availableWidth = (screenWidth - 32 - 8) / 2;
|
||||||
|
const double charWidth = 11.0;
|
||||||
|
final maxDigits = ((availableWidth - 32) / charWidth).floor().clamp(1, 8);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
height: double.infinity,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: context.colorScheme.surfaceContainerLow,
|
color: context.colorScheme.surfaceContainerLow,
|
||||||
@@ -29,7 +30,6 @@ class EntitiyCountTile extends StatelessWidget {
|
|||||||
border: Border.all(width: 0.5, color: context.colorScheme.outline.withAlpha(25)),
|
border: Border.all(width: 0.5, color: context.colorScheme.outline.withAlpha(25)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Icon and Label
|
// Icon and Label
|
||||||
@@ -38,18 +38,17 @@ class EntitiyCountTile extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(icon, color: context.primaryColor),
|
Icon(icon, color: context.primaryColor),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Flexible(
|
||||||
|
child: Text(
|
||||||
label,
|
label,
|
||||||
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
|
style: TextStyle(color: context.primaryColor, fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
|
||||||
// Number
|
// Number
|
||||||
LayoutBuilder(
|
const Spacer(),
|
||||||
builder: (context, constraints) {
|
RichText(
|
||||||
final maxDigits = calculateMaxDigits(constraints.maxWidth);
|
|
||||||
return RichText(
|
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
|
style: const TextStyle(fontSize: 18, fontFamily: 'OverpassMono', fontWeight: FontWeight.w600),
|
||||||
children: [
|
children: [
|
||||||
@@ -63,8 +62,6 @@ class EntitiyCountTile extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -282,20 +282,24 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
_SectionHeaderText(text: "assets".t(context: context)),
|
_SectionHeaderText(text: "assets".t(context: context)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
// 1. Wrap in IntrinsicHeight
|
||||||
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
// 2. Stretch children vertically to fill the IntrinsicHeight
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "local".t(context: context),
|
label: "local".t(context: context),
|
||||||
count: localAssetCount,
|
count: localAssetCount,
|
||||||
icon: Icons.smartphone,
|
icon: Icons.smartphone,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "remote".t(context: context),
|
label: "remote".t(context: context),
|
||||||
count: remoteAssetCount,
|
count: remoteAssetCount,
|
||||||
icon: Icons.cloud,
|
icon: Icons.cloud,
|
||||||
@@ -304,23 +308,26 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
_SectionHeaderText(text: "albums".t(context: context)),
|
_SectionHeaderText(text: "albums".t(context: context)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "local".t(context: context),
|
label: "local".t(context: context),
|
||||||
count: localAlbumCount,
|
count: localAlbumCount,
|
||||||
icon: Icons.smartphone,
|
icon: Icons.smartphone,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "remote".t(context: context),
|
label: "remote".t(context: context),
|
||||||
count: remoteAlbumCount,
|
count: remoteAlbumCount,
|
||||||
icon: Icons.cloud,
|
icon: Icons.cloud,
|
||||||
@@ -329,23 +336,26 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
_SectionHeaderText(text: "other".t(context: context)),
|
_SectionHeaderText(text: "other".t(context: context)),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "memories".t(context: context),
|
label: "memories".t(context: context),
|
||||||
count: memoryCount,
|
count: memoryCount,
|
||||||
icon: Icons.calendar_today,
|
icon: Icons.calendar_today,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "hashed_assets".t(context: context),
|
label: "hashed_assets".t(context: context),
|
||||||
count: localHashedCount,
|
count: localHashedCount,
|
||||||
icon: Icons.tag,
|
icon: Icons.tag,
|
||||||
@@ -354,6 +364,7 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
// To be removed once the experimental feature is stable
|
// To be removed once the experimental feature is stable
|
||||||
if (CurrentPlatform.isAndroid &&
|
if (CurrentPlatform.isAndroid &&
|
||||||
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
|
appSettingsService.getSetting<bool>(AppSettingsEnum.manageLocalMediaAndroid)) ...[
|
||||||
@@ -364,20 +375,22 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
return counts.when(
|
return counts.when(
|
||||||
data: (c) => Padding(
|
data: (c) => Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
|
child: IntrinsicHeight(
|
||||||
child: Flex(
|
child: Flex(
|
||||||
direction: Axis.horizontal,
|
direction: Axis.horizontal,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch, // Added
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "local".t(context: context),
|
label: "local".t(context: context),
|
||||||
count: c.total,
|
count: c.total,
|
||||||
icon: Icons.delete_outline,
|
icon: Icons.delete_outline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: EntitiyCountTile(
|
child: EntityCountTile(
|
||||||
label: "hashed_assets".t(context: context),
|
label: "hashed_assets".t(context: context),
|
||||||
count: c.hashed,
|
count: c.hashed,
|
||||||
icon: Icons.tag,
|
icon: Icons.tag,
|
||||||
@@ -386,6 +399,7 @@ class _SyncStatsCounts extends ConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
loading: () => const CircularProgressIndicator(),
|
loading: () => const CircularProgressIndicator(),
|
||||||
error: (e, st) => Text('Error: $e'),
|
error: (e, st) => Text('Error: $e'),
|
||||||
);
|
);
|
||||||
|
|||||||
150
mobile/test/domain/services/quick_action_service_test.dart
Normal file
150
mobile/test/domain/services/quick_action_service_test.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/quick_action.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||||
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('QuickActionService', () {
|
||||||
|
late QuickActionService service;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
// Use repository with default behavior for testing
|
||||||
|
service = const QuickActionService(ActionButtonOrderRepository());
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildQuickActionTypes should respect custom order', () {
|
||||||
|
final remoteAsset = RemoteAsset(
|
||||||
|
id: 'test-id',
|
||||||
|
name: 'test.jpg',
|
||||||
|
checksum: 'checksum',
|
||||||
|
type: AssetType.image,
|
||||||
|
ownerId: 'owner-id',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final context = ActionButtonContext(
|
||||||
|
asset: remoteAsset,
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: false,
|
||||||
|
isTrashEnabled: true,
|
||||||
|
isInLockedView: false,
|
||||||
|
currentAlbum: null,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: false,
|
||||||
|
source: ActionSource.viewer,
|
||||||
|
);
|
||||||
|
|
||||||
|
final customOrder = [
|
||||||
|
ActionButtonType.archive,
|
||||||
|
ActionButtonType.share,
|
||||||
|
ActionButtonType.edit,
|
||||||
|
ActionButtonType.delete,
|
||||||
|
];
|
||||||
|
|
||||||
|
final types = service.buildQuickActionTypes(context, quickActionOrder: customOrder);
|
||||||
|
|
||||||
|
expect(types.length, lessThanOrEqualTo(ActionButtonBuilder.defaultQuickActionLimit));
|
||||||
|
expect(types.first, ActionButtonType.archive);
|
||||||
|
expect(types[1], ActionButtonType.share);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildQuickActionTypes should resolve archive to unarchive when archived', () {
|
||||||
|
final remoteAsset = RemoteAsset(
|
||||||
|
id: 'test-id',
|
||||||
|
name: 'test.jpg',
|
||||||
|
checksum: 'checksum',
|
||||||
|
type: AssetType.image,
|
||||||
|
ownerId: 'owner-id',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final context = ActionButtonContext(
|
||||||
|
asset: remoteAsset,
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: true, // archived
|
||||||
|
isTrashEnabled: true,
|
||||||
|
isInLockedView: false,
|
||||||
|
currentAlbum: null,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: false,
|
||||||
|
source: ActionSource.viewer,
|
||||||
|
);
|
||||||
|
|
||||||
|
final customOrder = [ActionButtonType.archive];
|
||||||
|
|
||||||
|
final types = service.buildQuickActionTypes(context, quickActionOrder: customOrder);
|
||||||
|
|
||||||
|
expect(types.contains(ActionButtonType.unarchive), isTrue);
|
||||||
|
expect(types.contains(ActionButtonType.archive), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildQuickActionTypes should filter types that shouldShow returns false', () {
|
||||||
|
final localAsset = LocalAsset(
|
||||||
|
id: 'local-id',
|
||||||
|
name: 'test.jpg',
|
||||||
|
checksum: 'checksum',
|
||||||
|
type: AssetType.image,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final context = ActionButtonContext(
|
||||||
|
asset: localAsset,
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: false,
|
||||||
|
isTrashEnabled: true,
|
||||||
|
isInLockedView: false,
|
||||||
|
currentAlbum: null,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: false,
|
||||||
|
source: ActionSource.viewer,
|
||||||
|
);
|
||||||
|
|
||||||
|
final customOrder = [
|
||||||
|
ActionButtonType.archive, // should not show for local-only asset
|
||||||
|
ActionButtonType.share,
|
||||||
|
];
|
||||||
|
|
||||||
|
final types = service.buildQuickActionTypes(context, quickActionOrder: customOrder);
|
||||||
|
|
||||||
|
expect(types.contains(ActionButtonType.archive), isFalse);
|
||||||
|
expect(types.contains(ActionButtonType.share), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('buildQuickActionTypes should respect limit', () {
|
||||||
|
final remoteAsset = RemoteAsset(
|
||||||
|
id: 'test-id',
|
||||||
|
name: 'test.jpg',
|
||||||
|
checksum: 'checksum',
|
||||||
|
type: AssetType.image,
|
||||||
|
ownerId: 'owner-id',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final context = ActionButtonContext(
|
||||||
|
asset: remoteAsset,
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: false,
|
||||||
|
isTrashEnabled: true,
|
||||||
|
isInLockedView: false,
|
||||||
|
currentAlbum: null,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: false,
|
||||||
|
source: ActionSource.viewer,
|
||||||
|
);
|
||||||
|
|
||||||
|
final types = service.buildQuickActionTypes(
|
||||||
|
context,
|
||||||
|
quickActionOrder: ActionButtonBuilder.defaultQuickActionOrder,
|
||||||
|
limit: 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(types.length, 2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -3,6 +3,11 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:immich_mobile/constants/enums.dart';
|
import 'package:immich_mobile/constants/enums.dart';
|
||||||
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
import 'package:immich_mobile/domain/models/album/album.model.dart';
|
||||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||||
|
import 'package:immich_mobile/domain/services/quick_action.service.dart';
|
||||||
|
import 'package:immich_mobile/infrastructure/repositories/action_button_order.repository.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/archive_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/edit_image_action_button.widget.dart';
|
||||||
|
import 'package:immich_mobile/presentation/widgets/action_buttons/share_action_button.widget.dart';
|
||||||
import 'package:immich_mobile/utils/action_button.utils.dart';
|
import 'package:immich_mobile/utils/action_button.utils.dart';
|
||||||
|
|
||||||
LocalAsset createLocalAsset({
|
LocalAsset createLocalAsset({
|
||||||
@@ -137,6 +142,56 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('edit button', () {
|
||||||
|
test('should show for images when not in locked view', () {
|
||||||
|
final context = ActionButtonContext(
|
||||||
|
asset: createRemoteAsset(type: AssetType.image),
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: false,
|
||||||
|
isTrashEnabled: true,
|
||||||
|
isInLockedView: false,
|
||||||
|
currentAlbum: null,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: false,
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ActionButtonType.edit.shouldShow(context), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not show in locked view', () {
|
||||||
|
final context = ActionButtonContext(
|
||||||
|
asset: createRemoteAsset(type: AssetType.image),
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: false,
|
||||||
|
isTrashEnabled: true,
|
||||||
|
isInLockedView: true,
|
||||||
|
currentAlbum: null,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: false,
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ActionButtonType.edit.shouldShow(context), isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not show for non-image assets', () {
|
||||||
|
final context = ActionButtonContext(
|
||||||
|
asset: createRemoteAsset(type: AssetType.video),
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: false,
|
||||||
|
isTrashEnabled: true,
|
||||||
|
isInLockedView: false,
|
||||||
|
currentAlbum: null,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: false,
|
||||||
|
source: ActionSource.timeline,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ActionButtonType.edit.shouldShow(context), isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('shareLink button', () {
|
group('shareLink button', () {
|
||||||
test('should show when not in locked view and asset has remote', () {
|
test('should show when not in locked view and asset has remote', () {
|
||||||
final remoteAsset = createRemoteAsset();
|
final remoteAsset = createRemoteAsset();
|
||||||
@@ -961,5 +1016,38 @@ void main() {
|
|||||||
expect(archivedWidgets, isNotEmpty);
|
expect(archivedWidgets, isNotEmpty);
|
||||||
expect(nonArchivedWidgets, isNotEmpty);
|
expect(nonArchivedWidgets, isNotEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should build quick actions honoring custom order', () {
|
||||||
|
final remoteAsset = createRemoteAsset();
|
||||||
|
final context = ActionButtonContext(
|
||||||
|
asset: remoteAsset,
|
||||||
|
isOwner: true,
|
||||||
|
isArchived: false,
|
||||||
|
isTrashEnabled: true,
|
||||||
|
isInLockedView: false,
|
||||||
|
currentAlbum: null,
|
||||||
|
advancedTroubleshooting: false,
|
||||||
|
isStacked: false,
|
||||||
|
source: ActionSource.viewer,
|
||||||
|
);
|
||||||
|
|
||||||
|
final quickActionService = const QuickActionService(ActionButtonOrderRepository());
|
||||||
|
final quickActionTypes = quickActionService.buildQuickActionTypes(
|
||||||
|
context,
|
||||||
|
quickActionOrder: const [
|
||||||
|
ActionButtonType.archive,
|
||||||
|
ActionButtonType.share,
|
||||||
|
ActionButtonType.edit,
|
||||||
|
ActionButtonType.delete,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final quickActions = quickActionTypes.map((type) => type.buildButton(context)).toList();
|
||||||
|
|
||||||
|
expect(quickActions.length, ActionButtonBuilder.defaultQuickActionLimit);
|
||||||
|
expect(quickActions.first, isA<ArchiveActionButton>());
|
||||||
|
expect(quickActions[1], isA<ShareActionButton>());
|
||||||
|
expect(quickActions[2], isA<EditImageActionButton>());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -717,8 +717,8 @@ importers:
|
|||||||
specifier: file:../open-api/typescript-sdk
|
specifier: file:../open-api/typescript-sdk
|
||||||
version: link:../open-api/typescript-sdk
|
version: link:../open-api/typescript-sdk
|
||||||
'@immich/ui':
|
'@immich/ui':
|
||||||
specifier: ^0.50.0
|
specifier: ^0.50.1
|
||||||
version: 0.50.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)
|
version: 0.50.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)
|
||||||
'@mapbox/mapbox-gl-rtl-text':
|
'@mapbox/mapbox-gl-rtl-text':
|
||||||
specifier: 0.2.3
|
specifier: 0.2.3
|
||||||
version: 0.2.3(mapbox-gl@1.13.3)
|
version: 0.2.3(mapbox-gl@1.13.3)
|
||||||
@@ -2989,8 +2989,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
|
|
||||||
'@immich/ui@0.50.0':
|
'@immich/ui@0.50.1':
|
||||||
resolution: {integrity: sha512-7AW9SRZTAgal8xlkUAxm7o4+pSG7HcKb+Bh9JpWLaDRRdGyPCZMmsNa9CjZglOQ7wkAD07tQ9u4+zezBLe0dlQ==}
|
resolution: {integrity: sha512-fNlQGh75ZFa/UZAgJaYk9/ItHOXHNNzN4CunjCmE7WocVVkUZbUxopN9Ku3F5GULSqD/zJ5gNO6PQAZ1ZoSaaQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
svelte: ^5.0.0
|
svelte: ^5.0.0
|
||||||
|
|
||||||
@@ -14701,7 +14701,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
svelte: 5.45.2
|
svelte: 5.45.2
|
||||||
|
|
||||||
'@immich/ui@0.50.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)':
|
'@immich/ui@0.50.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2)
|
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2)
|
||||||
'@internationalized/date': 3.10.0
|
'@internationalized/date': 3.10.0
|
||||||
|
|||||||
@@ -50,13 +50,15 @@ RUN --mount=type=cache,id=pnpm-cli,target=/buildcache/pnpm-store \
|
|||||||
|
|
||||||
FROM builder AS plugins
|
FROM builder AS plugins
|
||||||
|
|
||||||
|
ARG TARGETPLATFORM
|
||||||
|
|
||||||
COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise
|
COPY --from=ghcr.io/jdx/mise:2025.11.3@sha256:ac26f5978c0e2783f3e68e58ce75eddb83e41b89bf8747c503bac2aa9baf22c5 /usr/local/bin/mise /usr/local/bin/mise
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
COPY ./plugins/mise.toml ./plugins/
|
COPY ./plugins/mise.toml ./plugins/
|
||||||
ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml
|
ENV MISE_TRUSTED_CONFIG_PATHS=/usr/src/app/plugins/mise.toml
|
||||||
ENV MISE_DATA_DIR=/buildcache/mise
|
ENV MISE_DATA_DIR=/buildcache/mise
|
||||||
RUN --mount=type=cache,id=mise-tools,target=/buildcache/mise \
|
RUN --mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||||
mise install --cd plugins
|
mise install --cd plugins
|
||||||
|
|
||||||
COPY ./plugins ./plugins/
|
COPY ./plugins ./plugins/
|
||||||
@@ -66,7 +68,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \
|
|||||||
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
|
--mount=type=bind,source=.pnpmfile.cjs,target=.pnpmfile.cjs \
|
||||||
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
--mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \
|
||||||
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
|
--mount=type=bind,source=pnpm-workspace.yaml,target=pnpm-workspace.yaml \
|
||||||
--mount=type=cache,id=mise-tools,target=/buildcache/mise \
|
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||||
cd plugins && mise run build
|
cd plugins && mise run build
|
||||||
|
|
||||||
FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29
|
FROM ghcr.io/immich-app/base-server-prod:202511261514@sha256:c04c1c38dd90e53455b180aedf93c3c63474c8d20ffe2c6d7a3a61a2181e6d29
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||||
"@immich/justified-layout-wasm": "^0.4.3",
|
"@immich/justified-layout-wasm": "^0.4.3",
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@immich/ui": "^0.50.0",
|
"@immich/ui": "^0.50.1",
|
||||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||||
"@mdi/js": "^7.4.47",
|
"@mdi/js": "^7.4.47",
|
||||||
"@photo-sphere-viewer/core": "^5.14.0",
|
"@photo-sphere-viewer/core": "^5.14.0",
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<tr
|
<tr
|
||||||
class="flex h-12 w-full place-items-center border-3 border-transparent p-2 text-center even:bg-subtle/20 odd:bg-subtle/80 hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:p-5"
|
class="flex w-full place-items-center border-3 border-transparent p-2 text-center even:bg-subtle/20 odd:bg-subtle/80 hover:cursor-pointer hover:border-immich-primary/75 odd:dark:bg-immich-dark-gray/75 even:dark:bg-immich-dark-gray/50 dark:hover:border-immich-dark-primary/75 md:px-5 md:py-2"
|
||||||
onclick={() => goto(resolve(`${AppRoute.ALBUMS}/${album.id}`))}
|
onclick={() => goto(resolve(`${AppRoute.ALBUMS}/${album.id}`))}
|
||||||
{oncontextmenu}
|
{oncontextmenu}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user