diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index 8f5f85fb03..243a895967 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -475,7 +475,6 @@ 324F6C652ED5E90F00C76A85 /* PlaceDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324F6C642ED5E90F00C76A85 /* PlaceDetailsViewController.swift */; }; 325B69FD26D828EB00D8B67C /* OAQuickSearchCoordinatesViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 325B69FC26D828EB00D8B67C /* OAQuickSearchCoordinatesViewController.mm */; }; 325B6A0526D923AA00D8B67C /* OAQuickSearchCoordinateFormatsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 325B6A0426D923AA00D8B67C /* OAQuickSearchCoordinateFormatsViewController.m */; }; - 325CFBEB2B5052E000090DF2 /* TracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 325CFBEA2B5052E000090DF2 /* TracksViewController.swift */; }; 325D713C2A1B71EB00F81ECE /* ic_custom_marker_remove@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 325D71382A1B71E900F81ECE /* ic_custom_marker_remove@3x.png */; }; 325D713D2A1B71EB00F81ECE /* ic_custom_arrow_direction@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 325D71392A1B71EA00F81ECE /* ic_custom_arrow_direction@3x.png */; }; 325D713E2A1B71EB00F81ECE /* ic_custom_marker_remove@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 325D713A2A1B71EA00F81ECE /* ic_custom_marker_remove@2x.png */; }; @@ -1627,6 +1626,11 @@ CE7818262FC07944005CCF47 /* WikipediaContextMenuCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7818252FC07944005CCF47 /* WikipediaContextMenuCell.swift */; }; D1A0B0012F50000100A0B001 /* OpeningHoursParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0AFFE2F50000100A0B001 /* OpeningHoursParserTest.swift */; }; D1A0B0032F50000100A0B001 /* OpeningHoursParserTestSupport.mm in Sources */ = {isa = PBXBuildFile; fileRef = D1A0B0002F50000100A0B001 /* OpeningHoursParserTestSupport.mm */; }; + D71B9A8C2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71B9A8B2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift */; }; + D71B9A8E2FC96D2D00FBB0F3 /* OrganizeByTypeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71B9A8D2FC96D2D00FBB0F3 /* OrganizeByTypeExtension.swift */; }; + D71B9A902FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71B9A8F2FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift */; }; + D7B76D0D2FD16070004EE3E9 /* OrganizeByStepSizeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B76D0C2FD16070004EE3E9 /* OrganizeByStepSizeViewController.swift */; }; + D7BF04782FD2DB4400BABB31 /* TracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BF04772FD2DB4400BABB31 /* TracksViewController.swift */; }; DA0132D42A1E0AB500920C14 /* WidgetsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0132D32A1E0AB500920C14 /* WidgetsListViewController.swift */; }; DA0132DD2A1E4A6300920C14 /* ic_custom20_screen_side_right@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DA0132D52A1E4A6100920C14 /* ic_custom20_screen_side_right@2x.png */; }; DA0132DE2A1E4A6300920C14 /* ic_custom20_screen_side_top@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DA0132D62A1E4A6100920C14 /* ic_custom20_screen_side_top@2x.png */; }; @@ -3358,7 +3362,6 @@ FA9628E42AFCFCE1004B7DEF /* SectionHeaderFooterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9628E32AFCFCE1004B7DEF /* SectionHeaderFooterButton.swift */; }; FA9628E62AFD2F2E004B7DEF /* BLETemperatureSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9628E52AFD2F2E004B7DEF /* BLETemperatureSensor.swift */; }; FA9F416F2E6F10B7001A49E9 /* ImageCacheInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9F416E2E6F10B7001A49E9 /* ImageCacheInfoViewController.swift */; }; - FADEC0DE000000000000A001 /* OATouchIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADEC0DE000000000000A002 /* OATouchIndicatorController.swift */; }; FA9FA5992E97D11400376C47 /* MapScrollHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9FA5982E97D11400376C47 /* MapScrollHelper.swift */; }; FAA055762E02C51400F84EDF /* VehicleMetricsTripRecordingCommandsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA055752E02C51200F84EDF /* VehicleMetricsTripRecordingCommandsViewController.swift */; }; FAA0557A2E0301AA00F84EDF /* SelectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA055792E0301AA00F84EDF /* SelectionManager.swift */; }; @@ -3423,6 +3426,7 @@ FAD8489A2CAD6F950018AF92 /* WptPtExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD848992CAD6F950018AF92 /* WptPtExtension.swift */; }; FAD848A02CAE9FDD0018AF92 /* GpxFileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD8489F2CAE9FDD0018AF92 /* GpxFileExtension.swift */; }; FADD74092FC0433400004A51 /* RenderedObjectAmenityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADD74082FC0433400004A51 /* RenderedObjectAmenityProvider.swift */; }; + FADEC0DE000000000000A001 /* TouchIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADEC0DE000000000000A002 /* TouchIndicatorController.swift */; }; FAE0B7112AFCDAD400C02438 /* BLEPairedSensorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE0B7102AFCDAD400C02438 /* BLEPairedSensorsViewController.swift */; }; FAE0B7132AFCDC6B00C02438 /* BLEPairedSensors.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAE0B7122AFCDC6B00C02438 /* BLEPairedSensors.storyboard */; }; FAE0B7162AFCEF4D00C02438 /* СhoicePairedDeviceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE0B7152AFCEF4D00C02438 /* СhoicePairedDeviceTableViewCell.swift */; }; @@ -4043,7 +4047,6 @@ 325B69FC26D828EB00D8B67C /* OAQuickSearchCoordinatesViewController.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OAQuickSearchCoordinatesViewController.mm; sourceTree = ""; }; 325B6A0326D9239800D8B67C /* OAQuickSearchCoordinateFormatsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OAQuickSearchCoordinateFormatsViewController.h; sourceTree = ""; }; 325B6A0426D923AA00D8B67C /* OAQuickSearchCoordinateFormatsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = OAQuickSearchCoordinateFormatsViewController.m; sourceTree = ""; }; - 325CFBEA2B5052E000090DF2 /* TracksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracksViewController.swift; sourceTree = ""; }; 325D71382A1B71E900F81ECE /* ic_custom_marker_remove@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom_marker_remove@3x.png"; path = "Resources/Icons/ic_custom_marker_remove@3x.png"; sourceTree = ""; }; 325D71392A1B71EA00F81ECE /* ic_custom_arrow_direction@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom_arrow_direction@3x.png"; path = "Resources/Icons/ic_custom_arrow_direction@3x.png"; sourceTree = ""; }; 325D713A2A1B71EA00F81ECE /* ic_custom_marker_remove@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom_marker_remove@2x.png"; path = "Resources/Icons/ic_custom_marker_remove@2x.png"; sourceTree = ""; }; @@ -5561,6 +5564,11 @@ D1A0AFFE2F50000100A0B001 /* OpeningHoursParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpeningHoursParserTest.swift; sourceTree = ""; }; D1A0AFFF2F50000100A0B001 /* OpeningHoursParserTestSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OpeningHoursParserTestSupport.h; sourceTree = ""; }; D1A0B0002F50000100A0B001 /* OpeningHoursParserTestSupport.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OpeningHoursParserTestSupport.mm; sourceTree = ""; }; + D71B9A8B2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizeTracksByViewController.swift; sourceTree = ""; }; + D71B9A8D2FC96D2D00FBB0F3 /* OrganizeByTypeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizeByTypeExtension.swift; sourceTree = ""; }; + D71B9A8F2FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizeByTypeCell.swift; sourceTree = ""; }; + D7B76D0C2FD16070004EE3E9 /* OrganizeByStepSizeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizeByStepSizeViewController.swift; sourceTree = ""; }; + D7BF04772FD2DB4400BABB31 /* TracksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracksViewController.swift; sourceTree = ""; }; DA0132D32A1E0AB500920C14 /* WidgetsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsListViewController.swift; sourceTree = ""; }; DA0132D52A1E4A6100920C14 /* ic_custom20_screen_side_right@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom20_screen_side_right@2x.png"; path = "Resources/Icons/ic_custom20_screen_side_right@2x.png"; sourceTree = ""; }; DA0132D62A1E4A6100920C14 /* ic_custom20_screen_side_top@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom20_screen_side_top@2x.png"; path = "Resources/Icons/ic_custom20_screen_side_top@2x.png"; sourceTree = ""; }; @@ -8135,7 +8143,6 @@ FA9628E32AFCFCE1004B7DEF /* SectionHeaderFooterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaderFooterButton.swift; sourceTree = ""; }; FA9628E52AFD2F2E004B7DEF /* BLETemperatureSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLETemperatureSensor.swift; sourceTree = ""; }; FA9F416E2E6F10B7001A49E9 /* ImageCacheInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheInfoViewController.swift; sourceTree = ""; }; - FADEC0DE000000000000A002 /* OATouchIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATouchIndicatorController.swift; sourceTree = ""; }; FA9FA5982E97D11400376C47 /* MapScrollHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapScrollHelper.swift; sourceTree = ""; }; FAA055752E02C51200F84EDF /* VehicleMetricsTripRecordingCommandsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleMetricsTripRecordingCommandsViewController.swift; sourceTree = ""; }; FAA055792E0301AA00F84EDF /* SelectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionManager.swift; sourceTree = ""; }; @@ -8202,6 +8209,7 @@ FAD848992CAD6F950018AF92 /* WptPtExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WptPtExtension.swift; sourceTree = ""; }; FAD8489F2CAE9FDD0018AF92 /* GpxFileExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GpxFileExtension.swift; sourceTree = ""; }; FADD74082FC0433400004A51 /* RenderedObjectAmenityProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderedObjectAmenityProvider.swift; sourceTree = ""; }; + FADEC0DE000000000000A002 /* TouchIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TouchIndicatorController.swift; sourceTree = ""; }; FADFF8872C885AFD00F68E4F /* OsmAndSharedWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OsmAndSharedWrapper.h; sourceTree = ""; }; FAE0B7102AFCDAD400C02438 /* BLEPairedSensorsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEPairedSensorsViewController.swift; sourceTree = ""; }; FAE0B7122AFCDC6B00C02438 /* BLEPairedSensors.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BLEPairedSensors.storyboard; sourceTree = ""; }; @@ -8460,7 +8468,7 @@ children = ( FA1FC6BE2EA1285E00A60962 /* BLEScannerViewController.swift */, FA9F416E2E6F10B7001A49E9 /* ImageCacheInfoViewController.swift */, - FADEC0DE000000000000A002 /* OATouchIndicatorController.swift */, + FADEC0DE000000000000A002 /* TouchIndicatorController.swift */, C5AD0F152EAA60E3006FB675 /* ButtonGridVisualizationSettingsViewController.swift */, 32119B4E2847D7F7005E1E0C /* OAOsmandDevelopmentSimulateLocationViewController.h */, 32119B4F2847D807005E1E0C /* OAOsmandDevelopmentSimulateLocationViewController.mm */, @@ -12661,13 +12669,16 @@ DA5A7DC226C563A300F274C7 /* MyPlaces */ = { isa = PBXGroup; children = ( + D7B76D0C2FD16070004EE3E9 /* OrganizeByStepSizeViewController.swift */, + D7BF04772FD2DB4400BABB31 /* TracksViewController.swift */, + D71B9A8B2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift */, + D71B9A8F2FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift */, DA5A7DC426C563A300F274C7 /* MyPlaces.storyboard */, DA5A7E0126C563A300F274C7 /* OAFavoriteListViewController.h */, DA5A7E0C26C563A300F274C7 /* OAFavoriteListViewController.mm */, DA5A7DC726C563A300F274C7 /* OAOsmEditsListViewController.h */, DA5A7DC626C563A300F274C7 /* OAOsmEditsListViewController.mm */, 327D68A42A8F9BA80076D1E0 /* SavedArticlesTabViewController.swift */, - 325CFBEA2B5052E000090DF2 /* TracksViewController.swift */, C50E32812CA3FBDF00EEC41F /* TracksFiltersViewController.swift */, C50E32832CA57DDB00EEC41F /* TracksFilterDetailsViewController.swift */, C5A887E32CAD7B8E00DA2979 /* TracksSearchFilter.swift */, @@ -14496,6 +14507,7 @@ FA45851C2C946046003A5AD7 /* SharedLibHelpers */ = { isa = PBXGroup; children = ( + D71B9A8D2FC96D2D00FBB0F3 /* OrganizeByTypeExtension.swift */, FAA8F9052CC2843F008452CF /* TrackItemExtension.swift */, FA5D19E02CC00B0E00E01A27 /* GPXDataItemGPXFileWrapper.swift */, 05DACD7D2C1CAF470023FAD9 /* OASharedUtil.h */, @@ -17219,6 +17231,7 @@ DA5A846E26C563A900F274C7 /* OAWidgetState.m in Sources */, C5C81F922E535061009D3439 /* DriveModeButtonState.swift in Sources */, 3239C29C2EFC73FD00D526A8 /* ClickableWayHelper.swift in Sources */, + D7BF04782FD2DB4400BABB31 /* TracksViewController.swift in Sources */, FA64086B2D3E6EA5005FD12D /* CollapsableCardsView.swift in Sources */, DA5A849726C563A900F274C7 /* OAGpxData.mm in Sources */, 464BF4A92C0517E7000D6908 /* Map3DModeVisibility.swift in Sources */, @@ -17891,6 +17904,7 @@ DA5A818226C563A700F274C7 /* OAMapStyleAction.mm in Sources */, DA5A843126C563A800F274C7 /* OARouteTargetSelectionViewController.m in Sources */, 4617129E2B973C34000446F9 /* GlideUtils.swift in Sources */, + D71B9A902FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift in Sources */, 32D253B027F336C200324717 /* OACloudAccountCreateViewController.m in Sources */, CE4075A32FC48BD9004224AA /* BasePoiIconCollectionHandler.swift in Sources */, FA63F24C2AF3DAA00040A0CE /* SensorDataField.swift in Sources */, @@ -17962,6 +17976,7 @@ DA5A819E26C563A700F274C7 /* NSData+CRC32.m in Sources */, DA69ED5C2A385B1B001022C7 /* WidgetPanelViewController.swift in Sources */, FACE409F2AEA9ACB00E1E43A /* OAExternalSensorsPlugin.mm in Sources */, + D71B9A8E2FC96D2D00FBB0F3 /* OrganizeByTypeExtension.swift in Sources */, DA5A816526C563A700F274C7 /* OAApplicationMode.m in Sources */, FA6439EB2B051A5300331006 /* MeasurementFormatter+Extension.swift in Sources */, C58923FD2D68914800EC5910 /* BaseMonitoringAction.swift in Sources */, @@ -18076,7 +18091,7 @@ DA5A82F326C563A800F274C7 /* OATargetPointViewCell.mm in Sources */, DA5A845226C563A900F274C7 /* OAMapLayers.mm in Sources */, FA9F416F2E6F10B7001A49E9 /* ImageCacheInfoViewController.swift in Sources */, - FADEC0DE000000000000A001 /* OATouchIndicatorController.swift in Sources */, + FADEC0DE000000000000A001 /* TouchIndicatorController.swift in Sources */, C5983E1C2EFD6B75003F2639 /* WikipediaLanguagesViewController.swift in Sources */, DA4ABEEB2876DA5D00B996EF /* OASettingsCategoryItems.m in Sources */, FA29F6152F96117E00257B88 /* GeocoderService.swift in Sources */, @@ -18084,7 +18099,6 @@ 27E316922E72F5CE007172DA /* CompassMode.swift in Sources */, C57693192D67148400E4F772 /* ChangeTracksAppearanceTask.swift in Sources */, DA5A81DE26C563A700F274C7 /* OAStreet.mm in Sources */, - 325CFBEB2B5052E000090DF2 /* TracksViewController.swift in Sources */, 32DE2BE42B6D13730025F2B9 /* OATwoButtonsTableViewCell.swift in Sources */, FAA650682ADD8D0D0020DCEA /* BLEInitHeader.swift in Sources */, DA5A814426C563A700F274C7 /* OAOsmNotePoint.m in Sources */, @@ -18210,6 +18224,7 @@ 320F71272A823FB20071C0E7 /* PopularArticles.swift in Sources */, DA5A850B26C563A900F274C7 /* OAButton.m in Sources */, DA5A81C126C563A700F274C7 /* OAPOIParser.m in Sources */, + D7B76D0D2FD16070004EE3E9 /* OrganizeByStepSizeViewController.swift in Sources */, FAF6C89B2AD013F400A3ED94 /* UIColor+Extension.swift in Sources */, DA5A824C26C563A700F274C7 /* OANavigationLanguageViewController.m in Sources */, 46ED6C402B333B8100A5555F /* ExternalSensorsPlugin+Extension.swift in Sources */, @@ -18386,6 +18401,7 @@ 4671FB902CDE6E3A00446D18 /* GpxUtils.swift in Sources */, 321C83872F33586B0028FCCC /* OpeningHoursRowBehavior.swift in Sources */, DA5A847026C563A900F274C7 /* OACompassRulerWidgetState.m in Sources */, + D71B9A8C2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift in Sources */, DA5A82E626C563A800F274C7 /* OATableViewCustomHeaderView.m in Sources */, DA5A81C926C563A700F274C7 /* OAPointIContainer.mm in Sources */, DA5A83EB26C563A800F274C7 /* OAQuickSearchViewController.mm in Sources */, diff --git a/Resources/Images.xcassets/Icons/ic_custom_activity.imageset/Contents.json b/Resources/Images.xcassets/Icons/ic_custom_activity.imageset/Contents.json new file mode 100644 index 0000000000..a679b603ce --- /dev/null +++ b/Resources/Images.xcassets/Icons/ic_custom_activity.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "ic_custom_activity.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/Resources/Images.xcassets/Icons/ic_custom_activity.imageset/ic_custom_activity.svg b/Resources/Images.xcassets/Icons/ic_custom_activity.imageset/ic_custom_activity.svg new file mode 100644 index 0000000000..a42c9ca6e6 --- /dev/null +++ b/Resources/Images.xcassets/Icons/ic_custom_activity.imageset/ic_custom_activity.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index e896610347..1cc750b368 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -1814,6 +1814,7 @@ "select_tracks" = "Select tracks"; "selected_tracks_count" = "Tracks - %d"; "folder_tracks_count" = "%d tracks"; +"folder_one_track" = "1 track"; "delete_tracks_bottom_sheet_description_regular_part" = "Are you sure you want to delete "; "delete_tracks_bottom_sheet_description_bold_part" = "%d tracks?"; @@ -4560,3 +4561,25 @@ "missing_maps_header" = "Missing or outdated maps"; "missing_maps_description" = "Some offline maps required for this route are missing or not up to date. Please download or update these maps."; "shared_string_allow" = "Allow"; +"organize_by" = "Organize by"; +"organize_by_summary" = "Automatically organize your tracks into groups based on a selected parameter like activity, speed, or location."; +"group_general" = "General"; +"group_date_time" = "Date & Time"; +"group_altitude_elevation" = "Altitude and elevation"; +"group_sensors" = "Sensors"; +"shared_string_length" = "Length"; +"year_of_creation" = "Year of creation"; +"month_year_creation" = "Month and Year of creation"; +"nearest_city" = "Nearest city"; +"organize_by_max_speed" = "Max. speed"; +"avg_speed" = "Avg. speed"; +"organize_by_max_altitude" = "Max. altitude"; +"shared_string_avg_altitude" = "Avg. altitude"; +"shared_string_uphill" = "Uphill"; +"shared_string_downhill" = "Downhill"; +"sort_highest_first" = "Highest first"; +"sort_lowest_first" = "Lowest first"; +"set_step_size" = "Set step size"; +"set_step_size_summary" = "Choose a step size to group your tracks."; +"shared_string_step" = "Step"; +"shared_string_unlock" = "Unlock"; diff --git a/Sources/Controllers/MyPlaces/OrganizeByStepSizeViewController.swift b/Sources/Controllers/MyPlaces/OrganizeByStepSizeViewController.swift new file mode 100644 index 0000000000..3e194daf1b --- /dev/null +++ b/Sources/Controllers/MyPlaces/OrganizeByStepSizeViewController.swift @@ -0,0 +1,240 @@ +import UIKit +import OsmAndShared + +protocol OrganizeByStepSizeDelegate: AnyObject { + func onStepSizeChanged() +} + +final class OrganizeByStepSizeViewController: OABaseNavbarViewController { + + // MARK: - Private Types + + private enum Section: Int { + case main + } + + private enum RowKey: String { + case slider + } + + private enum DataKey: String { + case stepValue + } + + // MARK: - Instance Properties + + private static let previewDebounceInterval: TimeInterval = 0.15 + + private let smartFolder: SmartFolder + private let type: OrganizeByType + private let displayUnits: any MeasurementUnit + private let stepRange: Limits + private let initialParams: OrganizeByParams? + private var currentDisplayValue: Float + private var isExplicitlyDismissed = false + private var previewWorkItem: DispatchWorkItem? + + weak var stepDelegate: OrganizeByStepSizeDelegate? + + // MARK: - Initializers + + init(smartFolder: SmartFolder, type: OrganizeByType, originalParams: OrganizeByParams?) { + self.smartFolder = smartFolder + self.type = type + self.displayUnits = type.getDisplayUnits() + guard let range = type.stepRange else { + fatalError("OrganizeByStepSizeViewController requires a range-related OrganizeByType") + } + self.stepRange = range + self.initialParams = originalParams + + let existingBase: Double + if let rangeParams = smartFolder.organizeByParams as? OrganizeByRangeParams { + existingBase = rangeParams.stepSize + } else { + existingBase = type.getDefaultStepInBaseUnits() + } + let rawDisplayValue = Float(Int(type.getDisplayUnits().fromBase(value: existingBase))) + let minDisplayValue = (range.min as? NSNumber).map { Float(truncating: $0) } ?? rawDisplayValue + let maxDisplayValue = (range.max as? NSNumber).map { Float(truncating: $0) } ?? rawDisplayValue + self.currentDisplayValue = min(max(rawDisplayValue, minDisplayValue), maxDisplayValue) + + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Methods + + override func viewDidLoad() { + super.viewDidLoad() + tableView.rowHeight = UITableView.automaticDimension + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + navigationController?.presentationController?.delegate = self + } + + override func registerCells() { + addCell(TopBottomValuesSliderTableViewCell.reuseIdentifier) + } + + override func getTitle() -> String { + localizedString("set_step_size") + } + + override func getTableHeaderDescription() -> String { "" } + + override func getTableHeaderDescriptionAttr() -> NSAttributedString? { + let text = localizedString("set_step_size_summary") + return NSAttributedString(string: text, attributes: [ + .font: UIFont.preferredFont(forTextStyle: .body), + .foregroundColor: UIColor.textColorSecondary + ]) + } + + override func systemLeftBarButtonItem() -> UIBarButtonItem? { + UIBarButtonItem(barButtonSystemItem: .close, target: self, action: #selector(onClosePressed)) + } + + override func tableStyle() -> UITableView.Style { + .insetGrouped + } + + override func systemRightBarButtonItems() -> [UIBarButtonItem]? { + let isFeatureAccessible = !type.isPro || OAIAPHelper.isOsmAndProAvailable() + if isFeatureAccessible { + let image = UIImage.templateImageNamed("ic_checkmark_default")?.withTintColor(.white, renderingMode: .alwaysOriginal) + return [UIBarButtonItem(image: image, style: .done, target: self, action: #selector(onConfirmPressed))] + } else { + return [UIBarButtonItem(title: localizedString("shared_string_unlock"), style: .done, target: self, action: #selector(onUnlockPressed))] + } + } + + override func generateData() { + tableData.clearAllData() + let section = tableData.createNewSection() + let row = section.createNewRow() + row.cellType = TopBottomValuesSliderTableViewCell.reuseIdentifier + row.key = RowKey.slider.rawValue + row.title = localizedString("shared_string_step") + row.setObj(currentDisplayValue as Any, forKey: DataKey.stepValue.rawValue) + } + + override func getRow(_ indexPath: IndexPath?) -> UITableViewCell? { + guard let indexPath else { return nil } + let item = tableData.item(for: indexPath) + guard item.key == RowKey.slider.rawValue, + let cell = tableView.dequeueReusableCell( + withIdentifier: TopBottomValuesSliderTableViewCell.reuseIdentifier + ) as? TopBottomValuesSliderTableViewCell, + let value = item.obj(forKey: DataKey.stepValue.rawValue) as? Float else { return nil } + + let unitSymbol = displayUnits.getSymbol() + let minVal = (stepRange.min as? NSNumber).map { Float(truncating: $0) } ?? 0 + let maxVal = (stepRange.max as? NSNumber).map { Float(truncating: $0) } ?? 100 + + cell.selectionStyle = .none + cell.topRightLabelVisibility(true) + cell.topRightButtonVisibility(false) + cell.sliderValuesVisibility(true) + + cell.topLeftLabel.text = item.title + cell.topLeftLabel.font = .preferredFont(forTextStyle: .body) + cell.topRightLabel.text = "\(Int(value)) \(unitSymbol)" + cell.topRightLabel.textColor = .textColorSecondary + + cell.slider.minimumValue = minVal + cell.slider.maximumValue = maxVal + cell.slider.value = value + cell.slider.accessibilityLabel = item.title + cell.slider.accessibilityValue = "\(Int(value)) \(unitSymbol)" + cell.slider.tintColor = .systemBlue + cell.slider.maximumTrackTintColor = .sliderLineBg + cell.slider.removeTarget(self, action: nil, for: .valueChanged) + cell.slider.addTarget(self, action: #selector(onSliderChanged(_:)), for: .valueChanged) + + cell.bottomLeftLabel.text = "\(Int(minVal)) \(unitSymbol)" + cell.bottomLeftLabel.textColor = .textColorSecondary + cell.bottomRightLabel.text = "\(Int(maxVal)) \(unitSymbol)" + cell.bottomRightLabel.textColor = .textColorSecondary + + return cell + } + + private func makeCurrentParams() -> OrganizeByRangeParams { + let stepInBase = displayUnits.toBase(value: Double(Int(currentDisplayValue))) + return OrganizeByRangeParams(type: type, stepSize: stepInBase) + } + + private func applyStepInMemory() { + smartFolder.setOrganizeByParams(organizeByParams: makeCurrentParams()) + } + + private func persistCurrentStep() { + let params = makeCurrentParams() + SharedLibSmartFolderHelper.shared.setOrganizeByParams(folderId: smartFolder.getId(), params: params) + smartFolder.setOrganizeByParams(organizeByParams: params) + } + + private func schedulePreviewUpdate() { + previewWorkItem?.cancel() + let workItem = DispatchWorkItem { [weak self] in + guard let self else { return } + self.applyStepInMemory() + self.stepDelegate?.onStepSizeChanged() + } + previewWorkItem = workItem + DispatchQueue.main.asyncAfter(deadline: .now() + Self.previewDebounceInterval, execute: workItem) + } + + private func performCancel() { + previewWorkItem?.cancel() + SharedLibSmartFolderHelper.shared.setOrganizeByParams(folderId: smartFolder.getId(), params: initialParams) + smartFolder.setOrganizeByParams(organizeByParams: initialParams) + stepDelegate?.onStepSizeChanged() + } + + @objc private func onSliderChanged(_ sender: UISlider) { + let rounded = sender.value.rounded() + sender.value = rounded + currentDisplayValue = rounded + if let cell = tableView.cellForRow(at: IndexPath(row: 0, section: Section.main.rawValue)) as? TopBottomValuesSliderTableViewCell { + let formattedValue = "\(Int(rounded)) \(displayUnits.getSymbol())" + cell.topRightLabel.text = formattedValue + cell.slider.accessibilityValue = formattedValue + } + schedulePreviewUpdate() + } + + @objc private func onConfirmPressed() { + isExplicitlyDismissed = true + previewWorkItem?.cancel() + persistCurrentStep() + stepDelegate?.onStepSizeChanged() + dismiss(animated: true) + } + + @objc private func onClosePressed() { + isExplicitlyDismissed = true + performCancel() + dismiss(animated: true) + } + + @objc private func onUnlockPressed() { + guard let navigationController else { return } + OAChoosePlanHelper.showChoosePlanScreen(with: OAFeature.advanced_WIDGETS(), navController: navigationController) + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate + +extension OrganizeByStepSizeViewController: UIAdaptivePresentationControllerDelegate { + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + guard !isExplicitlyDismissed else { return } + performCancel() + } +} diff --git a/Sources/Controllers/MyPlaces/OrganizeByTypeCell.swift b/Sources/Controllers/MyPlaces/OrganizeByTypeCell.swift new file mode 100644 index 0000000000..64ab8180aa --- /dev/null +++ b/Sources/Controllers/MyPlaces/OrganizeByTypeCell.swift @@ -0,0 +1,105 @@ +import UIKit + +final class OrganizeByTypeCell: UITableViewCell { + + static let minHeight: CGFloat = 52 + private static let leadingInset: CGFloat = 16 + private static let checkmarkSize: CGFloat = 20 + private static let iconSize: CGFloat = 28 + private static let gap: CGFloat = 8 + private static let verticalPadding: CGFloat = 12 + + var onProBadgeTapped: (() -> Void)? + + private let checkmarkView = UIImageView() + private let iconView = UIImageView() + private let titleLabel = UILabel() + + private lazy var proBadgeButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(.icPaymentLabelPro, for: .normal) + button.sizeToFit() + button.addTarget(self, action: #selector(proBadgeButtonPressed), for: .touchUpInside) + return button + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupLayout() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupLayout() { + checkmarkView.isAccessibilityElement = false + checkmarkView.contentMode = .scaleAspectFit + checkmarkView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(checkmarkView) + + iconView.isAccessibilityElement = false + iconView.contentMode = .scaleAspectFit + iconView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(iconView) + + titleLabel.numberOfLines = 0 + titleLabel.adjustsFontForContentSizeCategory = true + titleLabel.font = .preferredFont(forTextStyle: .body) + titleLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(titleLabel) + + let leadingInset = Self.leadingInset + let checkmarkSize = Self.checkmarkSize + let iconSize = Self.iconSize + let gap = Self.gap + let verticalPadding = Self.verticalPadding + + NSLayoutConstraint.activate([ + checkmarkView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: leadingInset), + checkmarkView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + checkmarkView.widthAnchor.constraint(equalToConstant: checkmarkSize), + checkmarkView.heightAnchor.constraint(equalToConstant: checkmarkSize), + + iconView.leadingAnchor.constraint(equalTo: checkmarkView.trailingAnchor, constant: gap), + iconView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconView.widthAnchor.constraint(equalToConstant: iconSize), + iconView.heightAnchor.constraint(equalToConstant: iconSize), + + titleLabel.leadingAnchor.constraint(equalTo: iconView.trailingAnchor, constant: gap), + titleLabel.trailingAnchor.constraint(lessThanOrEqualTo: contentView.trailingAnchor, constant: -leadingInset), + titleLabel.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: verticalPadding), + titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -verticalPadding), + titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + + contentView.heightAnchor.constraint(greaterThanOrEqualToConstant: Self.minHeight) + ]) + } + + func configure(title: String?, icon: UIImage?, isSelected: Bool, isLocked: Bool) { + let leadingInset = Self.leadingInset + let checkmarkSize = Self.checkmarkSize + let iconSize = Self.iconSize + let gap = Self.gap + separatorInset = UIEdgeInsets(top: 0, left: leadingInset + checkmarkSize + gap + iconSize + gap, bottom: 0, right: leadingInset) + titleLabel.text = title + titleLabel.textColor = .textColorPrimary + + iconView.image = icon + iconView.tintColor = isSelected ? .iconColorActive : .iconColorDefault + + checkmarkView.image = isSelected ? .templateImageNamed("ic_checkmark_default") : nil + checkmarkView.tintColor = .iconColorActive + + accessoryType = .none + accessoryView = isLocked ? proBadgeButton : nil + selectionStyle = .default + + accessibilityTraits = isSelected ? [.button, .selected] : .button + proBadgeButton.accessibilityLabel = localizedString("shared_string_unlock") + } + + @objc private func proBadgeButtonPressed() { + onProBadgeTapped?() + } +} diff --git a/Sources/Controllers/MyPlaces/OrganizeTracksByViewController.swift b/Sources/Controllers/MyPlaces/OrganizeTracksByViewController.swift new file mode 100644 index 0000000000..213f7cc849 --- /dev/null +++ b/Sources/Controllers/MyPlaces/OrganizeTracksByViewController.swift @@ -0,0 +1,233 @@ +import UIKit +import OsmAndShared + +protocol OrganizeTracksByDelegate: AnyObject { + func onOrganizeByRangeParamsApplied(type: OrganizeByType, originalParams: OrganizeByParams?) + func onOrganizeByParamsApplied() +} + +final class OrganizeTracksByViewController: OABaseNavbarViewController { + + // MARK: - Private Types + + private enum RowKey: String { + case none + case type + } + + private enum DataKey: String { + case image + case isSelected + case isLocked + } + + private struct RowData { + let key: RowKey + let title: String + let image: UIImage? + let type: OrganizeByType? + let isSelected: Bool + let isLocked: Bool + } + + private struct SectionData { + let headerText: String? + let rows: [RowData] + } + + // MARK: - Instance Properties + + weak var delegate: OrganizeTracksByDelegate? + + private let smartFolder: SmartFolder + private var selectedType: OrganizeByType? + + // MARK: - Initializers + + init(smartFolder: SmartFolder) { + self.smartFolder = smartFolder + super.init() + selectedType = smartFolder.getOrganizeByType() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - Methods + + override func viewDidLoad() { + super.viewDidLoad() + tableView.rowHeight = UITableView.automaticDimension + tableView.estimatedRowHeight = OrganizeByTypeCell.minHeight + if let button = navigationItem.leftBarButtonItem?.customView as? UIButton { + button.tintColor = .textColorPrimary + button.accessibilityLabel = localizedString("shared_string_close") + } + } + + override func registerCells() { + tableView.register(OrganizeByTypeCell.self, forCellReuseIdentifier: OrganizeByTypeCell.reuseIdentifier) + } + + override func getTitle() -> String { + localizedString("organize_by") + } + + override func getTableHeaderDescription() -> String { "" } + + override func getTableHeaderDescriptionAttr() -> NSAttributedString? { + let text = localizedString("organize_by_summary") + return NSAttributedString(string: text, attributes: [ + .font: UIFont.preferredFont(forTextStyle: .body), + .foregroundColor: UIColor.textColorSecondary + ]) + } + + override func getCustomIconForLeftNavbarButton() -> UIImage? { + .templateImageNamed("ic_navbar_close") + } + + override func tableStyle() -> UITableView.Style { + .insetGrouped + } + + override func systemRightBarButtonItems() -> [UIBarButtonItem]? { + return [UIBarButtonItem(title: localizedString("shared_string_apply"), style: .done, target: self, action: #selector(onApplyButtonPressed))] + } + + override func generateData() { + let proAvailable = OAIAPHelper.isOsmAndProAvailable() + let sections = buildSections(proAvailable: proAvailable, selectedType: selectedType) + populateTableData(sections) + } + + override func getRow(_ indexPath: IndexPath?) -> UITableViewCell? { + guard let indexPath else { return nil } + let item = tableData.item(for: indexPath) + + guard let cell = tableView.dequeueReusableCell(withIdentifier: OrganizeByTypeCell.reuseIdentifier) as? OrganizeByTypeCell else { return nil } + cell.configure( + title: item.title, + icon: item.obj(forKey: DataKey.image.rawValue) as? UIImage, + isSelected: item.bool(forKey: DataKey.isSelected.rawValue), + isLocked: item.bool(forKey: DataKey.isLocked.rawValue) + ) + cell.onProBadgeTapped = { [weak self] in + self?.openChoosePlan() + } + return cell + } + + override func onRowSelected(_ indexPath: IndexPath) { + let item = tableData.item(for: indexPath) + + if item.key == RowKey.none.rawValue { + selectedType = nil + } else { + guard let type = item.obj(forKey: RowKey.type.rawValue) as? OrganizeByType else { return } + selectedType = type + } + generateData() + tableView.reloadData() + } + + private func applyDirectly() { + let params: OrganizeByParams? = selectedType.map { OrganizeByParams(type: $0) } + SharedLibSmartFolderHelper.shared.setOrganizeByParams(folderId: smartFolder.getId(), params: params) + smartFolder.setOrganizeByParams(organizeByParams: params) + delegate?.onOrganizeByParamsApplied() + dismiss(animated: true) + } + + private func applyAndOpenStepSize(type: OrganizeByType) { + let originalParams = smartFolder.organizeByParams + let stepSize: Double + if let existingRange = originalParams as? OrganizeByRangeParams, existingRange.type == type { + stepSize = existingRange.stepSize + } else { + stepSize = type.getDefaultStepInBaseUnits() + } + let params = OrganizeByRangeParams(type: type, stepSize: stepSize) + SharedLibSmartFolderHelper.shared.setOrganizeByParams(folderId: smartFolder.getId(), params: params) + smartFolder.setOrganizeByParams(organizeByParams: params) + dismiss(animated: true) { [weak self] in + guard let self else { return } + self.delegate?.onOrganizeByRangeParamsApplied(type: type, originalParams: originalParams) + } + } + + private func openChoosePlan() { + guard let navigationController else { return } + OAChoosePlanHelper.showChoosePlanScreen(with: OAFeature.advanced_WIDGETS(), navController: navigationController) + } + + private func buildSections(proAvailable: Bool, selectedType: OrganizeByType?) -> [SectionData] { + var sections: [SectionData] = [] + + let noneRow = RowData( + key: .none, + title: localizedString("shared_string_none"), + image: .icCustomList, + type: nil, + isSelected: selectedType == nil, + isLocked: false + ) + sections.append(SectionData(headerText: nil, rows: [noneRow])) + + for category in OrganizeByCategory.entries { + let types = OrganizeByType.companion.valuesOf(category: category) + guard !types.isEmpty else { continue } + var rows: [RowData] = [] + for type in types { + rows.append(RowData( + key: .type, + title: type.getName(), + image: type.image, + type: type, + isSelected: selectedType == type, + isLocked: type.isLockedBehindPro && !proAvailable + )) + } + sections.append(SectionData(headerText: category.getName(), rows: rows)) + } + + return sections + } + + private func populateTableData(_ sections: [SectionData]) { + tableData.clearAllData() + for sectionData in sections { + let section = tableData.createNewSection() + section.headerText = sectionData.headerText ?? "" + for rowData in sectionData.rows { + let row = section.createNewRow() + row.cellType = OrganizeByTypeCell.reuseIdentifier + row.key = rowData.key.rawValue + row.title = rowData.title + row.setObj(rowData.image as Any, forKey: DataKey.image.rawValue) + if let type = rowData.type { + row.setObj(type, forKey: RowKey.type.rawValue) + } + row.setObj(rowData.isSelected, forKey: DataKey.isSelected.rawValue) + row.setObj(rowData.isLocked, forKey: DataKey.isLocked.rawValue) + } + } + } + + @objc private func onApplyButtonPressed() { + guard let type = selectedType else { + applyDirectly() + return + } + if type.isRangeRelated() { + applyAndOpenStepSize(type: type) + return + } + if type.isLockedBehindPro, !OAIAPHelper.isOsmAndProAvailable() { + openChoosePlan() + return + } + applyDirectly() + } +} diff --git a/Sources/Controllers/MyPlaces/TracksViewController.swift b/Sources/Controllers/MyPlaces/TracksViewController.swift index 4c503bec08..76060ca0f0 100644 --- a/Sources/Controllers/MyPlaces/TracksViewController.swift +++ b/Sources/Controllers/MyPlaces/TracksViewController.swift @@ -18,7 +18,7 @@ private enum ButtonActionNumberTag: Int { case save = 2 } -final class TracksViewController: UITableViewController, OATrackSavingHelperUpdatableDelegate, TrackListUpdatableDelegate, OASelectTrackFolderDelegate, MapSettingsGpxViewControllerDelegate, MyPlacesSearchable, UISearchResultsUpdating, UISearchBarDelegate, FilterChangedListener { +final class TracksViewController: UITableViewController, OATrackSavingHelperUpdatableDelegate, TrackListUpdatableDelegate, OASelectTrackFolderDelegate, MapSettingsGpxViewControllerDelegate, MyPlacesSearchable, UISearchResultsUpdating, UISearchBarDelegate, FilterChangedListener, OrganizeTracksByDelegate, OrganizeByStepSizeDelegate { fileprivate var shouldReload = false @@ -30,7 +30,8 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda fileprivate var isRootFolder = true fileprivate var isVisibleOnMapFolder = false fileprivate var isSmartFolder = false - fileprivate var currentFolderPath = "" // in format: "rec/new folder" + fileprivate var currentFolderPath = "" + private var organizedGroup: OrganizedTracksGroup? // in format: "rec/new folder" fileprivate weak var hostVCDelegate: TrackListUpdatableDelegate? @@ -40,6 +41,7 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda private let visibleTracksKey = "visibleTracksKey" private let tracksFolderKey = "tracksFolderKey" private let tracksSmartFolderKey = "tracksSmartFolderKey" + private let organizedGroupKey = "organizedGroupKey" private let emptyFilterFolderKey = "emptyFilterFolderKey" private let emptySmartFolderKey = "emptySmartFolderKey" private let trackKey = "trackKey" @@ -94,7 +96,7 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda private var observers: [OAAutoObserverProxy] = [] private lazy var importHelper: OAGPXImportUIHelper = OAGPXImportUIHelper(hostViewController: self) - + private lazy var filterButton: UIButton = { var config = UIButton.Configuration.plain() config.imagePadding = 16 @@ -134,7 +136,7 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda super.init(coder: coder) } - init(isRootFolder: Bool) { + init(isRootFolder: Bool, isSmartFolder: Bool = false) { app = OsmAndApp.swiftInstance() settings = OAAppSettings.sharedManager() savingHelper = OASavingTrackHelper.sharedInstance() @@ -144,8 +146,9 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda routingHelper = OARoutingHelper.sharedInstance() gpxDB = OAGPXDatabase.sharedDb() smartFolderHelper = SharedLibSmartFolderHelper.shared - super.init(style: isRootFolder ? .insetGrouped : .grouped) + super.init(style: (isRootFolder || isSmartFolder) ? .insetGrouped : .grouped) self.isRootFolder = isRootFolder + self.isSmartFolder = isSmartFolder } convenience init(frame: CGRect, isRootFolder: Bool) { @@ -156,6 +159,7 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda private func onLoadFinished(folder: TrackFolder) { rootFolder = folder currentFolder = getTrackFolderByPath(currentFolderPath) ?? rootFolder + organizedGroup = nil onRefreshEnd() updateSearchResultsWithFilteredTracks() updateData() @@ -397,17 +401,52 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda } } - let gpxItems = isSmartFolder ? smartFolder.getTrackItems().compactMap { $0.dataItem } : currentTrackFolder.getTrackItems().compactMap { $0.dataItem } - if isSmartFolder && !isEditFilterActive && gpxItems.isEmpty { - let emptySmartFolderBannerRow = mainSection.createNewRow() - emptySmartFolderBannerRow.cellType = OALargeImageTitleDescrTableViewCell.reuseIdentifier - emptySmartFolderBannerRow.key = emptySmartFolderKey - emptySmartFolderBannerRow.title = localizedString("empty_smart_folder_title") - emptySmartFolderBannerRow.descr = localizedString("empty_smart_folder_descr") - emptySmartFolderBannerRow.iconName = "ic_custom_folder_open" - emptySmartFolderBannerRow.iconTintColor = .iconColorSecondary - emptySmartFolderBannerRow.setObj(localizedString("edit_filter"), forKey: buttonTitleKey) + if isSmartFolder { + if let group = organizedGroup { + let gpxItems = group.getTrackItems().compactMap { $0.dataItem } + let sortedTracks = TracksSortModeHelper.sortTracksWithMode(gpxItems, mode: sortMode) + for trackItem in sortedTracks { + createRowFor(track: trackItem, section: mainSection) + } + } else if !isEditFilterActive && smartFolder.organizeByParams != nil { + let groups = smartFolder.getOrganizedTrackItems() + if groups.isEmpty { + let gpxItems = smartFolder.getTrackItems().compactMap { $0.dataItem } + if gpxItems.isEmpty { + let emptySmartFolderBannerRow = mainSection.createNewRow() + emptySmartFolderBannerRow.cellType = OALargeImageTitleDescrTableViewCell.reuseIdentifier + emptySmartFolderBannerRow.key = emptySmartFolderKey + emptySmartFolderBannerRow.title = localizedString("empty_smart_folder_title") + emptySmartFolderBannerRow.descr = localizedString("empty_smart_folder_descr") + emptySmartFolderBannerRow.iconName = "ic_custom_folder_open" + emptySmartFolderBannerRow.iconTintColor = .iconColorSecondary + emptySmartFolderBannerRow.setObj(localizedString("edit_filter"), forKey: buttonTitleKey) + } else { + TracksSortModeHelper.sortTracksWithMode(gpxItems, mode: sortMode).forEach { createRowFor(track: $0, section: mainSection) } + } + } else { + let sortedGroups = sortOrganizedGroups(groups) + for group in sortedGroups { + createRowFor(organizedGroup: group, section: mainSection) + } + } + } else { + let gpxItems = smartFolder.getTrackItems().compactMap { $0.dataItem } + if !isEditFilterActive && gpxItems.isEmpty { + let emptySmartFolderBannerRow = mainSection.createNewRow() + emptySmartFolderBannerRow.cellType = OALargeImageTitleDescrTableViewCell.reuseIdentifier + emptySmartFolderBannerRow.key = emptySmartFolderKey + emptySmartFolderBannerRow.title = localizedString("empty_smart_folder_title") + emptySmartFolderBannerRow.descr = localizedString("empty_smart_folder_descr") + emptySmartFolderBannerRow.iconName = "ic_custom_folder_open" + emptySmartFolderBannerRow.iconTintColor = .iconColorSecondary + emptySmartFolderBannerRow.setObj(localizedString("edit_filter"), forKey: buttonTitleKey) + } else { + TracksSortModeHelper.sortTracksWithMode(gpxItems, mode: sortMode).forEach { createRowFor(track: $0, section: mainSection) } + } + } } else { + let gpxItems = currentTrackFolder.getTrackItems().compactMap { $0.dataItem } let sortedTracks = TracksSortModeHelper.sortTracksWithMode(gpxItems, mode: sortMode) for trackItem in sortedTracks { createRowFor(track: trackItem, section: mainSection) @@ -422,6 +461,10 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda } } + private func formattedTracksCount(_ count: Int) -> String { + count == 1 ? localizedString("folder_one_track") : String(format: localizedString("folder_tracks_count"), count) + } + fileprivate func createRowFor(folder: SortableFolder, section: OATableSectionData) { let folderRow = section.createNewRow() let folderName = folder.getDirName(includingSubdirs: false) @@ -440,10 +483,25 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda folderRow.iconName = "ic_custom_folder_smart" let tracksCount = smartFolder.getTrackItems().count folderRow.setObj(tracksCount, forKey: tracksCountKey) - folderRow.descr = String(format: localizedString("folder_tracks_count"), tracksCount) + folderRow.descr = formattedTracksCount(tracksCount) } } + private func createRowFor(organizedGroup: OrganizedTracksGroup, section: OATableSectionData) { + let row = section.createNewRow() + row.cellType = OASimpleTableViewCell.reuseIdentifier + row.key = organizedGroupKey + row.title = organizedGroup.getName() + row.descr = formattedTracksCount(organizedGroup.getTrackItems().count) + if organizedGroup.getType() == .activity { + row.icon = UIImage.mapSvgImageNamed("mx_\(organizedGroup.getIconName())") ?? .templateImageNamed("ic_custom_activity") + } else { + row.iconName = organizedGroup.getType().iconName + } + row.setObj(UIColor.iconColorActive, forKey: colorKey) + row.setObj(organizedGroup, forKey: organizedGroupKey) + } + fileprivate func createRowFor(track: GpxDataItem, section: OATableSectionData) { let trackRow = section.createNewRow() let fileName = track.gpxFileName @@ -477,6 +535,11 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda let doneBarButton = OABaseNavbarViewController.createRightNavbarButton(localizedString("shared_string_done"), icon: nil, color: .label, action: #selector(onNavbarDoneButtonClicked), target: self, menu: nil) navigationController?.navigationBar.topItem?.rightBarButtonItem = doneBarButton navigationItem.rightBarButtonItem = doneBarButton + } else if organizedGroup != nil { + hideBackButton(true) + let backButton = OABaseNavbarViewController.createRightNavbarButton(nil, icon: UIImage(systemName: "chevron.left"), color: .label, action: #selector(onNavbarOrganizedGroupBackButtonClicked), target: self, menu: nil) + navigationController?.navigationBar.topItem?.leftBarButtonItem = backButton + navigationItem.leftBarButtonItem = backButton } else { hideBackButton(false) navigationController?.navigationBar.topItem?.leftBarButtonItem = nil @@ -507,24 +570,27 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda } private func configureNavigationBarAppearance() { + let backgroundColor: UIColor = isSmartFolder ? .viewBg : .navBarBgColorPrimary + let titleColor: UIColor = isSmartFolder ? .textColorPrimary : .navBarTextColorPrimary + let appearance = UINavigationBarAppearance() appearance.configureWithOpaqueBackground() - appearance.backgroundColor = .navBarBgColorPrimary - appearance.shadowColor = .navBarBgColorPrimary - appearance.titleTextAttributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline), NSAttributedString.Key.foregroundColor: UIColor.navBarTextColorPrimary] - + appearance.backgroundColor = backgroundColor + appearance.shadowColor = backgroundColor + appearance.titleTextAttributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline), NSAttributedString.Key.foregroundColor: titleColor] + let blurAppearance = UINavigationBarAppearance() blurAppearance.backgroundEffect = UIBlurEffect(style: .regular) - blurAppearance.backgroundColor = .navBarBgColorPrimary - blurAppearance.shadowColor = .navBarBgColorPrimary - blurAppearance.titleTextAttributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline), NSAttributedString.Key.foregroundColor: UIColor.navBarTextColorPrimary] - + blurAppearance.backgroundColor = backgroundColor + blurAppearance.shadowColor = backgroundColor + blurAppearance.titleTextAttributes = [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .headline), NSAttributedString.Key.foregroundColor: titleColor] + navigationController?.navigationBar.standardAppearance = blurAppearance navigationController?.navigationBar.scrollEdgeAppearance = appearance if #available(iOS 26.0, *) { navigationController?.navigationBar.tintColor = .label } else { - navigationController?.navigationBar.tintColor = .navBarTextColorPrimary + navigationController?.navigationBar.tintColor = titleColor } navigationController?.navigationBar.prefersLargeTitles = false } @@ -557,7 +623,11 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda } else if isVisibleOnMapFolder { title = localizedString("tracks_on_map") } else if isSmartFolder { - title = isEditFilterActive ? localizedString("edit_filter") : smartFolder.getDirName(includingSubdirs: false) + if let organizedGroup { + title = organizedGroup.getName() + } else { + title = isEditFilterActive ? localizedString("edit_filter") : smartFolder.getDirName(includingSubdirs: false) + } } if !isRootFolder { @@ -571,15 +641,23 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda var menuActions: [UIMenuElement] = [] if !tableView.isEditing { if isSmartFolder { + let isGeneralView = organizedGroup == nil let refreshSmartFolderAction = UIAction(title: localizedString("shared_string_refresh"), image: .icCustomUpdate.resizedMenuImage()) { [weak self] _ in self?.onNavbarRefreshSmartFolderButtonClicked() } - let editFilterSmartFolderAction = UIAction(title: localizedString("edit_filter"), image: .icCustomParameters.resizedMenuImage()) { [weak self] _ in - self?.onNavbarEditFilterSmartFolderButtonClicked() - } let refreshSmartFolderActionWithDivider = UIMenu(title: "", options: .displayInline, children: [refreshSmartFolderAction]) - let editFilterSmartFolderActionWithDivider = UIMenu(title: "", options: .displayInline, children: [editFilterSmartFolderAction]) - menuActions.append(contentsOf: [refreshSmartFolderActionWithDivider, editFilterSmartFolderActionWithDivider]) + menuActions.append(refreshSmartFolderActionWithDivider) + if isGeneralView { + let editFilterSmartFolderAction = UIAction(title: localizedString("edit_filter"), image: .icCustomParameters.resizedMenuImage()) { [weak self] _ in + self?.onNavbarEditFilterSmartFolderButtonClicked() + } + let organizeByAction = UIAction(title: localizedString("organize_by"), image: .icCustomTracksOrganize.resizedMenuImage()) { [weak self] _ in + self?.onNavbarOrganizeBySmartFolderButtonClicked() + } + let editFilterSmartFolderActionWithDivider = UIMenu(title: "", options: .displayInline, children: [editFilterSmartFolderAction]) + let organizeByActionWithDivider = UIMenu(title: "", options: .displayInline, children: [organizeByAction]) + menuActions.append(contentsOf: [editFilterSmartFolderActionWithDivider, organizeByActionWithDivider]) + } } else { let addFolderAction = UIAction(title: localizedString("add_folder"), image: .icCustomFolderAddOutlined.resizedMenuImage()) { [weak self] _ in self?.onNavbarAddFolderButtonClicked() @@ -639,7 +717,7 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda if #available(iOS 26.0, *) { color = .label } else { - color = .navBarTextColorPrimary + color = isSmartFolder ? .textColorPrimary : .navBarTextColorPrimary } if let selectBarButton = OABaseNavbarViewController.createRightNavbarButton(localizedString("shared_string_select"), icon: nil, color: color, action: #selector(onNavbarSelectButtonClicked), target: self, menu: nil) { if #available(iOS 26.0, *) { @@ -654,7 +732,7 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda private func setupHeaderView() -> UIView? { let headerView = UIView(frame: .init(x: 0, y: 0, width: tableView.frame.width, height: 44)) - headerView.backgroundColor = isRootFolder ? .clear : .groupBg + headerView.backgroundColor = (isRootFolder || isSmartFolder) ? .clear : .groupBg headerView.addSubview(filterButton) headerView.addSubview(sortButton) filterButton.translatesAutoresizingMaskIntoConstraints = false @@ -737,7 +815,7 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda } let footer = OAUtilities.setupTableHeaderView(withText: getTotalTracksStatistics(), font: .preferredFont(forTextStyle: .footnote), textColor: .textColorSecondary, isBigTitle: false, parentViewWidth: view.frame.width) - footer.backgroundColor = isRootFolder ? .clear : .groupBg + footer.backgroundColor = (isRootFolder || isSmartFolder) ? .clear : .groupBg for subview in footer.subviews { if let label = subview as? UILabel { label.textAlignment = .center @@ -766,8 +844,12 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda private func getTotalTracksStatistics() -> String { let folderAnalysis: TrackFolderAnalysis if isSmartFolder { - guard let smartFolder = smartFolder else { return "" } - folderAnalysis = smartFolder.getFolderAnalysis() + if let organizedGroup { + folderAnalysis = organizedGroup.getFolderAnalysis() + } else { + guard let smartFolder = smartFolder else { return "" } + folderAnalysis = smartFolder.getFolderAnalysis() + } } else { guard let analysis = getTrackFolderByPath(currentFolderPath)?.getFolderAnalysis() else { return "" } folderAnalysis = analysis @@ -870,6 +952,10 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda } private func updateSearchController() { + if isSmartFolder { + searchController.searchBar.searchTextField.placeholder = localizedString("search_activity") + return + } if isNameFiltered { searchController.searchBar.searchTextField.attributedPlaceholder = NSAttributedString(string: localizedString("search_activity"), attributes: [NSAttributedString.Key.foregroundColor: UIColor.textColorTertiary]) searchController.searchBar.searchTextField.backgroundColor = .groupBg @@ -911,12 +997,75 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda return rootFolder.getFlattenedSubFolders().first(where: { $0.getDirFile().path().hasSuffix(path) }) ?? rootFolder } + func onOrganizeByParamsApplied() { + reloadAfterOrganizeByChange() + } + + func onOrganizeByRangeParamsApplied(type: OrganizeByType, originalParams: OrganizeByParams?) { + reloadAfterOrganizeByChange() + let vc = OrganizeByStepSizeViewController(smartFolder: smartFolder, type: type, originalParams: originalParams) + vc.stepDelegate = self + showMediumSheetViewController(viewController: vc, isLargeAvailable: true) + } + + func onStepSizeChanged() { + reloadAfterOrganizeByChange() + } + // MARK: - Navbar Toolbar Actions - + + private func enterOrganizedGroup(_ group: OrganizedTracksGroup) { + organizedGroup = group + updateNavigationBarTitle() + setupNavbar() + updateSortButtonAndMenu() + generateData() + tableView.reloadData() + } + + private func leaveOrganizedGroup() { + organizedGroup = nil + updateNavigationBarTitle() + setupNavbar() + updateSortButtonAndMenu() + generateData() + tableView.reloadData() + } + private func onNavbarRefreshSmartFolderButtonClicked() { smartFolderHelper.refreshSmartFolder(smartFolder: smartFolder) reloadTracks(forceLoad: true) } + + private func onNavbarOrganizeBySmartFolderButtonClicked() { + let vc = OrganizeTracksByViewController(smartFolder: smartFolder) + vc.delegate = self + let nav = UINavigationController(rootViewController: vc) + nav.modalPresentationStyle = .fullScreen + present(nav, animated: true) + } + + private func reloadAfterOrganizeByChange() { + if let name = smartFolder?.getName(), let refreshed = smartFolderHelper.getSmartFolder(name: name) { + smartFolder = refreshed + } + resetSortModeForOrganizeByChange() + smartFolderHelper.refreshSmartFolder(smartFolder: smartFolder) + updateData() + } + + private func resetSortModeForOrganizeByChange() { + let newMode: TracksSortMode + if smartFolder.organizeByParams != nil { + let isNumeric = smartFolder?.getOrganizeByType()?.getTrackSortScope() == TracksSortScope.organizedByValue + newMode = isNumeric ? .shortestDistanceFirst : .nameAZ + } else { + newMode = TracksSortModeHelper.getDefaultSortMode(for: smartFolder.getId()) + } + setTracksSortMode(newMode, isSortingSubfolders: false) + sortMode = newMode + updateSortButtonAndMenu() + } @objc private func onNavbarEditFilterSmartFolderButtonClicked() { isEditFilterActive = true @@ -1156,6 +1305,10 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda } } + @objc private func onNavbarOrganizedGroupBackButtonClicked() { + leaveOrganizedGroup() + } + @objc private func onNavbarCancelButtonClicked() { selectedTracks.removeAll() selectedFolders.removeAll() @@ -1909,12 +2062,11 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda private func showTracksViewControllerForSmartFolder(withName name: String) { let smartFolder = smartFolderHelper.getSmartFolder(name: name) - let vc = TracksViewController(isRootFolder: false) + let vc = TracksViewController(isRootFolder: false, isSmartFolder: true) vc.smartFolder = smartFolder vc.rootFolder = rootFolder vc.currentFolder = currentFolder vc.visibleTracksFolder = visibleTracksFolder - vc.isSmartFolder = true vc.hostVCDelegate = self show(vc) } @@ -2040,7 +2192,9 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda cell.descriptionLabel.text = item.descr } cell.accessoryType = tableView.isEditing ? .none : .disclosureIndicator - if let iconName = item.iconName { + if let icon = item.icon { + cell.leftIconView.image = icon + } else if let iconName = item.iconName { cell.leftIconView.image = UIImage.templateImageNamed(iconName) } else { cell.leftIconView.image = nil @@ -2143,6 +2297,10 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda } else if item.key == tracksSmartFolderKey { guard let title = item.title else { return } showTracksViewControllerForSmartFolder(withName: title) + } else if item.key == organizedGroupKey { + if let group = item.obj(forKey: organizedGroupKey) as? OrganizedTracksGroup { + enterOrganizedGroup(group) + } } else if item.key == trackKey { if let trackPath = item.obj(forKey: pathKey) as? String, let track = rootFolder.getFlattenedTrackItems().first(where: { $0.gpxFilePath == trackPath }), @@ -2244,6 +2402,33 @@ final class TracksViewController: UITableViewController, OATrackSavingHelperUpda return UIMenu(title: "", image: nil, children: [secondButtonsSection, thirdButtonsSection, lastButtonsSection]) } return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: menuProvider) + } else if item.key == organizedGroupKey { + guard let group = item.obj(forKey: organizedGroupKey) as? OrganizedTracksGroup else { return nil } + let menuProvider: UIContextMenuActionProvider = { [weak self] _ in + guard let self else { return nil } + let showOnMapAction = UIAction(title: localizedString("show_all_tracks"), image: .icCustomMapPinOutlined.resizedMenuImage()) { [weak self] _ in + guard let self else { return } + let tracksToShow = group.getTrackItems().compactMap { $0.gpxFilePath }.filter { !self.settings.mapSettingVisibleGpx.contains($0) } + if !tracksToShow.isEmpty { + self.settings.showGpx(tracksToShow, update: true) + } + if let vc = MapSettingsGpxViewController() { + vc.delegate = self + show(vc) + } + } + let exportAction = UIAction(title: localizedString("shared_string_export"), image: .icCustomExportOutlined.resizedMenuImage()) { [weak self] _ in + guard let self else { return } + let exportFilePaths = group.getTrackItems().compactMap { $0.path } + let vc = OAExportItemsViewController(tracks: exportFilePaths) + self.navigationController?.pushViewController(vc, animated: true) + } + let mapSection = UIMenu(title: "", options: .displayInline, children: [showOnMapAction]) + let exportSection = UIMenu(title: "", options: .displayInline, children: [exportAction]) + let composedMenu = UIMenu(title: "", image: nil, children: [mapSection, exportSection]) + return composedMenu + } + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: menuProvider) } else if item.key == trackKey || item.key == recordingTrackKey { guard !isEditFilterActive else { return nil } let isCurrentTrack = item.key == recordingTrackKey @@ -2500,7 +2685,74 @@ extension TracksViewController: TrackFolderLoaderTaskLoadTracksListener { } extension TracksViewController { + private func isShowingOrganizedGroups() -> Bool { + isSmartFolder && organizedGroup == nil && smartFolder?.organizeByParams != nil + } + + private func sortOrganizedGroups(_ groups: [OrganizedTracksGroup]) -> [OrganizedTracksGroup] { + switch sortMode { + case .nameAZ: + return groups.sorted { $0.getName().localizedCaseInsensitiveCompare($1.getName()) == .orderedAscending } + case .nameZA: + return groups.sorted { $0.getName().localizedCaseInsensitiveCompare($1.getName()) == .orderedDescending } + case .longestDistanceFirst: + return groups.sorted { $0.getComparisonValue() > $1.getComparisonValue() } + case .shortestDistanceFirst: + return groups.sorted { $0.getComparisonValue() < $1.getComparisonValue() } + default: + return groups + } + } + + private func createOrganizedGroupsSortMenu() -> UIMenu { + let isNumeric = smartFolder?.getOrganizeByType()?.getTrackSortScope() == TracksSortScope.organizedByValue + + if isNumeric { + let highestAction = UIAction( + title: localizedString("sort_highest_first"), + image: .icCustomSortLongToShort.resizedMenuImage(), + state: sortMode == .longestDistanceFirst ? .on : .off + ) { [weak self] _ in + self?.applySortModeForGroups(.longestDistanceFirst) + } + let lowestAction = UIAction( + title: localizedString("sort_lowest_first"), + image: .icCustomSortShortToLong.resizedMenuImage(), + state: sortMode == .shortestDistanceFirst ? .on : .off + ) { [weak self] _ in + self?.applySortModeForGroups(.shortestDistanceFirst) + } + return UIMenu(title: "", children: [highestAction, lowestAction]) + } else { + let nameAZAction = UIAction( + title: localizedString("track_sort_az"), + image: TracksSortMode.nameAZ.image?.resizedMenuImage(), + state: sortMode == .nameAZ ? .on : .off + ) { [weak self] _ in + self?.applySortModeForGroups(.nameAZ) + } + let nameZAAction = UIAction( + title: localizedString("track_sort_za"), + image: TracksSortMode.nameZA.image?.resizedMenuImage(), + state: sortMode == .nameZA ? .on : .off + ) { [weak self] _ in + self?.applySortModeForGroups(.nameZA) + } + return UIMenu(title: "", children: [nameAZAction, nameZAAction]) + } + } + + private func applySortModeForGroups(_ mode: TracksSortMode) { + setTracksSortMode(mode, isSortingSubfolders: false) + sortMode = getTracksSortMode() + updateSortButtonAndMenu() + updateData() + } + private func createSortMenu(isSortingSubfolders: Bool) -> UIMenu { + if !isSortingSubfolders && isShowingOrganizedGroups() { + return createOrganizedGroupsSortMenu() + } let sortingOptions = UIMenu(options: .displayInline, children: [ createAction(for: .nearest, isSortingSubfolders: isSortingSubfolders), createAction(for: .lastModified, isSortingSubfolders: isSortingSubfolders) @@ -2551,8 +2803,21 @@ extension TracksViewController { } } + private func sortButtonImage() -> UIImage? { + if isSearchActive || isSelectionModeInSearch { return sortModeForSearch.image } + if isShowingOrganizedGroups() { + switch sortMode { + case .longestDistanceFirst: return .icCustomSortLongToShort + case .shortestDistanceFirst: return .icCustomSortShortToLong + case .nameZA: return TracksSortMode.nameZA.image + default: return TracksSortMode.nameAZ.image + } + } + return sortMode.image + } + private func updateSortButtonAndMenu() { - sortButton.setImage(isSearchActive || isSelectionModeInSearch ? sortModeForSearch.image : sortMode.image, for: .normal) + sortButton.setImage(sortButtonImage(), for: .normal) sortButton.menu = createSortMenu(isSortingSubfolders: false) } } diff --git a/Sources/Controllers/Settings/OsmandDevelopment/OAOsmandDevelopmentViewController.mm b/Sources/Controllers/Settings/OsmandDevelopment/OAOsmandDevelopmentViewController.mm index 346adec194..969325ace4 100644 --- a/Sources/Controllers/Settings/OsmandDevelopment/OAOsmandDevelopmentViewController.mm +++ b/Sources/Controllers/Settings/OsmandDevelopment/OAOsmandDevelopmentViewController.mm @@ -279,7 +279,7 @@ - (void)onSwitchPressed:(UISwitch *)sender else if ([item.key isEqualToString:kShowTouchesKey]) { [[OAAppSettings sharedManager].showTouches set:sender.isOn]; - [[OATouchIndicatorController shared] apply]; + [[TouchIndicatorController shared] apply]; } else if ([item.key isEqualToString:kSimulateOBDDataKey]) { [[OAAppSettings sharedManager].simulateOBDData set:sender.isOn]; diff --git a/Sources/Controllers/Settings/OsmandDevelopment/OATouchIndicatorController.swift b/Sources/Controllers/Settings/OsmandDevelopment/TouchIndicatorController.swift similarity index 98% rename from Sources/Controllers/Settings/OsmandDevelopment/OATouchIndicatorController.swift rename to Sources/Controllers/Settings/OsmandDevelopment/TouchIndicatorController.swift index 746905ef32..846720ca5d 100644 --- a/Sources/Controllers/Settings/OsmandDevelopment/OATouchIndicatorController.swift +++ b/Sources/Controllers/Settings/OsmandDevelopment/TouchIndicatorController.swift @@ -8,7 +8,7 @@ import UIKit @objcMembers -final class OATouchIndicatorController: NSObject, UIGestureRecognizerDelegate { +final class TouchIndicatorController: NSObject, UIGestureRecognizerDelegate { // MARK: - Subtypes @@ -120,7 +120,7 @@ final class OATouchIndicatorController: NSObject, UIGestureRecognizerDelegate { // MARK: - Type Properties - static let shared = OATouchIndicatorController() + static let shared = TouchIndicatorController() // MARK: - Instance Properties diff --git a/Sources/Helpers/SharedLibHelpers/OrganizeByTypeExtension.swift b/Sources/Helpers/SharedLibHelpers/OrganizeByTypeExtension.swift new file mode 100644 index 0000000000..9c10ee7fba --- /dev/null +++ b/Sources/Helpers/SharedLibHelpers/OrganizeByTypeExtension.swift @@ -0,0 +1,44 @@ +import UIKit +import OsmAndShared + +extension OrganizeByType { + var isLockedBehindPro: Bool { + switch self { + case .length: return false + default: return isPro + } + } + + var iconName: String { + switch self { + case .activity: return "ic_custom_activity" + case .duration: return "ic_custom_time_span" + case .timeInMotion: return "ic_custom_time_in_motion" + case .length: return "ic_custom_length" + case .yearOfCreation: return "ic_custom_calendar_month" + case .monthAndYear: return "ic_custom_calendar_month" + case .nearestCity: return "ic_custom_city" + case .maxSpeed: return "ic_custom_speed_max" + case .avgSpeed: return "ic_custom_speed_average" + case .maxAltitude: return "ic_custom_altitude_max" + case .avgAltitude: return "ic_custom_altitude_average" + case .uphill: return "ic_custom_uphill" + case .downhill: return "ic_custom_downhill" + case .sensorSpeedMax: return "ic_custom_sensor_speed_outlined" + case .sensorSpeedAvg: return "ic_custom_sensor_speed_outlined" + case .heartRateMax: return "ic_custom_sensor_heart_rate_outlined" + case .heartRateAvg: return "ic_custom_sensor_heart_rate_outlined" + case .cadenceMax: return "ic_custom_sensor_cadence_outlined" + case .cadenceAvg: return "ic_custom_sensor_cadence_outlined" + case .powerMax: return "ic_custom_sensor_bicycle_power_outlined" + case .powerAvg: return "ic_custom_sensor_bicycle_power_outlined" + case .tempMax: return "ic_custom_sensor_thermometer" + case .tempAvg: return "ic_custom_sensor_thermometer" + default: return "ic_custom_info_outlined" + } + } + + var image: UIImage? { + .templateImageNamed(iconName) + } +} diff --git a/Sources/Plugins/Development/OAOsmandDevelopmentPlugin.mm b/Sources/Plugins/Development/OAOsmandDevelopmentPlugin.mm index 384217d1b3..296de89bbe 100644 --- a/Sources/Plugins/Development/OAOsmandDevelopmentPlugin.mm +++ b/Sources/Plugins/Development/OAOsmandDevelopmentPlugin.mm @@ -51,7 +51,7 @@ - (instancetype) init [OAWidgetsAvailabilityHelper regWidgetVisibilityWithWidgetType:OAWidgetType.devZoomLevel appModes:@[]]; [OAWidgetsAvailabilityHelper regWidgetVisibilityWithWidgetType:OAWidgetType.devTargetDistance appModes:@[]]; - [[OATouchIndicatorController shared] apply]; + [[TouchIndicatorController shared] apply]; } return self; }