diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index c61bf7bd16..109d907394 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -219,6 +219,9 @@ 271A62052E3CA8CD00B34CB1 /* DistanceByTapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 271A62042E3CA8CD00B34CB1 /* DistanceByTapViewController.swift */; }; 27291B612EE81169005D0B0A /* PreviewImageViewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27291B5F2EE81169005D0B0A /* PreviewImageViewTableViewCell.swift */; }; 27291B622EE81169005D0B0A /* PreviewImageViewTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27291B602EE81169005D0B0A /* PreviewImageViewTableViewCell.xib */; }; + 272AFB9B2FD2DD3C006C2E21 /* OAFavoritesBridgeHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesBridgeHelper.mm */; }; + 272AFBAC2FD810CC006C2E21 /* EmptyStateCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 272AFBAA2FD810CC006C2E21 /* EmptyStateCollectionViewCell.swift */; }; + 272AFBAD2FD810CC006C2E21 /* EmptyStateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 272AFBAB2FD810CC006C2E21 /* EmptyStateCollectionViewCell.xib */; }; 274167472E4DD3660051DD4B /* BaseWidgetView+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274167462E4DD3660051DD4B /* BaseWidgetView+Extension.swift */; }; 274167492E4DF5840051DD4B /* TopTextViewState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 274167482E4DF5840051DD4B /* TopTextViewState.swift */; }; 2745FEF72F3A1207004F6AB4 /* PreviewImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2745FEF62F3A1207004F6AB4 /* PreviewImageView.swift */; }; @@ -229,6 +232,8 @@ 2758DD522E5CBABD00051096 /* MainExternalInputDeviceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758DD512E5CBABD00051096 /* MainExternalInputDeviceViewController.swift */; }; 2758DD572E5EFAD700051096 /* EditKeyAssignmentController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758DD562E5EFAD700051096 /* EditKeyAssignmentController.swift */; }; 2758DD592E5F05F000051096 /* KeyAssignment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2758DD582E5F05F000051096 /* KeyAssignment.swift */; }; + 275ED65B2FE032600088D42B /* OAFavoriteFolderBridgeItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 275ED65A2FE032600088D42B /* OAFavoriteFolderBridgeItem.mm */; }; + 275ED65E2FE032C80088D42B /* OAFavoritePointBridgeItem.mm in Sources */ = {isa = PBXBuildFile; fileRef = 275ED65D2FE032C80088D42B /* OAFavoritePointBridgeItem.mm */; }; 275FE9992ED5A748007E4945 /* ProfileAppearanceUpdateSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 275FE9982ED5A748007E4945 /* ProfileAppearanceUpdateSize.swift */; }; 2765D1072FC0953B00D612A3 /* MyPlacesSortModeHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2765D1062FC0953B00D612A3 /* MyPlacesSortModeHelper.swift */; }; 276C9D162EF152C7005ABD35 /* MapButtonAppearanceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 276C9D152EF152C7005ABD35 /* MapButtonAppearanceViewController.swift */; }; @@ -301,6 +306,8 @@ 27F050952DD38E400058075F /* RouteInfoWidgetState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F050942DD38E400058075F /* RouteInfoWidgetState.swift */; }; 27F050992DD4B6080058075F /* RouteInfoCalculator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F050982DD4B6060058075F /* RouteInfoCalculator.swift */; }; 27F0509F2DD4DEB30058075F /* RouteInfoWidget.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27F0509E2DD4DEB30058075F /* RouteInfoWidget.xib */; }; + 27F055AA2FE2EB860025F578 /* StatsFooterCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F055A92FE2EB860025F578 /* StatsFooterCollectionViewCell.swift */; }; + 27F055AC2FE2F76F0025F578 /* BackupBannerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27F055AB2FE2F76F0025F578 /* BackupBannerCollectionViewCell.swift */; }; 27FA9F572F45B9040064B5BE /* MapUnderlayAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FA9F562F45B9040064B5BE /* MapUnderlayAction.swift */; }; 27FB159B2EF307DC0054F4DC /* ButtonAppearanceParams.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FB159A2EF307DC0054F4DC /* ButtonAppearanceParams.swift */; }; 27FD5F102EB12A8F002114F7 /* ProfileAppearanceIconSizeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27FD5F0F2EB12A8F002114F7 /* ProfileAppearanceIconSizeViewController.swift */; }; @@ -1577,6 +1584,7 @@ C589BCA12C6105F7002769F6 /* OATwoIconsButtonTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = C589BCA02C6105F7002769F6 /* OATwoIconsButtonTableViewCell.xib */; }; C58A6A492F68920800DCE4F6 /* MapSettingsBuildings3DScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58A6A482F68920800DCE4F6 /* MapSettingsBuildings3DScreen.swift */; }; C58A6A4D2F69632800DCE4F6 /* Buildings3DColorType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58A6A4C2F69632800DCE4F6 /* Buildings3DColorType.swift */; }; + C58ACDE12FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */; }; C596D5832E8D634300D13D93 /* AutoZoom3DAngleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C596D5822E8D634300D13D93 /* AutoZoom3DAngleViewController.swift */; }; C5983E132EF93B51003F2639 /* MapSettingsWikipediaScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5983E122EF93B51003F2639 /* MapSettingsWikipediaScreen.swift */; }; C5983E1C2EFD6B75003F2639 /* WikipediaLanguagesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5983E1B2EFD6B75003F2639 /* WikipediaLanguagesViewController.swift */; }; @@ -1633,6 +1641,13 @@ C5E202952A2102D800BFC32C /* ic_navbar_add@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = C5E202932A2102D800BFC32C /* ic_navbar_add@2x.png */; }; C5E3B59F2DDDC40800083695 /* SelectRouteActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5E3B59E2DDDC40800083695 /* SelectRouteActivityViewController.swift */; }; C5E92A492F5720D8001DE758 /* StatisticsSelectionBottomSheetViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5E92A482F5720D8001DE758 /* StatisticsSelectionBottomSheetViewController.swift */; }; + C5EB56EE2FD18B8A00D01657 /* FavoriteListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */; }; + C5EB56F12FD18B8A00D01657 /* FavoriteListViewController+Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56F02FD18B8A00D01657 /* FavoriteListViewController+Models.swift */; }; + C5EB56F32FD18B8A00D01657 /* FavoriteListViewController+DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56F22FD18B8A00D01657 /* FavoriteListViewController+DataSource.swift */; }; + C5EB56F52FD18B8A00D01657 /* FavoriteListViewController+Cells.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56F42FD18B8A00D01657 /* FavoriteListViewController+Cells.swift */; }; + C5EB56F72FD18B8A00D01657 /* FavoriteListViewController+ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56F62FD18B8A00D01657 /* FavoriteListViewController+ContextMenu.swift */; }; + C5EB56F92FD18B8A00D01657 /* FavoriteListViewController+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56F82FD18B8A00D01657 /* FavoriteListViewController+Actions.swift */; }; + C5EB56FB2FD18B8A00D01657 /* FavoriteListViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56FA2FD18B8A00D01657 /* FavoriteListViewController+Delegates.swift */; }; C5EB72C42BD12F4D00C50C23 /* RouteParameterDevelopmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB72C32BD12F4C00C50C23 /* RouteParameterDevelopmentViewController.swift */; }; C5EE465B2E9D1F8B0019C9E0 /* CarPlayNavigationModeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE465A2E9D1F8B0019C9E0 /* CarPlayNavigationModeProvider.swift */; }; C9DC5FD49885FD738343EB13 /* libPods-OsmAnd Maps.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CC99B1D5FA05EDFE17BF38B8 /* libPods-OsmAnd Maps.a */; }; @@ -2568,7 +2583,6 @@ DA5A83AE26C563A800F274C7 /* OASubscriptionCancelViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DA5A7DBD26C563A300F274C7 /* OASubscriptionCancelViewController.xib */; }; DA5A83B026C563A800F274C7 /* OASubscriptionCancelViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7DBF26C563A300F274C7 /* OASubscriptionCancelViewController.mm */; }; DA5A83B126C563A800F274C7 /* OAWebViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7DC026C563A300F274C7 /* OAWebViewController.m */; }; - DA5A83B226C563A800F274C7 /* MyPlaces.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DA5A7DC426C563A300F274C7 /* MyPlaces.storyboard */; }; DA5A83B526C563A800F274C7 /* OAActionAddProfileViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7DCB26C563A300F274C7 /* OAActionAddProfileViewController.m */; }; DA5A83B626C563A800F274C7 /* OAActionAddCategoryViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7DCD26C563A300F274C7 /* OAActionAddCategoryViewController.mm */; }; DA5A83B726C563A800F274C7 /* OAFloatingButtonsHudViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7DCE26C563A300F274C7 /* OAFloatingButtonsHudViewController.mm */; }; @@ -2597,7 +2611,6 @@ DA5A83D626C563A800F274C7 /* OAReplaceFavoriteViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7E0026C563A300F274C7 /* OAReplaceFavoriteViewController.mm */; }; DA5A83D926C563A800F274C7 /* OAFavoriteImportViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7E0426C563A300F274C7 /* OAFavoriteImportViewController.mm */; }; DA5A83DB26C563A800F274C7 /* OASelectFavoriteGroupViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7E0726C563A300F274C7 /* OASelectFavoriteGroupViewController.mm */; }; - DA5A83DD26C563A800F274C7 /* OAFavoriteListViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7E0C26C563A300F274C7 /* OAFavoriteListViewController.mm */; }; DA5A83DF26C563A800F274C7 /* OAEditGroupViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7E1026C563A300F274C7 /* OAEditGroupViewController.mm */; }; DA5A83E026C563A800F274C7 /* OAEditDescriptionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DA5A7E1126C563A300F274C7 /* OAEditDescriptionViewController.xib */; }; DA5A83E226C563A800F274C7 /* OAEditColorViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DA5A7E1426C563A300F274C7 /* OAEditColorViewController.m */; }; @@ -3796,6 +3809,10 @@ 271A62042E3CA8CD00B34CB1 /* DistanceByTapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DistanceByTapViewController.swift; sourceTree = ""; }; 27291B5F2EE81169005D0B0A /* PreviewImageViewTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewImageViewTableViewCell.swift; sourceTree = ""; }; 27291B602EE81169005D0B0A /* PreviewImageViewTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PreviewImageViewTableViewCell.xib; sourceTree = ""; }; + 272AFB972FD2DC42006C2E21 /* OAFavoritesBridgeHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OAFavoritesBridgeHelper.h; sourceTree = ""; }; + 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesBridgeHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OAFavoritesBridgeHelper.mm; sourceTree = ""; }; + 272AFBAA2FD810CC006C2E21 /* EmptyStateCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyStateCollectionViewCell.swift; sourceTree = ""; }; + 272AFBAB2FD810CC006C2E21 /* EmptyStateCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = EmptyStateCollectionViewCell.xib; sourceTree = ""; }; 274167462E4DD3660051DD4B /* BaseWidgetView+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BaseWidgetView+Extension.swift"; sourceTree = ""; }; 274167482E4DF5840051DD4B /* TopTextViewState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopTextViewState.swift; sourceTree = ""; }; 2745FEF62F3A1207004F6AB4 /* PreviewImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewImageView.swift; sourceTree = ""; }; @@ -3806,6 +3823,10 @@ 2758DD512E5CBABD00051096 /* MainExternalInputDeviceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainExternalInputDeviceViewController.swift; sourceTree = ""; }; 2758DD562E5EFAD700051096 /* EditKeyAssignmentController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditKeyAssignmentController.swift; sourceTree = ""; }; 2758DD582E5F05F000051096 /* KeyAssignment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyAssignment.swift; sourceTree = ""; }; + 275ED6592FE032290088D42B /* OAFavoriteFolderBridgeItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OAFavoriteFolderBridgeItem.h; sourceTree = ""; }; + 275ED65A2FE032600088D42B /* OAFavoriteFolderBridgeItem.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OAFavoriteFolderBridgeItem.mm; sourceTree = ""; }; + 275ED65C2FE032A90088D42B /* OAFavoritePointBridgeItem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OAFavoritePointBridgeItem.h; sourceTree = ""; }; + 275ED65D2FE032C80088D42B /* OAFavoritePointBridgeItem.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OAFavoritePointBridgeItem.mm; sourceTree = ""; }; 275FE9982ED5A748007E4945 /* ProfileAppearanceUpdateSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileAppearanceUpdateSize.swift; sourceTree = ""; }; 2765D1062FC0953B00D612A3 /* MyPlacesSortModeHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyPlacesSortModeHelper.swift; sourceTree = ""; }; 276C9D152EF152C7005ABD35 /* MapButtonAppearanceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapButtonAppearanceViewController.swift; sourceTree = ""; }; @@ -3879,6 +3900,8 @@ 27F050942DD38E400058075F /* RouteInfoWidgetState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteInfoWidgetState.swift; sourceTree = ""; }; 27F050982DD4B6060058075F /* RouteInfoCalculator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteInfoCalculator.swift; sourceTree = ""; }; 27F0509E2DD4DEB30058075F /* RouteInfoWidget.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RouteInfoWidget.xib; sourceTree = ""; }; + 27F055A92FE2EB860025F578 /* StatsFooterCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsFooterCollectionViewCell.swift; sourceTree = ""; }; + 27F055AB2FE2F76F0025F578 /* BackupBannerCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupBannerCollectionViewCell.swift; sourceTree = ""; }; 27FA9F562F45B9040064B5BE /* MapUnderlayAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapUnderlayAction.swift; sourceTree = ""; }; 27FB159A2EF307DC0054F4DC /* ButtonAppearanceParams.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonAppearanceParams.swift; sourceTree = ""; }; 27FD5F0F2EB12A8F002114F7 /* ProfileAppearanceIconSizeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileAppearanceIconSizeViewController.swift; sourceTree = ""; }; @@ -5538,6 +5561,7 @@ C589BCA02C6105F7002769F6 /* OATwoIconsButtonTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = OATwoIconsButtonTableViewCell.xib; sourceTree = ""; }; C58A6A482F68920800DCE4F6 /* MapSettingsBuildings3DScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSettingsBuildings3DScreen.swift; sourceTree = ""; }; C58A6A4C2F69632800DCE4F6 /* Buildings3DColorType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buildings3DColorType.swift; sourceTree = ""; }; + C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteSortModeHelper.swift; sourceTree = ""; }; C596D5822E8D634300D13D93 /* AutoZoom3DAngleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoZoom3DAngleViewController.swift; sourceTree = ""; }; C5983E122EF93B51003F2639 /* MapSettingsWikipediaScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapSettingsWikipediaScreen.swift; sourceTree = ""; }; C5983E1B2EFD6B75003F2639 /* WikipediaLanguagesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WikipediaLanguagesViewController.swift; sourceTree = ""; }; @@ -5598,6 +5622,13 @@ C5E202932A2102D800BFC32C /* ic_navbar_add@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_navbar_add@2x.png"; path = "Resources/Icons/ic_navbar_add@2x.png"; sourceTree = ""; }; C5E3B59E2DDDC40800083695 /* SelectRouteActivityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectRouteActivityViewController.swift; sourceTree = ""; }; C5E92A482F5720D8001DE758 /* StatisticsSelectionBottomSheetViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsSelectionBottomSheetViewController.swift; sourceTree = ""; }; + C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteListViewController.swift; sourceTree = ""; }; + C5EB56F02FD18B8A00D01657 /* FavoriteListViewController+Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteListViewController+Models.swift"; sourceTree = ""; }; + C5EB56F22FD18B8A00D01657 /* FavoriteListViewController+DataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteListViewController+DataSource.swift"; sourceTree = ""; }; + C5EB56F42FD18B8A00D01657 /* FavoriteListViewController+Cells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteListViewController+Cells.swift"; sourceTree = ""; }; + C5EB56F62FD18B8A00D01657 /* FavoriteListViewController+ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteListViewController+ContextMenu.swift"; sourceTree = ""; }; + C5EB56F82FD18B8A00D01657 /* FavoriteListViewController+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteListViewController+Actions.swift"; sourceTree = ""; }; + C5EB56FA2FD18B8A00D01657 /* FavoriteListViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FavoriteListViewController+Delegates.swift"; sourceTree = ""; }; C5EB72C32BD12F4C00C50C23 /* RouteParameterDevelopmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteParameterDevelopmentViewController.swift; sourceTree = ""; }; C5EE465A2E9D1F8B0019C9E0 /* CarPlayNavigationModeProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarPlayNavigationModeProvider.swift; sourceTree = ""; }; CA4ED0221B1888DD1A891DAE /* OAPlanTypeCardRow.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OAPlanTypeCardRow.mm; sourceTree = ""; }; @@ -7032,7 +7063,6 @@ DA5A7DBF26C563A300F274C7 /* OASubscriptionCancelViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OASubscriptionCancelViewController.mm; sourceTree = ""; }; DA5A7DC026C563A300F274C7 /* OAWebViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OAWebViewController.m; sourceTree = ""; }; DA5A7DC126C563A300F274C7 /* OASubscriptionCancelViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OASubscriptionCancelViewController.h; sourceTree = ""; }; - DA5A7DC426C563A300F274C7 /* MyPlaces.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = MyPlaces.storyboard; sourceTree = ""; }; DA5A7DC826C563A300F274C7 /* OASuperViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OASuperViewController.h; sourceTree = ""; }; DA5A7DC926C563A300F274C7 /* OABaseScrollableHudViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OABaseScrollableHudViewController.h; sourceTree = ""; }; DA5A7DCB26C563A300F274C7 /* OAActionAddProfileViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OAActionAddProfileViewController.m; sourceTree = ""; }; @@ -7076,13 +7106,11 @@ DA5A7DFC26C563A300F274C7 /* OATargetDestinationViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OATargetDestinationViewController.m; sourceTree = ""; }; DA5A7DFD26C563A300F274C7 /* OATargetDestinationViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OATargetDestinationViewController.xib; sourceTree = ""; }; DA5A7E0026C563A300F274C7 /* OAReplaceFavoriteViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OAReplaceFavoriteViewController.mm; sourceTree = ""; }; - DA5A7E0126C563A300F274C7 /* OAFavoriteListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OAFavoriteListViewController.h; sourceTree = ""; }; DA5A7E0426C563A300F274C7 /* OAFavoriteImportViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OAFavoriteImportViewController.mm; sourceTree = ""; }; DA5A7E0726C563A300F274C7 /* OASelectFavoriteGroupViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OASelectFavoriteGroupViewController.mm; sourceTree = ""; }; DA5A7E0826C563A300F274C7 /* OASelectFavoriteGroupViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OASelectFavoriteGroupViewController.h; sourceTree = ""; }; DA5A7E0926C563A300F274C7 /* OAFavoriteImportViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OAFavoriteImportViewController.h; sourceTree = ""; }; DA5A7E0A26C563A300F274C7 /* OAReplaceFavoriteViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OAReplaceFavoriteViewController.h; sourceTree = ""; }; - DA5A7E0C26C563A300F274C7 /* OAFavoriteListViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OAFavoriteListViewController.mm; sourceTree = ""; }; DA5A7E0D26C563A300F274C7 /* OARouteBaseViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OARouteBaseViewController.h; sourceTree = ""; }; DA5A7E1026C563A300F274C7 /* OAEditGroupViewController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = OAEditGroupViewController.mm; sourceTree = ""; }; DA5A7E1126C563A300F274C7 /* OAEditDescriptionViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = OAEditDescriptionViewController.xib; sourceTree = ""; }; @@ -8498,6 +8526,27 @@ path = ExternalInputDevice; sourceTree = ""; }; + 275ED6502FDC5AA60088D42B /* FavoriteListViewController */ = { + isa = PBXGroup; + children = ( + C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */, + C5EB56F02FD18B8A00D01657 /* FavoriteListViewController+Models.swift */, + C5EB56F22FD18B8A00D01657 /* FavoriteListViewController+DataSource.swift */, + C5EB56F42FD18B8A00D01657 /* FavoriteListViewController+Cells.swift */, + C5EB56F62FD18B8A00D01657 /* FavoriteListViewController+ContextMenu.swift */, + C5EB56F82FD18B8A00D01657 /* FavoriteListViewController+Actions.swift */, + C5EB56FA2FD18B8A00D01657 /* FavoriteListViewController+Delegates.swift */, + C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */, + 272AFB972FD2DC42006C2E21 /* OAFavoritesBridgeHelper.h */, + 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesBridgeHelper.mm */, + 275ED6592FE032290088D42B /* OAFavoriteFolderBridgeItem.h */, + 275ED65A2FE032600088D42B /* OAFavoriteFolderBridgeItem.mm */, + 275ED65C2FE032A90088D42B /* OAFavoritePointBridgeItem.h */, + 275ED65D2FE032C80088D42B /* OAFavoritePointBridgeItem.mm */, + ); + path = FavoriteListViewController; + sourceTree = ""; + }; 2783B4342E66E68F00682723 /* Devices */ = { isa = PBXGroup; children = ( @@ -12581,6 +12630,9 @@ CEAF08172FD91C4A0048A74A /* FolderCardsCell.swift */, CEDB71E42FD993C3009DAA0D /* TrackStatsTableCell.swift */, 271A5C432FCDE1D700C27411 /* SortButtonCollectionViewCell.swift */, + 272AFBAA2FD810CC006C2E21 /* EmptyStateCollectionViewCell.swift */, + 27F055A92FE2EB860025F578 /* StatsFooterCollectionViewCell.swift */, + 27F055AB2FE2F76F0025F578 /* BackupBannerCollectionViewCell.swift */, DA5A7BB626C563A100F274C7 /* Xibs */, 468CD3822C809E3400CC3436 /* ElevationChartCell.swift */, 46BCB15F2CC2AC6D004E0283 /* GradientChartCell.swift */, @@ -12591,6 +12643,7 @@ DA5A7BB626C563A100F274C7 /* Xibs */ = { isa = PBXGroup; children = ( + 272AFBAB2FD810CC006C2E21 /* EmptyStateCollectionViewCell.xib */, 2702BC602EF98AD900A545A2 /* TopBottomValuesSliderTableViewCell.xib */, 27291B602EE81169005D0B0A /* PreviewImageViewTableViewCell.xib */, 27E8D9F02ED4688D00F98462 /* SegmentButtonsSliderTableViewCell.xib */, @@ -12908,9 +12961,7 @@ D7BF04772FD2DB4400BABB31 /* TracksViewController.swift */, D71B9A8B2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift */, D71B9A8F2FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift */, - DA5A7DC426C563A300F274C7 /* MyPlaces.storyboard */, - DA5A7E0126C563A300F274C7 /* OAFavoriteListViewController.h */, - DA5A7E0C26C563A300F274C7 /* OAFavoriteListViewController.mm */, + 275ED6502FDC5AA60088D42B /* FavoriteListViewController */, 327D68A42A8F9BA80076D1E0 /* SavedArticlesTabViewController.swift */, C50E32812CA3FBDF00EEC41F /* TracksFiltersViewController.swift */, C50E32832CA57DDB00EEC41F /* TracksFilterDetailsViewController.swift */, @@ -15907,7 +15958,6 @@ DAF50C6C299BB269006AAF2E /* routes.addon.render.xml in Resources */, DA86607D2268A2870041F57D /* ic_custom_remove@3x.png in Resources */, DA2F091D2217FF75006C7F05 /* ic_action_add_osm_note@2x.png in Resources */, - DA5A83B226C563A800F274C7 /* MyPlaces.storyboard in Resources */, 156E91BF24499CE300B181BA /* ic_custom_direction_device@3x.png in Resources */, 0A7C7F302703955200F779E4 /* ic_custom_track_line_thin@2x.png in Resources */, DA80D9A32716CC1800445DAF /* map_track_point_finish@2x.png in Resources */, @@ -17047,6 +17097,7 @@ 8476E2E81F6C1EEC00EC274F /* map_start_point@3x.png in Resources */, 462545AB28D3688900637964 /* ic_custom_cross_buy_colored_day@3x.png in Resources */, DA5A841226C563A800F274C7 /* OAGPXWptListViewController.xib in Resources */, + 272AFBAD2FD810CC006C2E21 /* EmptyStateCollectionViewCell.xib in Resources */, DA2EC62D29FBBE0200ECEB37 /* warnings_tunnel@3x.png in Resources */, 8476E2AA1F6C1EEC00EC274F /* map_default_location_view_angle@2x.png in Resources */, 0A8D1A1126FBA1750042444C /* ic_small_descent@2x.png in Resources */, @@ -17638,7 +17689,6 @@ DAEDAAA12865A0F100CE54D0 /* OAGenerateBackupInfoTask.m in Sources */, 32839A372AF0F911007B5057 /* WikiImageCacheHelper.swift in Sources */, 4663AB412951CB6200D9781E /* OAInputTableViewCell.m in Sources */, - DA5A83DD26C563A800F274C7 /* OAFavoriteListViewController.mm in Sources */, FAAD725A2E1BBE51000807A0 /* AppStartupMonitor.swift in Sources */, DA5A81C626C563A700F274C7 /* OACollatorStringMatcher.m in Sources */, DA5A81E926C563A700F274C7 /* OAHistoryHelper.m in Sources */, @@ -17957,6 +18007,7 @@ FA9217072D50C9640074E958 /* GalleryGridViewController.swift in Sources */, C553A28D2D53CD33007C9444 /* AverageSpeedComputerService.swift in Sources */, DAAC0EBD2801807200867D35 /* OAImportBackupTask.m in Sources */, + 272AFBAC2FD810CC006C2E21 /* EmptyStateCollectionViewCell.swift in Sources */, DA5A830226C563A800F274C7 /* OADeviceScreenTableViewCell.m in Sources */, 468D9C6E2C01030400ED368A /* QuickActionButtonState.swift in Sources */, DA5A830326C563A800F274C7 /* OAQuickActionCell.mm in Sources */, @@ -17971,6 +18022,7 @@ DA5A814D26C563A700F274C7 /* OASearchCategoriesListController.mm in Sources */, DA5A81F726C563A700F274C7 /* OAChoosePlanViewController.mm in Sources */, FAA6505A2ADD42C50020DCEA /* DeviceFactory.swift in Sources */, + 272AFB9B2FD2DD3C006C2E21 /* OAFavoritesBridgeHelper.mm in Sources */, DA5A84F126C563A900F274C7 /* OAMapSource.m in Sources */, FA282AF62C2C456700CC7AC1 /* WeatherNavigationBarView.swift in Sources */, DA5A825526C563A700F274C7 /* OAArrivalAnnouncementViewController.m in Sources */, @@ -18079,6 +18131,13 @@ DA5A848F26C563A900F274C7 /* OABaseTableViewController.m in Sources */, DA5A849326C563A900F274C7 /* OAGpxApproximationViewController.mm in Sources */, 32C21CB727E0A66B00DE4266 /* OARecordSettingsBottomSheetViewController.m in Sources */, + C5EB56EE2FD18B8A00D01657 /* FavoriteListViewController.swift in Sources */, + C5EB56F12FD18B8A00D01657 /* FavoriteListViewController+Models.swift in Sources */, + C5EB56F32FD18B8A00D01657 /* FavoriteListViewController+DataSource.swift in Sources */, + C5EB56F52FD18B8A00D01657 /* FavoriteListViewController+Cells.swift in Sources */, + C5EB56F72FD18B8A00D01657 /* FavoriteListViewController+ContextMenu.swift in Sources */, + C5EB56F92FD18B8A00D01657 /* FavoriteListViewController+Actions.swift in Sources */, + C5EB56FB2FD18B8A00D01657 /* FavoriteListViewController+Delegates.swift in Sources */, DA5A850926C563A900F274C7 /* UITableViewCell+getTableView.m in Sources */, DA5A83FF26C563A800F274C7 /* OAQuickSearchEmptyResultListItem.mm in Sources */, DA5A838926C563A800F274C7 /* OADonationSettingsViewController.mm in Sources */, @@ -18420,6 +18479,7 @@ DA5A839026C563A800F274C7 /* OAOsmAndLiveSelectionViewController.mm in Sources */, DA5A83C626C563A800F274C7 /* OAActionConfigurationViewController.mm in Sources */, DA5A812226C563A700F274C7 /* OAIAPHelper.mm in Sources */, + C58ACDE12FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift in Sources */, CE2AAB2B2FBF281000A5D22E /* CancelableScrollView.swift in Sources */, DA5A853C26C563A900F274C7 /* OAReverseGeocoder.mm in Sources */, 05DACD7F2C1CAF470023FAD9 /* OASharedUtil.m in Sources */, @@ -18614,6 +18674,7 @@ DA5A849226C563A900F274C7 /* OAMeasurementEditingContext.mm in Sources */, DA5A856226C563A900F274C7 /* OAWaypointHelper.mm in Sources */, DA5A838726C563A800F274C7 /* OAManageResourcesViewController.mm in Sources */, + 27F055AC2FE2F76F0025F578 /* BackupBannerCollectionViewCell.swift in Sources */, 46B619DC29EFE97500950A20 /* OAAltitudeWidget.mm in Sources */, DA5A81C226C563A700F274C7 /* OAPOICategory.m in Sources */, FAA650572ADD42C50020DCEA /* BLEHeartRateDevice.swift in Sources */, @@ -18865,7 +18926,9 @@ DA5A837826C563A800F274C7 /* OARouteSettingsBaseViewController.mm in Sources */, 3202C5932D6647CB0094AC8B /* ItemsCollectionViewController.swift in Sources */, 27FA9F572F45B9040064B5BE /* MapUnderlayAction.swift in Sources */, + 275ED65B2FE032600088D42B /* OAFavoriteFolderBridgeItem.mm in Sources */, FA82B9192E74199200A250F1 /* ImageDownloadRequestModifier.swift in Sources */, + 27F055AA2FE2EB860025F578 /* StatsFooterCollectionViewCell.swift in Sources */, DA5A847226C563A900F274C7 /* OATurnResource.m in Sources */, 32AC14832D6DF0B7009BE64E /* ChipsCollectionHandler.swift in Sources */, DA5A850326C563A900F274C7 /* OARouteInfoLegendItemView.m in Sources */, @@ -18953,6 +19016,7 @@ 46C841492C32F44A00E284B0 /* OAAddOSMBugAction.m in Sources */, FAC5D4312BFC7F5200E9E658 /* SpeedLimitView.swift in Sources */, CA4ED672AF26061C16E48A61 /* OACompassModeWidgetState.mm in Sources */, + 275ED65E2FE032C80088D42B /* OAFavoritePointBridgeItem.mm in Sources */, 46A8FD33297D985700AF9C0C /* OAUninstallSpeedCamerasViewController.mm in Sources */, CA4ED3D4E92007B8D20ECBC5 /* OAWeatherCacheSettingsViewController.mm in Sources */, CA4ED855557936D7E91423FC /* OAWeatherHelper.mm in Sources */, diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index 6a67292a12..7657d5bfca 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -648,6 +648,8 @@ "shared_string_subfolders_in" = "Subfolders in"; "shared_string_sorted_by" = "is sorted by:"; "shared_string_nearest" = "Nearest"; +"distance_nearest" = "Nearest first"; +"distance_farthest" = "Farthest first"; "track_sort_az" = "Name A - Z"; "track_sort_za" = "Name Z - A"; "newest_date_first" = "Newest date first"; @@ -1009,6 +1011,14 @@ "hud_no_movement" = "No movement"; // Favorites +"shared_string_pinned" = "Pinned"; +"delete_groups_confirmation" = "Delete groups?"; +"add_to_track" = "Add to track"; +"unpin_folder" = "Unpin folder"; +"pin_folder" = "Pin folder"; +"no_search_results" = "No results"; +"favorite_search_empty_state_description" = "Adjust your search or filters to see if that helps"; +"favorites_empty_folder_description" = "This folder doesn\’t have any points yet."; "shared_string_color" = "Color"; "fav_colors" = "Colors"; "shared_string_name" = "Name"; @@ -1057,6 +1067,16 @@ "fav_point_emoticons_message" = "Favorite renamed to \'%@\' to save the string containing emoticons to a file."; "access_default_color" = "Default color"; "default_color_descr" = "This color wil be used for all new favorites added to the group."; +"favorites_delete_confirmation_title" = "Delete %ld favorites?"; +"items_delete_confirmation_title" = "Delete %ld items?"; +"folders_delete_confirmation_title" = "Delete %ld folders?"; +"favorites_delete_confirmation_message" = "This action cannot be undone."; +"mixed_delete_confirmation_message" = "This will delete %ld folders and %ld points. This action cannot be undone."; +"empty_state_favourites" = "Add Favorites"; +"empty_state_favourites_desc" = "Import Favorites or add them by marking points on the map."; +"tracks_empty_folder" = "Empty folder"; +"tracks_empty_folder_description" = "This folder doesn’t have any points yet."; +"delete_favorite_confirmation_title" = "Delete \"%@\"?"; "favorite_friends_category" = "Friends"; "favorite_places_category" = "Places"; @@ -1676,6 +1696,7 @@ "change_folder" = "Change folder"; "select_folder" = "Select folder"; "select_folder_descr" = "Select folder or add new one"; +"add_new_folder" = "Add new folder"; "add_folder" = "Add folder"; "add_smart_folder" = "Add smart folder"; "save_as_smart_folder" = "Save as smart folder"; @@ -4123,13 +4144,16 @@ "original_icon_description" = "Each point retains its individual icon."; "original_color_description" = "Each point retains its individual color."; "original_shape_description" = "Each point retains its individual shape."; +"add_to" = "Add to"; "my_places_no_tracks_title_root" = "You don’t have track files"; "my_places_no_tracks_descr_root" = "You can import, create or record track files using OsmAnd."; "my_places_no_tracks_title" = "Empty folder"; "my_places_no_tracks_descr" = "This folder doesn’t have any track yet."; "delete_folder" = "Delete folder"; +"favorite_confirm_delete_group" = "Delete \"%1$@\" group and all included points (%2$lu)?"; "root_folder" = "Root folder"; +"shared_string_folders" = "Folders"; "all_folders" = "All folders"; "copy_as_new_folder" = "Copy as new folder"; "add_to_a_folder" = "Add to a folder"; diff --git a/Sources/Controllers/Cells/BackupBannerCollectionViewCell.swift b/Sources/Controllers/Cells/BackupBannerCollectionViewCell.swift new file mode 100644 index 0000000000..7f3f8586ba --- /dev/null +++ b/Sources/Controllers/Cells/BackupBannerCollectionViewCell.swift @@ -0,0 +1,68 @@ +// +// BackupBannerCollectionViewCell.swift +// OsmAnd Maps +// +// Created by Vladyslav Lysenko on 17.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +protocol BackupBannerCollectionViewCellDelegate: AnyObject { + func didClose() + func didOpenOsmAndCloud() + func backupBannerHeight(_ banner: FreeBackupBanner, fittingWidth: CGFloat) -> CGFloat +} + +final class BackupBannerCollectionViewCell: UICollectionViewCell { + weak var delegate: BackupBannerCollectionViewCellDelegate? { + didSet { + updateBannerLayout() + } + } + + private var heightConstraint: NSLayoutConstraint? + + private lazy var banner: FreeBackupBanner? = { + guard let banner = Bundle.main.loadNibNamed(FreeBackupBanner.reuseIdentifier, owner: self)?.first as? FreeBackupBanner else { return nil } + banner.configure(bannerType: .favorite) + banner.translatesAutoresizingMaskIntoConstraints = false + return banner + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupBanner() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + private func setupBanner() { + guard let banner else { return } + contentView.addSubview(banner) + + NSLayoutConstraint.activate([ + banner.topAnchor.constraint(equalTo: contentView.topAnchor), + banner.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + banner.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + banner.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + + heightConstraint = banner.heightAnchor.constraint(equalToConstant: banner.frame.height) + heightConstraint?.isActive = true + } + + private func updateBannerLayout() { + guard let banner else { return } + banner.didOsmAndCloudButtonAction = { [weak self] in + self?.delegate?.didOpenOsmAndCloud() + } + banner.didCloseButtonAction = { [weak self] in + self?.delegate?.didClose() + } + + let fittingWidth = contentView.bounds.width > 0.0 ? contentView.bounds.width : bounds.width + let bannerHeight = delegate?.backupBannerHeight(banner, fittingWidth: fittingWidth) ?? banner.frame.height + heightConstraint?.constant = bannerHeight + } +} diff --git a/Sources/Controllers/Cells/EmptyStateCollectionViewCell.swift b/Sources/Controllers/Cells/EmptyStateCollectionViewCell.swift new file mode 100644 index 0000000000..630c524e6e --- /dev/null +++ b/Sources/Controllers/Cells/EmptyStateCollectionViewCell.swift @@ -0,0 +1,21 @@ +// +// EmptyStateCollectionViewCell.swift +// OsmAnd Maps +// +// Created by Vladyslav Lysenko on 09.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +final class EmptyStateCollectionViewCell: UICollectionViewCell { + @IBOutlet weak var button: UIButton! + @IBOutlet private weak var cellImageView: UIImageView! + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var descriptionLabel: UILabel! + + func configure(image: UIImage, title: String, description: String) { + cellImageView.image = image + cellImageView.tintColor = .iconColorDefault + titleLabel.text = title + descriptionLabel.text = description + } +} diff --git a/Sources/Controllers/Cells/StatsFooterCollectionViewCell.swift b/Sources/Controllers/Cells/StatsFooterCollectionViewCell.swift new file mode 100644 index 0000000000..3599e0d310 --- /dev/null +++ b/Sources/Controllers/Cells/StatsFooterCollectionViewCell.swift @@ -0,0 +1,37 @@ +// +// StatsFooterCollectionViewCell.swift +// OsmAnd Maps +// +// Created by Vladyslav Lysenko on 17.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +final class StatsFooterCollectionViewCell: UICollectionViewCell { + private static let statsFooterInsets = NSDirectionalEdgeInsets(top: 12.0, leading: 20.0, bottom: 12.0, trailing: 20.0) + + lazy var label: UILabel = { + let label = UILabel() + label.font = .preferredFont(forTextStyle: .footnote) + label.adjustsFontForContentSizeCategory = true + label.textColor = .textColorSecondary + label.textAlignment = .center + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + override init(frame: CGRect) { + super.init(frame: frame) + setupLabel() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + private func setupLabel() { + contentView.addSubview(label) + + NSLayoutConstraint.activate([label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: Self.statsFooterInsets.top), label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: Self.statsFooterInsets.leading), label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -Self.statsFooterInsets.trailing), label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -Self.statsFooterInsets.bottom)]) + } +} diff --git a/Sources/Controllers/Cells/Xibs/EmptyStateCollectionViewCell.xib b/Sources/Controllers/Cells/Xibs/EmptyStateCollectionViewCell.xib new file mode 100644 index 0000000000..cc6f2be944 --- /dev/null +++ b/Sources/Controllers/Cells/Xibs/EmptyStateCollectionViewCell.xib @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift new file mode 100644 index 0000000000..d5f126ae43 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift @@ -0,0 +1,477 @@ +// +// FavoriteListViewController+Actions.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import CoreLocation +import UniformTypeIdentifiers + +extension FavoriteListViewController { + func openFavoriteGroupAppearance(_ groupName: String) { + guard let viewController = OAFavoriteGroupEditorViewController(group: OAFavoritesBridgeHelper.pointsGroup(forGroupName: groupName)) else { return } + favoriteGroupAppearanceGroupName = groupName + favoriteGroupAppearanceEditor = viewController + viewController.delegate = self + navigationController?.pushViewController(viewController, animated: true) + } + + func openFavoriteItemsMove(_ favoriteItems: [Any]) { + guard !favoriteItems.isEmpty, + let navigationController, + let groupController = OAEditGroupViewController(groupName: nil, groups: OAFavoritesBridgeHelper.favoriteGroupNames(forMovingFavoriteItems: favoriteItems)) else { + return + } + self.groupController = groupController + favoriteItemsToMove = favoriteItems + groupController.delegate = self + navigationController.present(UINavigationController(rootViewController: groupController), animated: true) + } + + func openFavoriteGroupAddToTrack(_ groupName: String) { + guard OAFavoritesBridgeHelper.canUseGroup(withName: groupName), let navigationController, let viewController = OAOpenAddTrackViewController(screenType: .addToATrack) else { return } + addToTrackGroupName = groupName + addToTrackFavoriteItems = nil + viewController.delegate = self + navigationController.present(UINavigationController(rootViewController: viewController), animated: true) + } + + func openFavoriteItemsAddToTrack(_ favoriteItems: [Any]) { + guard !favoriteItems.isEmpty, let navigationController, let viewController = OAOpenAddTrackViewController(screenType: .addToATrack) else { return } + addToTrackFavoriteItems = favoriteItems + addToTrackGroupName = nil + viewController.delegate = self + navigationController.present(UINavigationController(rootViewController: viewController), animated: true) + } + + func favoritePointRows(forGroupName groupName: String) -> [FavoritePointRow] { + let sortMode = isSearchResultsMode ? searchFavoriteSortMode() : favoriteSortMode(entryId: groupName) + let favorites = OAFavoritesBridgeHelper.favoritePoints(forGroupName: groupName).map { FavoritePointRow(item: $0) } + return FavoriteSortModeHelper.sortFavoritePointsWithMode(favorites, mode: sortMode) + } + + func favoritePointRows(allFolders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoritePointRow] { + allFolders.filter { isSearchGroup($0.bridgeItem.groupName, parentGroupName: parentGroupName) }.flatMap { OAFavoritesBridgeHelper.favoritePoints(forGroupName: $0.bridgeItem.groupName).map { FavoritePointRow(item: $0) } } + } + + func makeActionsMenu() -> UIMenu { + let addFolderAction = UIAction(title: localizedString("add_new_folder"), image: .icCustomFolderAddOutlined) { [weak self] _ in + self?.openNewFavoriteGroupEditor() + } + let importAction = UIAction(title: localizedString("shared_string_import"), image: .icCustomImportOutlined) { [weak self] _ in + self?.openPickerToImport() + } + + let addFolderSection = UIMenu(title: "", options: .displayInline, children: [addFolderAction]) + let importSection = UIMenu(title: "", options: .displayInline, children: [importAction]) + return UIMenu(title: "", children: [addFolderSection, importSection]) + } + + func setEditing(_ isEditing: Bool) { + let shouldResetSearchSelection = !isEditing && isSelectionModeInSearch + if !isEditing { + collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) } + isSelectionModeInSearch = false + isSearchActive = false + searchText = "" + selectionManager.deselectAll() + } + + collectionView.isEditing = isEditing + collectionView.reloadData() + myPlacesDelegate?.updateEditMode(isEditing) + configureNavigation() + navigationController?.setToolbarHidden(!isEditing, animated: true) + if shouldResetSearchSelection { + clearSearchControllerText() + applySnapshot(animatingDifferences: false) + } + } + + func showRenameAlert(for folder: FavoriteFolderRow) { + let alert = UIAlertController(title: localizedString("shared_string_rename"), message: localizedString("enter_new_name"), preferredStyle: .alert) + let applyAction = UIAlertAction(title: localizedString("shared_string_apply"), style: .default) { [weak self, weak alert] _ in + guard let self, let text = alert?.textFields?.first?.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return } + let oldGroupName = folder.bridgeItem.groupName + let newGroupName = self.groupName(oldGroupName, replacingLastComponentWith: text) + OAFavoritesBridgeHelper.renameFavoriteGroup(oldGroupName, newName: newGroupName) + self.renameFavoriteSortModeKeys(from: oldGroupName, to: newGroupName) + self.applySnapshot(animatingDifferences: true) + } + + alert.addAction(applyAction) + alert.addAction(UIAlertAction(title: localizedString("shared_string_cancel"), style: .cancel)) + alert.addTextField { textField in + textField.placeholder = localizedString("enter_new_name") + textField.text = folder.title + } + + alert.preferredAction = applyAction + present(alert, animated: true) + } + + func showDeleteAlert(for folder: FavoriteFolderRow) { + let message = String(format: localizedString("favorite_confirm_delete_group"), folder.title, folder.bridgeItem.subtreePointsCount) + let alert = UIAlertController(title: localizedString("delete_folder"), message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: localizedString("shared_string_delete"), style: .destructive) { [weak self] _ in + guard OAFavoritesBridgeHelper.deleteFavoriteGroup(folder.bridgeItem.groupName) else { return } + self?.clearFavoriteSortModes(forGroupNames: [folder.bridgeItem.groupName]) + self?.applySnapshot(animatingDifferences: true) + }) + + alert.addAction(UIAlertAction(title: localizedString("shared_string_cancel"), style: .cancel)) + present(alert, animated: true) + } + + func showFavoriteDeleteAlert(for favorite: FavoritePointRow) { + let title = String(format: localizedString("delete_favorite_confirmation_title"), favorite.title) + let alert = UIAlertController(title: title, message: localizedString("favorites_delete_confirmation_message"), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: localizedString("shared_string_delete"), style: .destructive) { [weak self] _ in + guard OAFavoritesBridgeHelper.deleteFavoritePoint(favorite.bridgeItem) else { return } + self?.applySnapshot(animatingDifferences: true) + }) + + alert.addAction(UIAlertAction(title: localizedString("shared_string_cancel"), style: .cancel)) + present(alert, animated: true) + } + + func shareFavoritePoint(_ point: OAFavoritePointBridgeItem, sourceView: UIView) { + pointToShare = point + let items = favoritePointShareItems(for: point) + guard !items.isEmpty else { + pointToShare = nil + return + } + showActivity(items, + applicationActivities: favoritePointShareActivities(), + excludedActivityTypes: nil, + sourceView: sourceView, + barButtonItem: nil) { [weak self] in + self?.pointToShare = nil + } + } + + func selectedFavoritePointsCount(for selectedItems: [Any]) -> Int { + let folderPointsCount = selectedItems.compactMap { $0 as? OAFavoriteFolderBridgeItem }.reduce(0) { $0 + Int($1.subtreePointsCount) } + let pointsCount = selectedItems.filter { $0 is OAFavoritePointBridgeItem }.count + return folderPointsCount + pointsCount + } + + func bridgeItems(for indexPaths: [IndexPath]) -> [Any] { + indexPaths.compactMap { indexPath in + guard let item = dataSource.itemIdentifier(for: indexPath) else { return nil } + switch item { + case .folder(let folder): + return folder.bridgeItem + case .favorite(let favorite): + return favorite.bridgeItem + default: + return nil + } + } + } + + func openFavoriteItemsAppearance() { + guard collectionView.indexPathsForSelectedItems?.isEmpty == false else { + let alert = UIAlertController(title: "", message: localizedString("fav_select"), preferredStyle: .alert) + alert.addAction(UIAlertAction(title: localizedString("shared_string_ok"), style: .default)) + present(alert, animated: true) + return + } + + guard let navigationController else { return } + let colorController = OAEditColorViewController() + colorController.delegate = self + self.colorController = colorController + let modalNavigationController = UINavigationController(rootViewController: colorController) + navigationController.present(modalNavigationController, animated: true) + } + + func updateSelection(at indexPath: IndexPath) { + guard let selectionItem = dataSource.itemIdentifier(for: indexPath)?.selectionItem else { return } + selectionManager.toggle(selectionItem) + } + + @objc func selectButtonPressed() { + setEditing(true) + } + + @objc func searchSelectButtonPressed() { + isSelectionModeInSearch = true + isSearchActive = false + if isRootFolder { + let searchController = navigationController?.navigationBar.topItem?.searchController + searchController?.isActive = false + } else { + subfolderSearchController.isActive = false + } + + selectButtonPressed() + } + + @objc func cancelButtonPressed() { + setEditing(false) + configureToolbar() + } + + @objc func selectAllButtonPressed() { + let selectableIndexPaths = selectableIndexPaths() + if selectionManager.areAllSelected { + selectableIndexPaths.forEach { collectionView.deselectItem(at: $0, animated: false) } + selectionManager.deselectAll() + } else { + selectableIndexPaths.forEach { collectionView.selectItem(at: $0, animated: false, scrollPosition: []) } + selectionManager.selectAll() + } + + updateSelectionUI() + } + + @objc func favoriteDataDidChange() { + DispatchQueue.main.async { [weak self] in + OAFavoritesBridgeHelper.invalidateFavoriteFoldersCache() + self?.applySnapshot(animatingDifferences: true) + } + } + + @objc func productPurchased() { + DispatchQueue.main.async { [weak self] in + self?.applySnapshot(animatingDifferences: true) + } + } + + @objc func shareButtonClicked(_ sender: Any) { + let sourceView = sender as? UIView ?? collectionView + shareItems(for: sourceView) + setEditing(false) + applySnapshot() + } + + @objc func moveButtonClicked(_ sender: Any) { + guard let selectedItems = collectionView.indexPathsForSelectedItems, !selectedItems.isEmpty else { + let alert = UIAlertController(title: "", message: localizedString("fav_select"), preferredStyle: .alert) + let defaultAction = UIAlertAction(title: localizedString("shared_string_ok"), style: .default) + alert.addAction(defaultAction) + present(alert, animated: true) + return + } + + openFavoriteItemsMove(bridgeItems(for: selectedItems)) + } + + @objc func deleteButtonClicked(_ sender: Any) { + let selectedIndexPaths = collectionView.indexPathsForSelectedItems ?? [] + if selectedIndexPaths.isEmpty { + let alert = UIAlertController(title: nil, message: localizedString("fav_select_remove"), preferredStyle: .alert) + let defaultAction = UIAlertAction(title: localizedString("ok"), style: .default) + alert.addAction(defaultAction) + present(alert, animated: true) + return + } + + let selectedBridgeItems = bridgeItems(for: selectedIndexPaths) + let title = deleteConfirmationTitle(for: selectedBridgeItems) + let message = deleteConfirmationMessage(for: selectedBridgeItems) + + let alert = UIAlertController( + title: title, + message: message, + preferredStyle: .alert + ) + + let deleteButton = UIAlertAction( + title: localizedString("shared_string_delete"), + style: .destructive + ) { [weak self] _ in + self?.removeSelectedFavoriteItems() + } + + let cancelButton = UIAlertAction( + title: localizedString("shared_string_cancel"), + style: .cancel, + handler: nil + ) + + alert.addAction(deleteButton) + alert.addAction(cancelButton) + + present(alert, animated: true, completion: nil) + } + + @objc func importButtonClicked(_ sender: Any) { + openPickerToImport() + } + + @objc func clearSearchButtonClicked(_ sender: Any) { + searchText = "" + clearSearchControllerText() + configureToolbar() + navigationController?.setToolbarHidden(shouldHideSearchToolbar(), animated: true) + applySnapshot(animatingDifferences: false) + } + + @objc func updateDistanceAndDirection() { + updateDistanceAndDirection(false) + } + + private func selectableIndexPaths() -> [IndexPath] { + var indexPaths: [IndexPath] = [] + for section in 0.. Bool { + guard let parentGroupName else { return true } + guard !parentGroupName.isEmpty else { return groupName.isEmpty } + return groupName == parentGroupName || isNestedFolder(groupName, in: parentGroupName) + } + + private func groupName(_ groupName: String, replacingLastComponentWith lastComponent: String) -> String { + guard let separatorIndex = groupName.lastIndex(of: "/") else { return lastComponent } + let parentGroupName = groupName[.. [Any] { + var items: [Any] = [] + let sharingText = NSMutableString() + appendFavoritePointShareLine(point.title, to: sharingText) + appendFavoritePointShareLine(point.displayGroupName, to: sharingText) + appendFavoritePointShareLine(point.itemDescription, to: sharingText) + appendFavoritePointCoordinatesAndURL(to: sharingText, point: point) + if let url = URL(string: OAFavoritesBridgeHelper.sharePoiURLString(forFavoritePoint: point)) { + items.append(ShareLinkItem(url: url, title: point.title, icon: point.icon)) + } + if sharingText.length > 0 { + items.append(sharingText) + } + return items + } + + private func appendFavoritePointShareLine(_ line: String?, to sharingText: NSMutableString) { + guard let line, !line.isEmpty else { return } + if sharingText.length > 0 { + sharingText.append("\n") + } + sharingText.append(line) + } + + private func appendFavoritePointCoordinatesAndURL(to sharingText: NSMutableString, point: OAFavoritePointBridgeItem) { + let geoURLString = OAFavoritesBridgeHelper.geoURLString(forFavoritePoint: point) + if !geoURLString.isEmpty { + sharingText.append("\nLocation: \(geoURLString)") + } + + let shareURLString = OAFavoritesBridgeHelper.sharePoiURLString(forFavoritePoint: point) + if !shareURLString.isEmpty { + sharingText.append("\n\(shareURLString)") + } + } + + private func favoritePointShareActivities() -> [UIActivity] { + let activities: [OAShareMenuActivityType] = [.clipboard, .copyAddress, .copyPOIName, .copyCoordinates, .geo] + return activities.compactMap { type in + let activity = OAShareMenuActivity(type: type) + activity?.delegate = self + return activity + } + } + + private func shareItems(for sourceView: UIView) { + guard let selectedItems = collectionView.indexPathsForSelectedItems, !selectedItems.isEmpty else { + let alert = UIAlertController( + title: "", + message: localizedString("fav_export_select"), + preferredStyle: .alert + ) + + let defaultAction = UIAlertAction( + title: localizedString("shared_string_ok"), + style: .default, + handler: nil + ) + + alert.addAction(defaultAction) + present(alert, animated: true, completion: nil) + return + } + + guard let favoritesUrl = OAFavoritesBridgeHelper.shareFavoriteItems(bridgeItems(for: selectedItems)) else { return } + showActivity( + [favoritesUrl], + sourceView: sourceView, + barButtonItem: nil, + completionWithItemsHandler: { + try? FileManager.default.removeItem(at: favoritesUrl) + } + ) + } + + private func removeSelectedFavoriteItems() { + let selectedIndexPaths = collectionView.indexPathsForSelectedItems ?? [] + let items = bridgeItems(for: selectedIndexPaths) + let groupNames = items.compactMap { ($0 as? OAFavoriteFolderBridgeItem)?.groupName } + if OAFavoritesBridgeHelper.deleteFavoriteItems(items) { + clearFavoriteSortModes(forGroupNames: groupNames) + } + + setEditing(false) + applySnapshot(animatingDifferences: true) + } + + private func deleteConfirmationTitle(for selectedItems: [Any]) -> String { + let foldersCount = selectedItems.filter { $0 is OAFavoriteFolderBridgeItem }.count + let pointsCount = selectedItems.filter { $0 is OAFavoritePointBridgeItem }.count + + if foldersCount > 0 && pointsCount == 0 { + return String(format: localizedString("folders_delete_confirmation_title"), foldersCount) + } else if pointsCount > 0 && foldersCount == 0 { + return String(format: localizedString("favorites_delete_confirmation_title"), pointsCount) + } else { + return String(format: localizedString("items_delete_confirmation_title"), pointsCount + foldersCount) + } + } + + private func deleteConfirmationMessage(for selectedItems: [Any]) -> String { + let folders = selectedItems.compactMap { $0 as? OAFavoriteFolderBridgeItem } + let points = selectedItems.compactMap { $0 as? OAFavoritePointBridgeItem } + if folders.isEmpty { + return localizedString("favorites_delete_confirmation_message") + } + + let folderPointsCount = folders.reduce(0) { $0 + Int($1.subtreePointsCount) } + let pointsCount = folderPointsCount + points.count + + return String(format: localizedString("mixed_delete_confirmation_message"), folders.count, pointsCount) + } + + private func openPickerToImport() { + let gpxType = UTType(importedAs: "com.topografix.gpx", conformingTo: .xml) + let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [gpxType], asCopy: true) + documentPicker.allowsMultipleSelection = false + documentPicker.delegate = self + present(documentPicker, animated: true) + } +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift new file mode 100644 index 0000000000..f3bd8b69ce --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift @@ -0,0 +1,191 @@ +// +// FavoriteListViewController+Cells.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +extension FavoriteListViewController { + var headerCellRegistration: CellRegistration { + CellRegistration { cell, _, section in + var content = cell.defaultContentConfiguration() + content.text = section.title + content.textProperties.color = .textColorPrimary + content.textProperties.font = .systemFont(ofSize: 20, weight: .semibold) + cell.contentConfiguration = content + let disclosureOptions = UICellAccessory.OutlineDisclosureOptions(style: .header) + cell.accessories = [.outlineDisclosure(options: disclosureOptions)] + cell.tintColor = .iconColorActive + } + } + + var sortHeaderCellRegistration: UICollectionView.CellRegistration { + UICollectionView.CellRegistration { [weak self] cell, _, sortHeader in + cell.sortButton.setImage(sortHeader.sortMode.image, for: .normal) + cell.sortButton.menu = self?.makeSortMenu(includesDistanceSortModes: sortHeader.includesDistanceSortModes) + } + } + + var backupBannerCellRegistration: UICollectionView.CellRegistration { + UICollectionView.CellRegistration { [weak self] cell, _, _ in + cell.delegate = self + } + } + + var folderCellRegistration: RowCellRegistration { + RowCellRegistration { [weak self] cell, _, folder in + var content = cell.defaultContentConfiguration() + content.image = (folder.isPinned ? .icCustomFolderPin : UIImage.templateImageNamed(folder.iconName))?.resizedTemplateImage(with: FavoriteListViewController.imageSize) + content.imageProperties.tintColor = folder.iconColor + content.text = folder.title + content.textProperties.color = folder.titleColor + content.textProperties.font = folder.titleFont + content.textProperties.numberOfLines = 2 + content.secondaryText = folder.subtitle + content.secondaryTextProperties.color = .textColorSecondary + cell.contentConfiguration = content + cell.backgroundConfiguration = self?.listCellBackgroundConfiguration() + cell.accessories = self?.collectionView.isEditing == true ? [.multiselect()] : [.multiselect(), .disclosureIndicator()] + } + } + + var favoriteCellRegistration: RowCellRegistration { + RowCellRegistration { [weak self] cell, _, favorite in + if let self, !self.currentSortMode.isDistanceOriented { + favorite.bridgeItem.updateDistanceAndDirection() + } + var content = cell.defaultContentConfiguration() + content.image = OAUtilities.resize(favorite.bridgeItem.icon, newSize: CGSize(width: Self.favoriteIconSize, height: Self.favoriteIconSize)) + content.text = favorite.title + content.textProperties.numberOfLines = 2 + content.textProperties.color = favorite.titleColor + content.textProperties.font = favorite.titleFont + content.secondaryAttributedText = self?.favoriteSecondaryAttributedText(for: favorite, includesGroupName: self?.isSearchResultsMode == true) + content.secondaryTextProperties.color = .textColorSecondary + content.secondaryTextProperties.numberOfLines = 1 + cell.contentConfiguration = content + cell.backgroundConfiguration = self?.listCellBackgroundConfiguration() + cell.accessories = [.multiselect()] + } + } + + var statsFooterCellRegistration: UICollectionView.CellRegistration { + UICollectionView.CellRegistration { cell, _, stats in + cell.backgroundColor = .clear + cell.contentView.backgroundColor = .clear + cell.label.text = stats.text + } + } + + var emptyStateCellRegistration: UICollectionView.CellRegistration { + UICollectionView.CellRegistration(cellNib: UINib(nibName: EmptyStateCollectionViewCell.reuseIdentifier, bundle: nil)) { [weak self] cell, _, _ in + guard let self else { return } + cell.button.removeTarget(nil, action: nil, for: .touchUpInside) + if self.isSearchResultsMode { + cell.configure(image: .icCustomSearch, + title: localizedString("no_search_results"), + description: localizedString("favorite_search_empty_state_description")) + cell.button.setTitle(localizedString("shared_string_clear_all"), for: .normal) + cell.button.addTarget(self, action: #selector(self.clearSearchButtonClicked), for: .touchUpInside) + return + } + + let isRootFolder = self.isRootFolder + cell.configure(image: isRootFolder ? .icCustomFavorites : .icCustomFolderOpen, + title: localizedString(isRootFolder ? "empty_state_favourites" : "tracks_empty_folder"), + description: localizedString(isRootFolder ? "empty_state_favourites_desc" : "tracks_empty_folder_description")) + cell.button.setTitle(localizedString("shared_string_import"), for: .normal) + cell.button.addTarget(self, action: #selector(self.importButtonClicked), for: .touchUpInside) + } + } + + func updateVisibleFavoriteCellsDistanceAndDirection() { + for indexPath in collectionView.indexPathsForVisibleItems { + guard case .favorite(let favorite) = dataSource.itemIdentifier(for: indexPath), + let cell = collectionView.cellForItem(at: indexPath) as? FavoriteListCell, + var content = cell.contentConfiguration as? UIListContentConfiguration else { + continue + } + + favorite.bridgeItem.updateDistanceAndDirection() + content.secondaryAttributedText = favoriteSecondaryAttributedText(for: favorite, includesGroupName: isSearchResultsMode) + cell.contentConfiguration = content + } + } + + private func favoriteSecondaryAttributedText(for favorite: FavoritePointRow, includesGroupName: Bool) -> NSAttributedString { + let font = UIFont.scaledSystemFont(ofSize: 15) + let directionAttributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: UIColor.textColorDirectionActive + ] + let secondaryAttributes: [NSAttributedString.Key: Any] = [ + .font: font, + .foregroundColor: UIColor.textColorSecondary + ] + + let result = NSMutableAttributedString() + let date = favorite.lastModified.map { DateFormatter.detailsDateFormatter.string(from: $0) } + let groupName = favorite.bridgeItem.groupName.isEmpty ? localizedString("shared_string_favorites") : favorite.bridgeItem.groupName + + if currentSortMode.isDateOriented { + appendFavoriteSecondaryText(date, to: result, attributes: secondaryAttributes) + appendFavoriteDistance(favorite, + to: result, + font: font, + directionAttributes: directionAttributes, + separatorAttributes: secondaryAttributes) + appendFavoriteSecondaryText(favorite.bridgeItem.address, to: result, attributes: secondaryAttributes) + } else { + appendFavoriteDistance(favorite, + to: result, + font: font, + directionAttributes: directionAttributes, + separatorAttributes: secondaryAttributes) + appendFavoriteSecondaryText(favorite.bridgeItem.address, to: result, attributes: secondaryAttributes) + appendFavoriteSecondaryText(date, to: result, attributes: secondaryAttributes) + } + if includesGroupName { + appendFavoriteSecondaryText(groupName, to: result, attributes: secondaryAttributes) + } + + return result + } + + private func appendFavoriteSecondaryText(_ text: String?, to result: NSMutableAttributedString, attributes: [NSAttributedString.Key: Any]) { + guard let text, !text.isEmpty else { return } + appendFavoriteSecondarySeparatorIfNeeded(to: result, attributes: attributes) + result.append(NSAttributedString(string: text, attributes: attributes)) + } + + private func appendFavoriteDistance(_ favorite: FavoritePointRow, + to result: NSMutableAttributedString, + font: UIFont, + directionAttributes: [NSAttributedString.Key: Any], + separatorAttributes: [NSAttributedString.Key: Any]) { + guard let distance = favorite.distance, let formattedDistance = OAOsmAndFormatter.getFormattedDistance(Float(distance)) else { return } + appendFavoriteSecondarySeparatorIfNeeded(to: result, attributes: separatorAttributes) + if let directionIcon = favoriteDirectionIcon(tintColor: .iconColorDirectionActive) { + let rotatedDirectionIcon = directionIcon.rotatedForAttributedString(with: favorite.bridgeItem.direction) + let attachment = NSTextAttachment() + attachment.image = rotatedDirectionIcon + attachment.bounds = CGRect(x: 0.0, + y: (font.capHeight - rotatedDirectionIcon.size.height) / 2.0, + width: rotatedDirectionIcon.size.width, + height: rotatedDirectionIcon.size.height) + result.append(NSAttributedString(attachment: attachment)) + } + result.append(NSAttributedString(string: formattedDistance, attributes: directionAttributes)) + } + + private func appendFavoriteSecondarySeparatorIfNeeded(to result: NSMutableAttributedString, attributes: [NSAttributedString.Key: Any]) { + guard result.length > 0 else { return } + result.append(NSAttributedString(string: " • ", attributes: attributes)) + } + + private func favoriteDirectionIcon(tintColor: UIColor) -> UIImage? { + let size = UIFontMetrics.default.scaledValue(for: 18.0) + return OAUtilities.resize(.icSmallDirection, newSize: CGSize(width: size, height: size))?.withTintColor(tintColor) + } +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift new file mode 100644 index 0000000000..766f7dfe24 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift @@ -0,0 +1,204 @@ +// +// FavoriteListViewController+ContextMenu.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +extension FavoriteListViewController { + func makeFolderContextMenu(for folder: FavoriteFolderRow, indexPath: IndexPath) -> UIMenu { + let folderFavoriteItem: [Any] = [folder.bridgeItem] + let subtreeFavoriteItems: [Any] = favoritePointRows(allFolders: favoriteFolders(), parentGroupName: folder.bridgeItem.groupName).map { $0.bridgeItem } + let hasFavoritePoints = !subtreeFavoriteItems.isEmpty + let showHideAction = UIAction(title: localizedString(folder.isVisible ? "shared_string_hide_from_map" : "shared_string_show_on_map"), image: folder.isVisible ? .icCustomHideOutlined : .icCustomShowOutlined) { [weak self] _ in + guard let self else { return } + OAFavoritesBridgeHelper.setFavoriteGroupVisible(folder.bridgeItem.groupName, visible: !folder.isVisible) + self.applySnapshot(animatingDifferences: true) + } + let pinAction = UIAction(title: localizedString(folder.isPinned ? "unpin_folder" : "pin_folder"), image: folder.isPinned ? .icCustomDrawingPinDisable : .icCustomDrawingPin) { [weak self] _ in + guard let self else { return } + OAFavoritesBridgeHelper.setFavoriteGroupPinned(folder.bridgeItem.groupName, pinned: !folder.isPinned) + self.applySnapshot(animatingDifferences: true) + } + let firstButtonsSection = UIMenu(title: "", options: .displayInline, children: [showHideAction, pinAction]) + + let renameAction = UIAction(title: localizedString("shared_string_rename"), image: .icCustomEdit) { [weak self] _ in + guard let self else { return } + self.showRenameAlert(for: folder) + } + let defaultAppearanceAction = UIAction(title: localizedString("default_appearance"), image: .icCustomAppearanceOutlined) { [weak self] _ in + guard let self else { return } + self.openFavoriteGroupAppearance(folder.bridgeItem.groupName) + } + let secondButtonsSection = UIMenu(title: "", options: .displayInline, children: [renameAction, defaultAppearanceAction]) + + let shareAction = UIAction(title: localizedString("shared_string_share"), image: .icCustomExportOutlined) { [weak self] _ in + guard let self else { return } + let sourceView: UIView = self.collectionView.cellForItem(at: indexPath) ?? self.collectionView + guard let favoritesUrl = OAFavoritesBridgeHelper.shareFavoriteItems([folder.bridgeItem]) else { return } + showActivity([favoritesUrl], sourceView: sourceView, barButtonItem: nil, completionWithItemsHandler: { + try? FileManager.default.removeItem(at: favoritesUrl) + }) + } + let moveAction = UIAction(title: localizedString("shared_string_move"), image: .icCustomFolderMoveOutlined) { [weak self] _ in + guard let self else { return } + self.openFavoriteItemsMove([folder.bridgeItem]) + } + let thirdButtons: [UIMenuElement] = (hasFavoritePoints ? [shareAction] : []) + (folder.bridgeItem.groupName.isEmpty ? [] : [moveAction]) + let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: thirdButtons) + + let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { _ in + OAFavoritesBridgeHelper.addFavoriteItems(toMapMarkers: folderFavoriteItem) + } + let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: .icCustomTrip) { [weak self] _ in + guard let self else { return } + self.openFavoriteGroupAddToTrack(folder.bridgeItem.groupName) + } + let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { _ in + OAFavoritesBridgeHelper.addFavoriteItems(toNavigation: folderFavoriteItem) + } + let addToActions: [UIMenuElement] = hasFavoritePoints ? [mapMarkersAction, trackAction, navigationAction] : [] + let fourthButtons: [UIMenuElement] = addToActions.isEmpty ? [] : [UIMenu(title: localizedString("add_to"), image: .icCustomAdd, children: addToActions)] + let fourthButtonsSection = UIMenu(title: "", options: .displayInline, children: fourthButtons) + + let deleteAction = UIAction(title: localizedString("shared_string_delete"), image: .icCustomTrashOutlined, attributes: .destructive) { [weak self] _ in + guard let self else { return } + self.showDeleteAlert(for: folder) + } + let lastButtonsSection = UIMenu(title: "", options: .displayInline, children: [deleteAction]) + + return UIMenu(title: "", children: [firstButtonsSection, secondButtonsSection, thirdButtonsSection, fourthButtonsSection, lastButtonsSection].filter { !$0.children.isEmpty }) + } + + func makePointContextMenu(for point: FavoritePointRow, indexPath: IndexPath) -> UIMenu { + let editAction = UIAction(title: localizedString("shared_string_edit"), image: .icCustomEdit) { [weak self] _ in + guard let self, let viewController = OAFavoritesBridgeHelper.editPointViewController(forFavoritePoint: point.bridgeItem) else { return } + viewController.delegate = self + let navigationController = UINavigationController(rootViewController: viewController) + self.navigationController?.present(navigationController, animated: true) + } + let firstButtonsSection = UIMenu(title: "", options: .displayInline, children: [editAction]) + + let moveAction = UIAction(title: localizedString("shared_string_move"), image: .icCustomFolderMoveOutlined) { [weak self] _ in + guard let self else { return } + self.openFavoriteItemsMove([point.bridgeItem]) + } + let shareAction = UIAction(title: localizedString("shared_string_share"), image: .icCustomExportOutlined) { [weak self] _ in + guard let self, + let sourceView: UIView = self.collectionView.cellForItem(at: indexPath) else { + return + } + + self.shareFavoritePoint(point.bridgeItem, sourceView: sourceView) + } + let secondButtonsSection = UIMenu(title: "", options: .displayInline, children: [moveAction, shareAction]) + + let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { _ in + OAFavoritesBridgeHelper.addFavoriteItems(toMapMarkers: [point.bridgeItem]) + } + let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: .icCustomTrip) { [weak self] _ in + guard let self else { return } + self.openFavoriteItemsAddToTrack([point.bridgeItem]) + } + let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { _ in + OAFavoritesBridgeHelper.addFavoriteItems(toNavigation: [point.bridgeItem]) + } + let addToMenu = UIMenu(title: localizedString("add_to"), image: .icCustomAdd, children: [mapMarkersAction, trackAction, navigationAction]) + let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: [addToMenu]) + + let deleteAction = UIAction(title: localizedString("shared_string_delete"), image: .icCustomTrashOutlined, attributes: .destructive) { [weak self] _ in + guard let self else { return } + self.showFavoriteDeleteAlert(for: point) + } + let lastButtonsSection = UIMenu(title: "", options: .displayInline, children: [deleteAction]) + + return UIMenu(title: "", children: [firstButtonsSection, secondButtonsSection, thirdButtonsSection, lastButtonsSection]) + } + + func makeAdditionalContextMenu() -> UIMenu { + var menuElements: [UIMenuElement] = [] + let indexPathItems = collectionView.indexPathsForSelectedItems ?? [] + let selectedBridgeItems = bridgeItems(for: indexPathItems) + let hasPoints = indexPathItems.contains { + guard case .favorite = dataSource.itemIdentifier(for: $0) else { return false } + return true + } + + let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { [weak self] _ in + OAFavoritesBridgeHelper.addFavoriteItems(toMapMarkers: selectedBridgeItems) + self?.setEditing(false) + self?.applySnapshot(animatingDifferences: true) + } + let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: .icCustomTrip) { [weak self] _ in + self?.openFavoriteItemsAddToTrack(selectedBridgeItems) + self?.setEditing(false) + self?.applySnapshot(animatingDifferences: true) + } + let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in + OAFavoritesBridgeHelper.addFavoriteItems(toNavigation: selectedBridgeItems) + self?.applySnapshot(animatingDifferences: true) + } + let addToMenu = UIMenu(title: localizedString("add_to"), image: .icCustomAdd, children: [navigationAction, trackAction, mapMarkersAction]) + let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: [addToMenu]) + menuElements.append(thirdButtonsSection) + + let changeAppearanceAction = UIAction(title: localizedString("change_appearance"), image: .icCustomAppearanceOutlined) { [weak self] _ in + self?.openFavoriteItemsAppearance() + } + let secondButtonsSection = UIMenu(title: "", options: .displayInline, children: [changeAppearanceAction]) + menuElements.append(secondButtonsSection) + + if !hasPoints { + let folders: [FavoriteFolderRow] = indexPathItems.compactMap { + guard case .folder(let folder) = dataSource.itemIdentifier(for: $0) else { return nil } + return folder + } + + if !folders.isEmpty { + var folderMenuElements: [UIMenuElement] = [] + + if folders.contains(where: { !$0.isPinned }) { + let unpinnedGroupNames = folders.filter({ !$0.isPinned }).map { $0.bridgeItem.groupName } + let pinAction = UIAction(title: localizedString("pin_folder"), image: .icCustomMapPinOutlined) { [weak self] _ in + OAFavoritesBridgeHelper.setFavoriteGroupsPinned(unpinnedGroupNames, pinned: true) + self?.applySnapshot(animatingDifferences: true) + } + folderMenuElements.append(pinAction) + } + + if folders.contains(where: { $0.isPinned }) { + let pinnedGroupNames = folders.filter({ $0.isPinned }).map { $0.bridgeItem.groupName } + let unpinAction = UIAction(title: localizedString("unpin_folder"), image: .icCustomMapPinOutlined) { [weak self] _ in + OAFavoritesBridgeHelper.setFavoriteGroupsPinned(pinnedGroupNames, pinned: false) + self?.applySnapshot(animatingDifferences: true) + } + folderMenuElements.append(unpinAction) + } + + if folders.contains(where: { $0.isVisible }) { + let visibleGroupNames = folders.filter({ $0.isVisible }).map { $0.bridgeItem.groupName } + let hideAction = UIAction(title: localizedString("shared_string_hide_from_map"), image: .icCustomHideOutlined) { [weak self] _ in + OAFavoritesBridgeHelper.setFavoriteGroupsVisible(visibleGroupNames, visible: false) + self?.applySnapshot(animatingDifferences: true) + } + folderMenuElements.append(hideAction) + } + + if folders.contains(where: { !$0.isVisible }) { + let hiddenGroupNames = folders.filter({ !$0.isVisible }).map { $0.bridgeItem.groupName } + let showAction = UIAction(title: localizedString("shared_string_show_on_map"), image: .icCustomShowOutlined) { [weak self] _ in + OAFavoritesBridgeHelper.setFavoriteGroupsVisible(hiddenGroupNames, visible: true) + self?.applySnapshot(animatingDifferences: true) + } + folderMenuElements.append(showAction) + } + + let firstButtonsSection = UIMenu(title: "", options: .displayInline, children: folderMenuElements) + menuElements.append(firstButtonsSection) + } + } + + return UIMenu(title: "", children: menuElements) + } +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift new file mode 100644 index 0000000000..d208453a0d --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift @@ -0,0 +1,336 @@ +// +// FavoriteListViewController+DataSource.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +extension FavoriteListViewController { + func favoriteSortMode(entryId: String? = nil) -> FavoriteSortMode { + let sortModes = settings.getFavoriteSortModes() + guard let sortModeTitle = sortModes[entryId ?? currentSortEntryId] else { return FavoriteSortModeHelper.defaultSortMode() } + return FavoriteSortMode.byTitle(sortModeTitle) + } + + func searchFavoriteSortMode() -> FavoriteSortMode { + let sortModeTitle = settings.searchFavoriteSortMode.get() + return FavoriteSortMode.byTitle(sortModeTitle) + } + + func clearFavoriteSortModes(forGroupNames groupNames: [String]) { + var sortModes = settings.getFavoriteSortModes() + let keysToRemove = sortModes.keys.filter { key in + groupNames.contains { groupName in + isFavoriteSortModeKey(key, insideOrEqualTo: groupName) + } + } + + guard !keysToRemove.isEmpty else { return } + keysToRemove.forEach { sortModes.removeValue(forKey: $0) } + settings.saveFavoriteSortModes(sortModes) + } + + func renameFavoriteSortModeKeys(from oldGroupName: String, to newGroupName: String, existingGroupNames: Set? = nil) { + guard !oldGroupName.isEmpty, oldGroupName != newGroupName else { return } + let groupNames = existingGroupNames ?? Set(OAFavoritesBridgeHelper.favoriteFolders().map { $0.groupName }) + guard !groupNames.contains(oldGroupName), groupNames.contains(newGroupName) else { return } + var sortModes = settings.getFavoriteSortModes() + let keysToRename = sortModes.keys.filter { isFavoriteSortModeKey($0, insideOrEqualTo: oldGroupName) } + guard !keysToRename.isEmpty else { return } + keysToRename.forEach { key in + if let value = sortModes.removeValue(forKey: key) { + sortModes[newGroupName + String(key.dropFirst(oldGroupName.count))] = value + } + } + + settings.saveFavoriteSortModes(sortModes) + } + + func updateFavoriteSortModeKeysAfterMove(_ favoriteItems: [Any], toGroupName targetGroupName: String) { + let groupNames = Set(OAFavoritesBridgeHelper.favoriteFolders().map { $0.groupName }) + favoriteItems.compactMap { $0 as? OAFavoriteFolderBridgeItem }.forEach { folder in + let oldGroupName = folder.groupName + let folderName = oldGroupName.split(separator: "/").last.map(String.init) ?? oldGroupName + let newGroupName = targetGroupName.isEmpty ? folderName : "\(targetGroupName)/\(folderName)" + renameFavoriteSortModeKeys(from: oldGroupName, to: newGroupName, existingGroupNames: groupNames) + } + } + + func createFavoriteMoveTargetGroupIfNeeded(_ groupName: String, favoriteItems: [Any]) { + let folders = favoriteItems.compactMap { $0 as? OAFavoriteFolderBridgeItem } + guard !folders.isEmpty, !folders.contains(where: { isFavoriteSortModeKey(groupName, insideOrEqualTo: $0.groupName) }) else { return } + var existingGroupNames = Set(OAFavoritesBridgeHelper.favoriteFolders().map { $0.groupName }) + var parentGroupName = "" + for folderName in groupName.split(separator: "/").map(String.init) { + let newGroupName = parentGroupName.isEmpty ? folderName : "\(parentGroupName)/\(folderName)" + if !existingGroupNames.contains(newGroupName), OAFavoritesBridgeHelper.addFavoriteGroup(folderName, parentGroupName: parentGroupName.isEmpty ? nil : parentGroupName, iconName: nil, color: nil, backgroundIconName: nil) { + existingGroupNames.insert(newGroupName) + } + parentGroupName = newGroupName + } + } + + func makeSortMenu(includesDistanceSortModes: Bool) -> UIMenu { + let modes: [FavoriteSortMode] = includesDistanceSortModes ? FavoriteSortMode.allCases : [.lastModified, .nameAZ, .nameZA, .newestDateFirst, .oldestDateFirst] + let groups: [[FavoriteSortMode]] = [[.lastModified], [.nameAZ, .nameZA], [.newestDateFirst, .oldestDateFirst], [.nearest, .farthest]] + let sections = groups.compactMap { group -> UIMenu? in + let actions = group.filter { modes.contains($0) }.map { makeSortAction(for: $0) } + return actions.isEmpty ? nil : UIMenu(options: .displayInline, children: actions) + } + + return UIMenu(title: "", children: sections) + } + + func makeDataSource() -> DataSource { + let sortHeaderCellRegistration = sortHeaderCellRegistration + let backupBannerCellRegistration = backupBannerCellRegistration + let folderCellRegistration = folderCellRegistration + let favoriteCellRegistration = favoriteCellRegistration + let headerCellRegistration = headerCellRegistration + let statsFooterCellRegistration = statsFooterCellRegistration + let emptyStateCellRegistration = emptyStateCellRegistration + return DataSource(collectionView: collectionView) { collectionView, indexPath, item in + switch item { + case .sortHeader(let sortHeader): + return collectionView.dequeueConfiguredReusableCell(using: sortHeaderCellRegistration, for: indexPath, item: sortHeader) + case .backupBanner: + return collectionView.dequeueConfiguredReusableCell(using: backupBannerCellRegistration, for: indexPath, item: item) + case .header(let section): + return collectionView.dequeueConfiguredReusableCell(using: headerCellRegistration, for: indexPath, item: section) + case .folder(let folder): + return collectionView.dequeueConfiguredReusableCell(using: folderCellRegistration, for: indexPath, item: folder) + case .favorite(let favorite): + return collectionView.dequeueConfiguredReusableCell(using: favoriteCellRegistration, for: indexPath, item: favorite) + case .statsFooter(let stats): + return collectionView.dequeueConfiguredReusableCell(using: statsFooterCellRegistration, for: indexPath, item: stats) + case .emptyState: + return collectionView.dequeueConfiguredReusableCell(using: emptyStateCellRegistration, for: indexPath, item: ()) + } + } + } + + func applySnapshot(animatingDifferences: Bool = false) { + switch screenMode { + case .root: + applyRootSnapshot(animatingDifferences: animatingDifferences) + case .folder(let folder, _): + applyFolderSnapshot(folder: folder, animatingDifferences: animatingDifferences) + } + } + + func closeFreeBackupBanner() { + UserDefaults.standard.set(true, forKey: Self.wasClosedFreeBackupFavoritesBannerKey) + applySnapshot(animatingDifferences: true) + } + + func favoriteFolders() -> [FavoriteFolderRow] { + OAFavoritesBridgeHelper.favoriteFolders().map { FavoriteFolderRow(item: $0) } + } + + func isNestedFolder(_ groupName: String, in parentGroupName: String) -> Bool { + guard !parentGroupName.isEmpty else { return false } + return groupName.hasPrefix(parentGroupName + "/") + } + + func hasSearchResults() -> Bool { + !searchFavoritePointRows(allFolders: favoriteFolders(), parentGroupName: searchParentGroupName).isEmpty + } + + func shouldHideSearchToolbar() -> Bool { + !collectionView.isEditing && (!isSearchActive || !hasSearchResults()) + } + + func clearSearchControllerText() { + if isRootFolder { + navigationController?.navigationBar.topItem?.searchController?.searchBar.text = "" + } else { + subfolderSearchController.searchBar.text = "" + } + } + + private func setFavoriteSortMode(_ sortMode: FavoriteSortMode) { + if isSearchResultsMode { + settings.searchFavoriteSortMode.set(sortMode.title) + } else { + var sortModes = settings.getFavoriteSortModes() + sortModes[currentSortEntryId] = sortMode.title + settings.saveFavoriteSortModes(sortModes) + } + + applySnapshot(animatingDifferences: false) + } + + private func isFavoriteSortModeKey(_ key: String, insideOrEqualTo groupName: String) -> Bool { + key == groupName || (!groupName.isEmpty && key.hasPrefix(groupName + "/")) + } + + private func makeSortAction(for sortMode: FavoriteSortMode) -> UIAction { + UIAction(title: sortMode.title, image: sortMode.image, state: currentSortMode == sortMode ? .on : .off) { [weak self] _ in + self?.setFavoriteSortMode(sortMode) + } + } + + private func applyRootSnapshot(animatingDifferences: Bool) { + let allFolders = favoriteFolders() + if isSearchResultsMode { + applySearchSnapshot(allFolders: allFolders, parentGroupName: nil, animatingDifferences: animatingDifferences) + return + } + + if allFolders.isEmpty { + applyEmptyStateSnapshot(animatingDifferences: animatingDifferences) + return + } + + var snapshot = Snapshot() + let foldersBySection = favoriteFoldersBySection(folders: allFolders).mapValues { FavoriteSortModeHelper.sortFoldersWithMode($0, mode: currentSortMode) } + let folderSections = rootSections(foldersBySection: foldersBySection) + let isPaymentBannerVisible = isAvailablePaymentBanner + let stats = folderStats(allFolders: allFolders, currentGroupName: nil) + var sections: [FavoriteListSection] = [.sortHeader] + if isPaymentBannerVisible { + sections.append(.backupBanner) + } + + sections.append(contentsOf: folderSections.map { FavoriteListSection.folderSection($0) }) + if stats != nil { + sections.append(.statsFooter) + } + + layoutSections = sections + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections(sections) + snapshot.appendItems([.sortHeader(currentSortHeader)], toSection: .sortHeader) + if isPaymentBannerVisible { + snapshot.appendItems([.backupBanner], toSection: .backupBanner) + } + + if let stats { + snapshot.appendItems([.statsFooter(stats)], toSection: .statsFooter) + } + + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + folderSections.forEach { section in + let headerItem = FavoriteListItem.header(section) + let folderItems = (foldersBySection[section] ?? []).map(FavoriteListItem.folder) + var sectionSnapshot = NSDiffableDataSourceSectionSnapshot() + sectionSnapshot.append([headerItem]) + sectionSnapshot.append(folderItems, to: headerItem) + sectionSnapshot.expand([headerItem]) + dataSource.apply(sectionSnapshot, to: .folderSection(section), animatingDifferences: animatingDifferences) + } + } + + private func applyFolderSnapshot(folder: FavoriteFolderRow, animatingDifferences: Bool) { + let allFolders = favoriteFolders() + if isSearchResultsMode { + applySearchSnapshot(allFolders: allFolders, parentGroupName: folder.bridgeItem.groupName, animatingDifferences: animatingDifferences) + return + } + + let folders = FavoriteSortModeHelper.sortFoldersWithMode(directFavoriteFolders(allFolders, parentGroupName: folder.bridgeItem.groupName).filter { matchesSearch($0.title) }, mode: currentSortMode) + let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(OAFavoritesBridgeHelper.favoritePoints(forGroupName: folder.bridgeItem.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.bridgeItem.address) }, mode: currentSortMode) + if favorites.isEmpty && folders.isEmpty { + applyEmptyStateSnapshot(animatingDifferences: animatingDifferences) + return + } + var snapshot = Snapshot() + let stats = folderStats(allFolders: allFolders, currentGroupName: folder.bridgeItem.groupName) + layoutSections = stats == nil ? [.sortHeader, .content] : [.sortHeader, .content, .statsFooter] + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections(layoutSections) + snapshot.appendItems([.sortHeader(currentSortHeader)], toSection: .sortHeader) + snapshot.appendItems(folders.map(FavoriteListItem.folder), toSection: .content) + snapshot.appendItems(favorites.map(FavoriteListItem.favorite), toSection: .content) + if let stats { + snapshot.appendItems([.statsFooter(stats)], toSection: .statsFooter) + } + + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + } + + private func applySearchSnapshot(allFolders: [FavoriteFolderRow], parentGroupName: String?, animatingDifferences: Bool) { + let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(searchFavoritePointRows(allFolders: allFolders, parentGroupName: parentGroupName), mode: currentSortMode) + if favorites.isEmpty { + applyEmptyStateSnapshot(animatingDifferences: animatingDifferences) + return + } + + var snapshot = Snapshot() + layoutSections = [.sortHeader, .content] + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections(layoutSections) + snapshot.appendItems([.sortHeader(currentSortHeader)], toSection: .sortHeader) + snapshot.appendItems(favorites.map(FavoriteListItem.favorite), toSection: .content) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + } + + private func applyEmptyStateSnapshot(animatingDifferences: Bool) { + var snapshot = Snapshot() + layoutSections = [] + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections([.emptyState]) + snapshot.appendItems([.emptyState]) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + } + + private func favoriteFoldersBySection(folders allFolders: [FavoriteFolderRow]) -> [FavoriteFolderSection: [FavoriteFolderRow]] { + let folders = directFavoriteFolders(allFolders, parentGroupName: nil).filter { matchesSearch($0.title) } + return [.pinned: folders.filter { $0.isPinned }, .visible: folders.filter { $0.isVisible && !$0.isPinned }, .hidden: folders.filter { !$0.isVisible && !$0.isPinned }] + } + + private func rootSections(foldersBySection: [FavoriteFolderSection: [FavoriteFolderRow]]) -> [FavoriteFolderSection] { + var sections: [FavoriteFolderSection] = [] + if !(foldersBySection[.pinned] ?? []).isEmpty { + sections.append(.pinned) + } + + if !isSearchResultsMode || !(foldersBySection[.visible] ?? []).isEmpty { + sections.append(.visible) + } + + if !(foldersBySection[.hidden] ?? []).isEmpty { + sections.append(.hidden) + } + + return sections + } + + private func folderStats(allFolders: [FavoriteFolderRow], currentGroupName: String?) -> FavoriteFolderStats? { + guard !isSearchResultsMode else { return nil } + guard let currentGroupName else { + let pointsCount = allFolders.reduce(0) { $0 + $1.bridgeItem.pointsCount } + guard !allFolders.isEmpty || pointsCount > 0 else { return nil } + let fileSize = allFolders.reduce(Int64(0)) { $0 + $1.bridgeItem.fileSize } + return FavoriteFolderStats(foldersCount: allFolders.count, pointsCount: Int(pointsCount), fileSize: fileSize) + } + + let nestedFolders = allFolders.filter { isNestedFolder($0.bridgeItem.groupName, in: currentGroupName) } + let currentFolder = allFolders.first { $0.bridgeItem.groupName == currentGroupName } + let pointsCount = currentFolder?.bridgeItem.subtreePointsCount ?? nestedFolders.reduce(0) { $0 + $1.bridgeItem.pointsCount } + guard !nestedFolders.isEmpty || pointsCount > 0 else { return nil } + let fileSize = (currentFolder?.bridgeItem.fileSize ?? 0) + nestedFolders.reduce(Int64(0)) { $0 + $1.bridgeItem.fileSize } + return FavoriteFolderStats(foldersCount: nestedFolders.count, pointsCount: Int(pointsCount), fileSize: fileSize) + } + + private func directFavoriteFolders(_ folders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoriteFolderRow] { + folders.filter { isDirectFolder($0.bridgeItem.groupName, parentGroupName: parentGroupName) } + } + + private func isDirectFolder(_ groupName: String, parentGroupName: String?) -> Bool { + guard let parentGroupName else { return groupName.isEmpty || !groupName.contains("/") } + guard !parentGroupName.isEmpty && groupName.hasPrefix(parentGroupName + "/") else { return false } + let childPath = groupName.dropFirst(parentGroupName.count + 1) + return !childPath.isEmpty && !childPath.contains("/") + } + + private func matchesSearch(_ text: String?) -> Bool { + guard !searchText.isEmpty else { return true } + return text?.range(of: searchText, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive], locale: Locale.current) != nil + } + + private func searchFavoritePointRows(allFolders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoritePointRow] { + favoritePointRows(allFolders: allFolders, parentGroupName: parentGroupName).filter { matchesSearch($0.title) } + } +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift new file mode 100644 index 0000000000..3b00f7ef5c --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift @@ -0,0 +1,267 @@ +// +// FavoriteListViewController+Delegates.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +extension FavoriteListViewController: UICollectionViewDelegate { + func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + guard let item = dataSource.itemIdentifier(for: indexPath) else { return } + switch item { + case .folder(let folder): + if collectionView.isEditing { + updateSelection(at: indexPath) + updateSelectionUI() + return + } + let viewController = FavoriteListViewController(frame: view.bounds, screenMode: .folder(folder, previousTitle: normalTitle)) + viewController.myPlacesDelegate = myPlacesDelegate + navigationController?.pushViewController(viewController, animated: true) + case .favorite(let favorite): + if collectionView.isEditing { + updateSelection(at: indexPath) + updateSelectionUI() + return + } + OAFavoritesBridgeHelper.openFavoritePoint(withIdentifier: favorite.bridgeItem.identifier) + default: + break + } + + collectionView.deselectItem(at: indexPath, animated: true) + } + + func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { + guard collectionView.isEditing else { return } + updateSelection(at: indexPath) + updateSelectionUI() + } + + func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { + isContextMenuVisible = true + return nil + } + + func collectionView(_ collectionView: UICollectionView, willEndContextMenuInteraction configuration: UIContextMenuConfiguration, animator: (any UIContextMenuInteractionAnimating)?) { + animator?.addCompletion { [weak self] in + guard let self else { return } + self.isContextMenuVisible = false + if self.shouldReloadCollectionView { + self.shouldReloadCollectionView = false + self.updateDistanceAndDirection(true) + } + } + } + + func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { + guard !collectionView.isEditing, let item = dataSource.itemIdentifier(for: indexPath) else { return nil } + let menuProvider: UIContextMenuActionProvider = { [weak self] _ in + guard let self else { return nil } + switch item { + case .folder(let folder): + return self.makeFolderContextMenu(for: folder, indexPath: indexPath) + case .favorite(let favorite): + return self.makePointContextMenu(for: favorite, indexPath: indexPath) + default: + return nil + } + } + + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: menuProvider) + } +} + +extension FavoriteListViewController: OAShareMenuDelegate { + func onCopy(_ type: OAShareMenuActivityType) { + guard let pointToShare else { return } + switch type { + case .clipboard: + copyFavoritePointShareText(OAFavoritesBridgeHelper.sharePoiURLString(forFavoritePoint: pointToShare)) + case .copyAddress: + if let address = pointToShare.address, !address.isEmpty { + copyFavoritePointShareText(address) + } else { + OAUtilities.showToast(localizedString("no_address_found"), details: nil, duration: 4, in: view) + } + case .copyPOIName: + if !pointToShare.title.isEmpty { + copyFavoritePointShareText(pointToShare.title) + } else { + OAUtilities.showToast(localizedString("toast_empty_name_error"), details: nil, duration: 4, in: view) + } + case .copyCoordinates: + copyFavoritePointShareText(OAFavoritesBridgeHelper.formattedCoordinates(forFavoritePoint: pointToShare)) + case .geo: + copyFavoritePointShareText(OAFavoritesBridgeHelper.geoURLString(forFavoritePoint: pointToShare)) + default: + break + } + } + + func copyFavoritePointShareText(_ text: String) { + UIPasteboard.general.string = text + OAUtilities.showToast(localizedString("copied_to_clipboard"), details: text, duration: 4, in: view) + } +} + +extension FavoriteListViewController: MyPlacesSearchable, UISearchResultsUpdating, UISearchBarDelegate { + func updateSearchResults(for searchController: UISearchController) { + searchResults(for: searchController) + } + + func searchResults(for searchController: UISearchController) { + isSearchActive = searchController.isActive + if isSearchActive || !isSelectionModeInSearch { + searchText = searchController.searchBar.searchTextField.text ?? "" + } + updateSegmentedControlVisibility() + configureToolbar() + navigationController?.setToolbarHidden(shouldHideSearchToolbar(), animated: true) + applySnapshot(animatingDifferences: false) + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + isSearchActive = false + if !isSelectionModeInSearch { + searchText = "" + } + updateSegmentedControlVisibility() + configureToolbar() + navigationController?.setToolbarHidden(!collectionView.isEditing, animated: true) + applySnapshot(animatingDifferences: false) + } +} + +extension FavoriteListViewController: OAEditColorViewControllerDelegate { + func colorChanged() { + guard let colorController else { return } + defer { + self.colorController = nil + } + + guard let selectedItems = collectionView.indexPathsForSelectedItems, !selectedItems.isEmpty else { return } + if colorController.saveChanges { + OAFavoritesBridgeHelper.changeFavoriteItems(bridgeItems(for: selectedItems), colorIndex: colorController.colorIndex) + } + + setEditing(false) + applySnapshot(animatingDifferences: true) + } +} + +extension FavoriteListViewController: OAEditGroupViewControllerDelegate { + func groupChanged() { + guard let groupController else { return } + defer { + self.groupController = nil + favoriteItemsToMove = nil + } + + guard groupController.saveChanges else { return } + + let targetGroupName = groupController.groupName ?? "" + guard let favoriteItemsToMove else { return } + createFavoriteMoveTargetGroupIfNeeded(targetGroupName, favoriteItems: favoriteItemsToMove) + OAFavoritesBridgeHelper.moveFavoriteItems(favoriteItemsToMove, toGroupName: targetGroupName) + updateFavoriteSortModeKeysAfterMove(favoriteItemsToMove, toGroupName: targetGroupName) + setEditing(false) + applySnapshot(animatingDifferences: true) + } +} + +extension FavoriteListViewController: OAOpenAddTrackDelegate { + func onFileSelected(_ gpxFilePath: String) { + if let addToTrackFavoriteItems { + OAFavoritesBridgeHelper.addFavoriteItems(toTrack: addToTrackFavoriteItems, gpxFileName: gpxFilePath) + self.addToTrackFavoriteItems = nil + } else if let addToTrackGroupName { + OAFavoritesBridgeHelper.addFavoriteGroup(toTrack: addToTrackGroupName, gpxFileName: gpxFilePath) + self.addToTrackGroupName = nil + } + } +} + +extension FavoriteListViewController: OAEditorDelegate { + func addNewItem(withName name: String?, iconName: String, color: UIColor, backgroundIconName: String) { + guard OAFavoritesBridgeHelper.addFavoriteGroup(name ?? "", + parentGroupName: parentGroupName, + iconName: iconName, + color: color, + backgroundIconName: backgroundIconName) else { return } + applySnapshot(animatingDifferences: true) + } + + func onEditorUpdated() { + if let oldGroupName = favoriteGroupAppearanceGroupName, let newGroupName = favoriteGroupAppearanceEditor?.editName { + renameFavoriteSortModeKeys(from: oldGroupName, to: newGroupName) + } + + favoriteGroupAppearanceGroupName = nil + favoriteGroupAppearanceEditor = nil + OAFavoritesBridgeHelper.invalidateFavoriteFoldersCache() + applySnapshot(animatingDifferences: true) + } + + func selectColorItem(_ colorItem: PaletteItemSolid) {} + + @discardableResult + func addAndGetNewColorItem(_ color: UIColor) -> PaletteItemSolid { + guard let newColorItem = appearanceCollection.addNewSelectedColor(color) else { + return appearanceCollection.defaultPointColorItem() + } + + return newColorItem + } + + func changeColorItem(_ colorItem: PaletteItemSolid, with color: UIColor) { + appearanceCollection.changeColor(colorItem, newColor: color) + } + + @discardableResult + func duplicateColorItem(_ colorItem: PaletteItemSolid) -> PaletteItemSolid { + guard let duplicatedColorItem = appearanceCollection.duplicateColor(colorItem) else { + return colorItem + } + + return duplicatedColorItem + } + + func deleteColorItem(_ colorItem: PaletteItemSolid) { + appearanceCollection.deleteColor(colorItem) + } +} + +extension FavoriteListViewController: OAEditPointViewControllerDelegate { + func saveTapped() { + OAFavoritesBridgeHelper.invalidateFavoriteFoldersCache() + applySnapshot() + } +} + +extension FavoriteListViewController: UIDocumentPickerDelegate { + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url = urls.first else { return } + OARootViewController.instance().import(asFavorites: url) + } +} + +extension FavoriteListViewController: BackupBannerCollectionViewCellDelegate { + func didClose() { + closeFreeBackupBanner() + } + + func didOpenOsmAndCloud() { + navigationController?.pushViewController(OACloudIntroductionViewController(), animated: true) + } + + func backupBannerHeight(_ banner: FreeBackupBanner, fittingWidth: CGFloat) -> CGFloat { + let fallbackWidth = collectionView.bounds.width - collectionView.layoutMargins.left - collectionView.layoutMargins.right + let bannerWidth = fittingWidth > 0.0 ? fittingWidth : fallbackWidth + let textWidth = max(0.0, bannerWidth - CGFloat(banner.leadingTrailingOffset)) + let titleHeight = OAUtilities.calculateTextBounds(banner.titleLabel.text ?? "", width: textWidth, font: banner.titleLabel.font).height + let descriptionHeight = OAUtilities.calculateTextBounds(banner.descriptionLabel.text ?? "", width: textWidth, font: banner.descriptionLabel.font).height + return ceil(CGFloat(banner.defaultFrameHeight) + titleHeight + descriptionHeight) + } +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift new file mode 100644 index 0000000000..335c5f22be --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift @@ -0,0 +1,162 @@ +// +// FavoriteListViewController+Models.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +enum ScreenMode { + case root + case folder(FavoriteFolderRow, previousTitle: String) +} + +enum FavoriteFolderSection: Hashable { + case pinned + case visible + case hidden + + var title: String { + switch self { + case .pinned: localizedString("shared_string_pinned") + case .visible: localizedString("shared_string_visible") + case .hidden: localizedString("shared_string_hidden") + } + } +} + +enum FavoriteListSection: Hashable { + case sortHeader + case backupBanner + case folderSection(FavoriteFolderSection) + case content + case statsFooter + case emptyState +} + +enum FavoriteListItem: Hashable { + case sortHeader(FavoriteSortHeader) + case backupBanner + case header(FavoriteFolderSection) + case folder(FavoriteFolderRow) + case favorite(FavoritePointRow) + case statsFooter(FavoriteFolderStats) + case emptyState + + var selectionItem: FavoriteSelectionItem? { + switch self { + case .folder(let folder): + return .folder(folder.bridgeItem.groupName) + case .favorite(let favorite): + return .favorite(favorite.bridgeItem.identifier) + default: + return nil + } + } +} + +enum FavoriteSelectionItem: Hashable { + case folder(String) + case favorite(String) +} + +struct FavoriteSortHeader: Hashable { + let sortMode: FavoriteSortMode + let includesDistanceSortModes: Bool +} + +struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { + private static let subtitleDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.setLocalizedDateFormatFromTemplate("dMMM") + return formatter + }() + + let bridgeItem: OAFavoriteFolderBridgeItem + + var title: String { bridgeItem.title } + + var isVisible: Bool { bridgeItem.isVisible } + + var isPinned: Bool { bridgeItem.isPinned } + + var lastModified: Date? { bridgeItem.lastModifiedDate } + + var subtitle: String { + let pointsText = "\(bridgeItem.subtreePointsCount) \(localizedString("shared_string_gpx_points").lowercased())" + guard let lastModified else { return pointsText + "." } + return String(format: localizedString("ltr_or_rtl_combine_via_comma"), Self.subtitleDateFormatter.string(from: lastModified), pointsText) + "." + } + + var iconName: String { + isVisible ? "ic_custom_folder" : "ic_custom_folder_hidden_outlined" + } + + var iconColor: UIColor { + isVisible ? (bridgeItem.color ?? .iconColorSelected) : .iconColorSecondary + } + + var titleColor: UIColor { + isVisible ? .textColorPrimary : .textColorSecondary + } + + var titleFont: UIFont { + guard !isVisible, let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitItalic) else { return .preferredFont(forTextStyle: .body) } + return UIFont(descriptor: descriptor, size: 0) + } + + init(item: OAFavoriteFolderBridgeItem) { + bridgeItem = item + } +} + +struct FavoritePointRow: Hashable, FavoriteSortablePoint { + let bridgeItem: OAFavoritePointBridgeItem + + var title: String { bridgeItem.title } + + var distance: CLLocationDistance? { bridgeItem.distance?.doubleValue } + + var lastModified: Date? { bridgeItem.timestampDate } + + var titleColor: UIColor { + bridgeItem.isVisible ? .textColorPrimary : .textColorSecondary + } + + var titleFont: UIFont { + guard !bridgeItem.isVisible, let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitItalic) else { return .preferredFont(forTextStyle: .body) } + return UIFont(descriptor: descriptor, size: 0) + } + + init(item: OAFavoritePointBridgeItem) { + bridgeItem = item + } +} + +struct FavoriteFolderStats: Hashable { + let foldersCount: Int + let pointsCount: Int + let fileSize: Int64 + + var text: String { + var parts: [String] = [] + if foldersCount > 0 { + parts.append("\(localizedString("shared_string_folders").lowercased()) \(foldersCount)") + } + + parts.append("\(localizedString("shared_string_gpx_points").lowercased()) \(pointsCount)") + parts.append("\(localizedString("shared_string_size").lowercased()) \(ByteCountFormatter.string(fromByteCount: fileSize, countStyle: .file))") + let text = parts.joined(separator: ", ") + "." + return text.prefix(1).uppercased() + String(text.dropFirst()) + } +} + +final class FavoriteListCell: UICollectionViewListCell { + private static let rowHeight: CGFloat = 68.0 + + override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes { + let attributes = super.preferredLayoutAttributesFitting(layoutAttributes) + attributes.frame.size.height = max(Self.rowHeight, attributes.frame.height) + return attributes + } +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift new file mode 100644 index 0000000000..8a95b61aee --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift @@ -0,0 +1,406 @@ +// +// FavoriteListViewController.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +final class FavoriteListViewController: UIViewController { + typealias DataSource = UICollectionViewDiffableDataSource + typealias Snapshot = NSDiffableDataSourceSnapshot + typealias CellRegistration = UICollectionView.CellRegistration + typealias RowCellRegistration = UICollectionView.CellRegistration + + static let imageSize: CGFloat = 30.0 + static let favoriteIconSize: CGFloat = 36.0 + static let sortHeaderHeight: CGFloat = 44.0 + static let sortHeaderLeadingInset: CGFloat = 16.0 + static let navigationTitleFontSize: CGFloat = 17.0 + static let navigationTitleMaximumSize: CGFloat = 22.0 + static let navigationSubtitleFontSize: CGFloat = 12.0 + static let navigationSubtitleMaximumSize: CGFloat = 18.0 + static let wasClosedFreeBackupFavoritesBannerKey = "wasClosedFreeBackupFavoritesBanner" + + let screenMode: ScreenMode + let settings = OAAppSettings.sharedManager() + var layoutSections: [FavoriteListSection] = [] + let appearanceCollection: OAGPXAppearanceCollection = .sharedInstance() + var groupController: OAEditGroupViewController? + var colorController: OAEditColorViewController? + var favoriteItemsToMove: [Any]? + var favoriteGroupAppearanceGroupName: String? + var favoriteGroupAppearanceEditor: OAFavoriteGroupEditorViewController? + var addToTrackGroupName: String? + var addToTrackFavoriteItems: [Any]? + var pointToShare: OAFavoritePointBridgeItem? + var searchText = "" + var isSearchActive = false + var isSelectionModeInSearch = false + var lastDistanceDirectionUpdate: TimeInterval = 0.0 + var isContextMenuVisible = false + var shouldReloadCollectionView = false + var locationUpdateObserver: OAAutoObserverProxy? + var headingUpdateObserver: OAAutoObserverProxy? + var isSearchResultsMode: Bool { + isSearchActive || isSelectionModeInSearch + } + var isAvailablePaymentBanner: Bool { + isRootFolder && !isSearchResultsMode && !UserDefaults.standard.bool(forKey: Self.wasClosedFreeBackupFavoritesBannerKey) && !OAIAPHelper.isOsmAndProAvailable() && !OABackupHelper.sharedInstance().isRegistered() + } + var isRootFolder: Bool { + guard case .root = screenMode else { return false } + return true + } + var normalTitle: String { + switch screenMode { + case .root: localizedString("shared_string_favorites") + case .folder(let folder, _): folder.title + } + } + var parentGroupName: String? { + guard case .folder(let folder, _) = screenMode, !folder.bridgeItem.groupName.isEmpty else { return nil } + return folder.bridgeItem.groupName + } + var searchParentGroupName: String? { + guard case .folder(let folder, _) = screenMode else { return nil } + return folder.bridgeItem.groupName + } + var currentSortMode: FavoriteSortMode { + isSearchResultsMode ? searchFavoriteSortMode() : favoriteSortMode() + } + var currentSortHeader: FavoriteSortHeader { + FavoriteSortHeader(sortMode: currentSortMode, includesDistanceSortModes: isSearchResultsMode || !isRootFolder) + } + var currentSortEntryId: String { + parentGroupName ?? "" + } + + lazy var collectionView: UICollectionView = { + let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) + collectionView.backgroundColor = .clear + collectionView.tintColor = .iconColorActive + collectionView.delegate = self + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.allowsMultipleSelectionDuringEditing = true + return collectionView + }() + lazy var subfolderSearchController: UISearchController = { + let searchController = UISearchController(searchResultsController: nil) + searchController.searchResultsUpdater = self + searchController.searchBar.delegate = self + searchController.obscuresBackgroundDuringPresentation = false + searchController.searchBar.searchTextField.placeholder = localizedString("search_activity") + return searchController + }() + lazy var selectionManager: SelectionManager = { + let items = collectionView.indexPathsForVisibleItems.compactMap { + dataSource.itemIdentifier(for: $0)?.selectionItem + } + return SelectionManager(allItems: items) + }() + lazy var dataSource: DataSource = makeDataSource() + + weak var myPlacesDelegate: MyPlacesDelegate? + + private var normalSubtitle: String { + switch screenMode { + case .root: localizedString("shared_string_my_places") + case .folder(_, let previousTitle): previousTitle + } + } + + convenience init(frame: CGRect) { + self.init(frame: frame, screenMode: .root) + } + + init(frame: CGRect, screenMode: ScreenMode) { + self.screenMode = screenMode + super.init(nibName: nil, bundle: nil) + view.frame = frame + } + + required init?(coder: NSCoder) { + screenMode = .root + super.init(coder: coder) + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .viewBg + configureCollectionView() + definesPresentationContext = true + NotificationCenter.default.addObserver(self, selector: #selector(favoriteDataDidChange), name: .favoriteImportViewControllerDidDismiss, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(productPurchased), name: Notification.Name(NSNotification.Name.OAIAPProductPurchased.rawValue), object: nil) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + definesPresentationContext = true + configureNavigation() + navigationController?.setToolbarHidden(true, animated: false) + configureToolbar() + applySnapshot() + registerDistanceAndDirectionObservers() + updateDistanceAndDirection(true) + } + + override func viewWillDisappear(_ animated: Bool) { + unregisterDistanceAndDirectionObservers() + if !isRootFolder { + navigationItem.searchController = nil + navigationController?.setNavigationBarHidden(true, animated: false) + } + + definesPresentationContext = false + super.viewWillDisappear(animated) + } + + func updateDistanceAndDirection(_ forceUpdate: Bool) { + guard Thread.isMainThread else { + DispatchQueue.main.async { [weak self] in + self?.updateDistanceAndDirection(forceUpdate) + } + return + } + + if isContextMenuVisible { + shouldReloadCollectionView = true + return + } + + guard !collectionView.isEditing + && OsmAndApp.swiftInstance().locationServices.lastKnownLocation != nil + && dataSource.snapshot().itemIdentifiers.contains(where: { item in + if case .favorite = item { + return true + } + return false + }) else { + return + } + + let currentTime = Date.now.timeIntervalSince1970 + guard forceUpdate || currentTime - lastDistanceDirectionUpdate >= 0.3 else { return } + lastDistanceDirectionUpdate = currentTime + if currentSortMode.isDistanceOriented { + applySnapshot(animatingDifferences: false) + } else { + updateVisibleFavoriteCellsDistanceAndDirection() + } + } + + func listCellBackgroundConfiguration() -> UIBackgroundConfiguration { + var configuration = UIBackgroundConfiguration.listGroupedCell() + configuration.backgroundColor = .groupBg + return configuration + } + + func configureNavigation() { + navigationController?.setNavigationBarHidden(false, animated: false) + if !isRootFolder { + let appearance = UINavigationBarAppearance() + appearance.backgroundColor = .viewBg + navigationController?.navigationBar.standardAppearance = appearance + navigationController?.navigationBar.scrollEdgeAppearance = appearance + navigationController?.navigationBar.tintColor = .iconColorActive + } + + navigationController?.navigationBar.prefersLargeTitles = false + configureNavigationButtons() + configureSearchVisibility() + updateNavigationBarTitle() + updateSegmentedControlVisibility() + } + + func configureToolbar() { + guard !isSearchActive || collectionView.isEditing else { + if hasSearchResults() { + configureSearchToolbar() + } + return + } + + let isSelected = collectionView.indexPathsForSelectedItems?.isEmpty == false + let fixedSpacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) + let actionsFixedSpacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) + let flexibleSpacer = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let shareButton = UIBarButtonItem(image: .icCustomExportOutlined, style: .plain, target: self, action: #selector(shareButtonClicked)) + let moveButton = UIBarButtonItem(image: .icCustomFolderMoveOutlined, style: .plain, target: self, action: #selector(moveButtonClicked)) + let actionsButton = UIBarButtonItem(image: .icCustomOverflowMenuStroke, style: .plain, target: nil, action: nil) + actionsButton.menu = makeAdditionalContextMenu() + let deleteButton = UIBarButtonItem(image: .icCustomTrashOutlined, style: .plain, target: self, action: #selector(deleteButtonClicked)) + deleteButton.tintColor = .iconColorDisruptive + let items = [shareButton, fixedSpacer, moveButton, actionsFixedSpacer, actionsButton, flexibleSpacer, deleteButton] + items.forEach { $0.isEnabled = isSelected } + if isRootFolder { + myPlacesDelegate?.updateToolbar?(with: items) + } else { + toolbarItems = items + } + } + + func updateSelectionUI() { + updateNavigationBarTitle() + configureNavigationButtons() + configureToolbar() + } + + func updateSegmentedControlVisibility() { + myPlacesDelegate?.updateSegmentedControlVisibility(isRootFolder && !collectionView.isEditing && !isSearchResultsMode) + } + + private func registerDistanceAndDirectionObservers() { + unregisterDistanceAndDirectionObservers() + let app: OsmAndAppProtocol = OsmAndApp.swiftInstance() + let updateDistanceAndDirectionSelector = #selector(updateDistanceAndDirection as () -> Void) + locationUpdateObserver = OAAutoObserverProxy(self, + withHandler: updateDistanceAndDirectionSelector, + andObserve: app.locationServices.updateLocationObserver) + headingUpdateObserver = OAAutoObserverProxy(self, + withHandler: updateDistanceAndDirectionSelector, + andObserve: app.locationServices.updateHeadingObserver) + } + + private func unregisterDistanceAndDirectionObservers() { + locationUpdateObserver?.detach() + locationUpdateObserver = nil + headingUpdateObserver?.detach() + headingUpdateObserver = nil + } + + private func configureCollectionView() { + view.addSubview(collectionView) + NSLayoutConstraint.activate([collectionView.topAnchor.constraint(equalTo: view.topAnchor), collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor), collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor), collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor)]) + } + + private func createLayout() -> UICollectionViewLayout { + UICollectionViewCompositionalLayout { [weak self] sectionIndex, environment in + guard let self else { return nil } + var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + let section = self.layoutSections.indices.contains(sectionIndex) ? self.layoutSections[sectionIndex] : nil + if section == .sortHeader { + return self.sortHeaderLayoutSection() + } + + if section == .statsFooter { + return self.statsFooterLayoutSection() + } + + if case .folderSection = section, self.isRootFolder { + configuration.headerMode = .firstItemInSection + } + + return NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: environment) + } + } + + private func sortHeaderLayoutSection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(Self.sortHeaderHeight)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item]) + let section = NSCollectionLayoutSection(group: group) + section.contentInsets.leading = Self.sortHeaderLeadingInset + return section + } + + private func statsFooterLayoutSection() -> NSCollectionLayoutSection { + let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(64.0)) + let item = NSCollectionLayoutItem(layoutSize: itemSize) + let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item]) + return NSCollectionLayoutSection(group: group) + } + + private func configureNavigationButtons() { + let targetNavigationItem = isRootFolder ? navigationController?.navigationBar.topItem : navigationItem + if collectionView.isEditing { + let cancelButton = UIBarButtonItem(title: localizedString("shared_string_cancel"), style: .plain, target: self, action: #selector(cancelButtonPressed)) + cancelButton.accessibilityLabel = localizedString("shared_string_cancel") + let selectAllTitle = localizedString(selectionManager.areAllSelected ? "shared_string_deselect_all" : "shared_string_select_all") + let selectAllButton = UIBarButtonItem(title: selectAllTitle, style: .plain, target: self, action: #selector(selectAllButtonPressed)) + selectAllButton.accessibilityLabel = selectAllTitle + targetNavigationItem?.leftBarButtonItem = cancelButton + targetNavigationItem?.rightBarButtonItems = [selectAllButton] + if isRootFolder { + myPlacesDelegate?.showBackButton(false) + } else { + self.navigationItem.hidesBackButton = true + } + } else { + let selectButton = UIBarButtonItem(title: localizedString("shared_string_select"), style: .plain, target: self, action: #selector(selectButtonPressed)) + selectButton.accessibilityLabel = localizedString("shared_string_select") + let actionsButton = UIBarButtonItem(image: .icNavbarOverflowMenuOutlined, menu: makeActionsMenu()) + actionsButton.accessibilityLabel = localizedString("shared_string_actions") + targetNavigationItem?.leftBarButtonItem = nil + targetNavigationItem?.rightBarButtonItems = [actionsButton, selectButton] + if isRootFolder { + myPlacesDelegate?.showBackButton(true) + } else { + self.navigationItem.hidesBackButton = false + } + } + } + + private func configureSearchVisibility() { + guard !isRootFolder else { + navigationController?.navigationBar.topItem?.hidesSearchBarWhenScrolling = false + return + } + + if #available(iOS 26.0, *), !OAUtilities.isIPad() { + navigationItem.preferredSearchBarPlacement = .stacked + } + + navigationItem.hidesSearchBarWhenScrolling = false + navigationItem.searchController = collectionView.isEditing ? nil : subfolderSearchController + } + + private func configureSearchToolbar() { + let selectButton = UIBarButtonItem(title: localizedString("shared_string_select"), style: .plain, target: self, action: #selector(searchSelectButtonPressed)) + selectButton.accessibilityLabel = localizedString("shared_string_select") + let items = [selectButton] + if isRootFolder { + myPlacesDelegate?.updateToolbar?(with: items) + } else { + toolbarItems = items + } + } + + private func updateNavigationBarTitle() { + if collectionView.isEditing { + let selectedItems = bridgeItems(for: collectionView.indexPathsForSelectedItems ?? []) + guard !selectedItems.isEmpty else { + setNavigationTitle("", subtitle: "", hideSubtitle: true) + return + } + + let pointsCount = selectedFavoritePointsCount(for: selectedItems) + let subtitle = "\(pointsCount) \(localizedString("shared_string_gpx_points").lowercased())" + setNavigationTitle("\(selectedItems.count)", subtitle: subtitle, hideSubtitle: false) + } else { + setNavigationTitle(normalTitle, subtitle: normalSubtitle, hideSubtitle: false) + } + } + + private func setNavigationTitle(_ title: String, subtitle: String, hideSubtitle: Bool) { + if isRootFolder { + if myPlacesDelegate?.updateTitle?(title, subtitle: subtitle, hideSubtitle: hideSubtitle) == nil { + myPlacesDelegate?.updateTitle?(title, hideSubtitle: hideSubtitle) + } + } else { + navigationItem.setStackViewWithTitle(title, titleColor: .textColorPrimary, titleFont: .scaledSystemFont(ofSize: Self.navigationTitleFontSize, weight: .semibold, maximumSize: Self.navigationTitleMaximumSize), subtitle: hideSubtitle ? "" : subtitle, subtitleColor: .textColorSecondary, subtitleFont: .scaledSystemFont(ofSize: Self.navigationSubtitleFontSize, maximumSize: Self.navigationSubtitleMaximumSize)) + } + } + + deinit { + unregisterDistanceAndDirectionObservers() + NotificationCenter.default.removeObserver(self, name: .favoriteImportViewControllerDidDismiss, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name(NSNotification.Name.OAIAPProductPurchased.rawValue), object: nil) + } +} + +extension Notification.Name { + static let favoriteImportViewControllerDidDismiss = Notification.Name("OAFavoriteImportViewControllerDidDismissNotification") +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteSortModeHelper.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteSortModeHelper.swift new file mode 100644 index 0000000000..d49ee8a7d4 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteSortModeHelper.swift @@ -0,0 +1,158 @@ +// +// FavoriteSortModeHelper.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 05.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import UIKit +import CoreLocation + +protocol FavoriteSortableFolder { + var title: String { get } + var isVisible: Bool { get } + var isPinned: Bool { get } + var lastModified: Date? { get } +} + +protocol FavoriteSortablePoint { + var title: String { get } + var distance: CLLocationDistance? { get } + var lastModified: Date? { get } +} + +@objc enum FavoriteSortMode: Int, CaseIterable { + case lastModified + case nameAZ + case nameZA + case nearest + case farthest + case newestDateFirst + case oldestDateFirst + + var title: String { + switch self { + case .lastModified: return localizedString("sort_last_modified") + case .nameAZ: return localizedString("track_sort_az") + case .nameZA: return localizedString("track_sort_za") + case .nearest: return localizedString("distance_nearest") + case .farthest: return localizedString("distance_farthest") + case .newestDateFirst: return localizedString("newest_date_first") + case .oldestDateFirst: return localizedString("oldest_date_first") + } + } + + var image: UIImage? { + switch self { + case .lastModified: return .icCustomLastModified + case .nameAZ: return .icCustomSortNameAscending + case .nameZA: return .icCustomSortNameDescending + case .nearest: return .icCustomSortNear + case .farthest: return .icCustomSortFar + case .newestDateFirst: return .icCustomSortDateNewest + case .oldestDateFirst: return .icCustomSortDateOldest + } + } + + var isDateOriented: Bool { + self == .lastModified || self == .newestDateFirst || self == .oldestDateFirst + } + + var isDistanceOriented: Bool { + self == .nearest || self == .farthest + } + + static func byTitle(_ title: String) -> FavoriteSortMode { + allCases.first { $0.title == title } ?? .nameAZ + } +} + +@objc final class FavoriteSortModeHelper: NSObject { + static func sortFoldersWithMode(_ folders: [Folder], mode: FavoriteSortMode) -> [Folder] { + folders.sorted { compareFolders($0, $1, mode: mode) == .orderedAscending } + } + + static func sortFavoritePointsWithMode(_ points: [Point], mode: FavoriteSortMode) -> [Point] { + points.sorted { compareFavoritePoints($0, $1, mode: mode) == .orderedAscending } + } + + static func defaultSortMode() -> FavoriteSortMode { + .nameAZ + } + + @objc static func defaultSortModeTitle() -> String { + defaultSortMode().title + } + + private static func compareFolders(_ lhs: Folder, _ rhs: Folder, mode: FavoriteSortMode) -> ComparisonResult { + if lhs.isPinned != rhs.isPinned { + return lhs.isPinned ? .orderedAscending : .orderedDescending + } + + if lhs.isVisible != rhs.isVisible { + return lhs.isVisible ? .orderedAscending : .orderedDescending + } + + switch mode { + case .lastModified, .newestDateFirst: + return compareDates(lhs.lastModified, rhs.lastModified, newestFirst: true, lhsTitle: lhs.title, rhsTitle: rhs.title) + case .oldestDateFirst: + return compareDates(lhs.lastModified, rhs.lastModified, newestFirst: false, lhsTitle: lhs.title, rhsTitle: rhs.title) + case .nameAZ: + return compareTitles(lhs.title, rhs.title) + case .nameZA: + return compareTitles(rhs.title, lhs.title) + case .nearest, .farthest: + return .orderedSame + } + } + + private static func compareFavoritePoints(_ lhs: Point, _ rhs: Point, mode: FavoriteSortMode) -> ComparisonResult { + switch mode { + case .nameAZ: + return compareTitles(lhs.title, rhs.title) + case .nameZA: + return compareTitles(rhs.title, lhs.title) + case .nearest: + return compareDistances(lhs.distance, rhs.distance, nearestFirst: true, lhsTitle: lhs.title, rhsTitle: rhs.title) + case .farthest: + return compareDistances(lhs.distance, rhs.distance, nearestFirst: false, lhsTitle: lhs.title, rhsTitle: rhs.title) + case .lastModified, .newestDateFirst: + return compareDates(lhs.lastModified, rhs.lastModified, newestFirst: true, lhsTitle: lhs.title, rhsTitle: rhs.title) + case .oldestDateFirst: + return compareDates(lhs.lastModified, rhs.lastModified, newestFirst: false, lhsTitle: lhs.title, rhsTitle: rhs.title) + } + } + + private static func compareDates(_ lhs: Date?, _ rhs: Date?, newestFirst: Bool, lhsTitle: String, rhsTitle: String) -> ComparisonResult { + if let lhs, let rhs, lhs != rhs { + return newestFirst ? rhs.compare(lhs) : lhs.compare(rhs) + } else if lhs != nil { + return .orderedAscending + } else if rhs != nil { + return .orderedDescending + } + + return compareTitles(lhsTitle, rhsTitle) + } + + private static func compareDistances(_ lhs: CLLocationDistance?, _ rhs: CLLocationDistance?, nearestFirst: Bool, lhsTitle: String, rhsTitle: String) -> ComparisonResult { + if let lhs, let rhs, lhs != rhs { + if nearestFirst { + return lhs < rhs ? .orderedAscending : .orderedDescending + } + return lhs > rhs ? .orderedAscending : .orderedDescending + } else if lhs != nil { + return .orderedAscending + } else if rhs != nil { + return .orderedDescending + } + + return compareTitles(lhsTitle, rhsTitle) + } + + private static func compareTitles(_ lhs: String, _ rhs: String) -> ComparisonResult { + lhs.localizedCaseInsensitiveCompare(rhs) + } +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoriteFolderBridgeItem.h b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoriteFolderBridgeItem.h new file mode 100644 index 0000000000..3c9885b68f --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoriteFolderBridgeItem.h @@ -0,0 +1,30 @@ +// +// OAFavoriteFolderBridgeItem.h +// OsmAnd +// +// Created by Vladyslav Lysenko on 15.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +@class OAFavoriteGroup; + +NS_ASSUME_NONNULL_BEGIN + +@interface OAFavoriteFolderBridgeItem : NSObject + +@property (nonatomic, readonly) NSString *identifier; +@property (nonatomic, readonly) NSString *groupName; +@property (nonatomic, readonly) NSString *title; +@property (nonatomic, readonly) NSUInteger pointsCount; +@property (nonatomic, readonly) NSUInteger subtreePointsCount; +@property (nonatomic, readonly) BOOL isVisible; +@property (nonatomic, readonly) BOOL isPinned; +@property (nonatomic, readonly, nullable) UIColor *color; +@property (nonatomic, readonly, nullable) NSDate *lastModifiedDate; +@property (nonatomic, readonly) long long fileSize; + +- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize subtreePointsCount:(NSUInteger)subtreePointsCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoriteFolderBridgeItem.mm b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoriteFolderBridgeItem.mm new file mode 100644 index 0000000000..759d521b74 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoriteFolderBridgeItem.mm @@ -0,0 +1,41 @@ +// +// OAFavoriteFolderBridgeItem.m +// OsmAnd Maps +// +// Created by Vladyslav Lysenko on 15.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +#import "OAFavoriteFolderBridgeItem.h" +#import "OAFavoritesHelper.h" + +@implementation OAFavoriteFolderBridgeItem + +- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize subtreePointsCount:(NSUInteger)subtreePointsCount +{ + self = [super init]; + if (self) + { + NSString *groupName = group.name ?: @""; + _identifier = [NSString stringWithFormat:@"%@-%lu", groupName, (unsigned long)index]; + _groupName = groupName; + _title = [self.class titleForGroupName:groupName]; + _pointsCount = group.points.count; + _subtreePointsCount = subtreePointsCount; + _isVisible = group.isVisible; + _isPinned = group.isPinned; + _color = group.color; + _lastModifiedDate = lastModifiedDate; + _fileSize = fileSize; + } + + return self; +} + ++ (NSString *)titleForGroupName:(NSString *)groupName +{ + NSString *lastComponent = [[groupName componentsSeparatedByString:@"/"] lastObject] ?: groupName; + return [OAFavoriteGroup getDisplayName:lastComponent] ?: lastComponent; +} + +@end diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.h b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.h new file mode 100644 index 0000000000..9471f73dd0 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.h @@ -0,0 +1,35 @@ +// +// OAFavoritePointBridgeItem.h +// OsmAnd +// +// Created by Vladyslav Lysenko on 15.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +@class OAFavoriteItem; + +NS_ASSUME_NONNULL_BEGIN + +@interface OAFavoritePointBridgeItem : NSObject + +@property (nonatomic, readonly) NSString *identifier; +@property (nonatomic, readonly) NSString *groupName; +@property (nonatomic, readonly) NSString *title; +@property (nonatomic, readonly, nullable) NSString *address; +@property (nonatomic, readonly) NSString *displayGroupName; +@property (nonatomic, readonly, nullable) NSString *itemDescription; +@property (nonatomic, readonly) NSString *encodedNameForLink; +@property (nonatomic, readonly, nullable) NSNumber *distance; +@property (nonatomic, readonly) CGFloat direction; +@property (nonatomic, readonly) double latitude; +@property (nonatomic, readonly) double longitude; +@property (nonatomic, readonly, nullable) NSDate *timestampDate; +@property (nonatomic, readonly, nullable) UIImage *icon; +@property (nonatomic, readonly) BOOL isVisible; + +- (instancetype)initWithFavorite:(OAFavoriteItem *)favorite; +- (void)updateDistanceAndDirection; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.mm b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.mm new file mode 100644 index 0000000000..e5e6f72f40 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.mm @@ -0,0 +1,81 @@ +// +// OAFavoritePointBridgeItem.m +// OsmAnd Maps +// +// Created by Vladyslav Lysenko on 15.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +#import "OAFavoritePointBridgeItem.h" +#import "OAFavoriteItem.h" +#import "OsmAndApp.h" +#import "OALocationServices.h" + +#include + +@implementation OAFavoritePointBridgeItem +{ + OAFavoriteItem *_favorite; +} + +- (instancetype)initWithFavorite:(OAFavoriteItem *)favorite +{ + self = [super init]; + if (self) + { + _identifier = [favorite getKey] ?: @""; + _groupName = [favorite getCategory] ?: @""; + _title = [favorite getDisplayName] ?: @""; + _address = [favorite getAddress]; + _displayGroupName = [favorite getCategoryDisplayName] ?: @""; + _itemDescription = [favorite getDescription]; + _encodedNameForLink = [[favorite getName] escapeUrl] ?: @""; + _distance = [self.class distanceForFavorite:favorite]; + _direction = [self.class directionForFavorite:favorite]; + _latitude = [favorite getLatitude]; + _longitude = [favorite getLongitude]; + _timestampDate = [favorite getTimestamp]; + _icon = [favorite getCompositeIcon]; + _isVisible = [favorite isVisible]; + _favorite = favorite; + } + + return self; +} + +- (void)updateDistanceAndDirection +{ + _distance = [self.class distanceForFavorite:_favorite]; + _direction = [self.class directionForFavorite:_favorite]; +} + ++ (NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite +{ + CLLocation *location = [OsmAndApp instance].locationServices.lastKnownLocation; + if (!location || !favorite.favorite) + return nil; + + const auto &favoritePosition31 = favorite.favorite->getPosition31(); + const auto favoriteLon = OsmAnd::Utilities::get31LongitudeX(favoritePosition31.x); + const auto favoriteLat = OsmAnd::Utilities::get31LatitudeY(favoritePosition31.y); + const auto distance = OsmAnd::Utilities::distance(location.coordinate.longitude, location.coordinate.latitude, favoriteLon, favoriteLat); + return @(distance); +} + ++ (CGFloat)directionForFavorite:(OAFavoriteItem *)favorite +{ + OsmAndAppInstance app = [OsmAndApp instance]; + CLLocation *location = app.locationServices.lastKnownLocation; + if (!location || !favorite.favorite) + return favorite.direction; + + CLLocationDirection newHeading = app.locationServices.lastKnownHeading; + CLLocationDirection newDirection = location.speed >= 1 && location.course >= 0.0 ? location.course : newHeading; + const auto &favoritePosition31 = favorite.favorite->getPosition31(); + const auto favoriteLon = OsmAnd::Utilities::get31LongitudeX(favoritePosition31.x); + const auto favoriteLat = OsmAnd::Utilities::get31LatitudeY(favoritePosition31.y); + CGFloat itemDirection = [app.locationServices radiusFromBearingToLocation:[[CLLocation alloc] initWithLatitude:favoriteLat longitude:favoriteLon]]; + return OsmAnd::Utilities::normalizedAngleDegrees(itemDirection - newDirection) * (M_PI / 180); +} + +@end diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.h b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.h new file mode 100644 index 0000000000..25997ad6b1 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.h @@ -0,0 +1,57 @@ +// +// OAFavoritesBridgeHelper.h +// OsmAnd +// +// Created by Vladyslav Lysenko on 05.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class UIColor, UIImage, OAFavoriteItem, OASGpxUtilitiesPointsGroup, OAEditPointViewController, OAFavoriteFolderBridgeItem, OAFavoritePointBridgeItem; + +@interface OAFavoritesBridgeHelper : NSObject + ++ (void)invalidateFavoriteFoldersCache; ++ (NSArray *)favoriteFolders; ++ (NSArray *)favoritePointsForGroupName:(NSString *)groupName; ++ (NSString *)sharePoiURLStringForFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem; ++ (NSString *)geoURLStringForFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem; ++ (NSString *)formattedCoordinatesForFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem; + ++ (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible; ++ (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned; ++ (void)setFavoriteGroupsVisible:(NSArray *)groupNames visible:(BOOL)visible; ++ (void)setFavoriteGroupsPinned:(NSArray *)groupNames pinned:(BOOL)pinned; ++ (BOOL)addFavoriteGroup:(NSString *)name + parentGroupName:(nullable NSString *)parentGroupName + iconName:(nullable NSString *)iconName + color:(nullable UIColor *)color + backgroundIconName:(nullable NSString *)backgroundIconName; ++ (void)renameFavoriteGroup:(NSString *)groupName newName:(NSString *)newName; ++ (void)moveFavoriteItems:(NSArray *)favoriteItems toGroupName:(NSString *)targetGroupName; ++ (NSArray *)favoriteGroupNamesForMovingFavoriteItems:(NSArray *)favoriteItems; ++ (void)changeFavoriteItems:(NSArray *)favoriteItems colorIndex:(NSInteger)colorIndex; + ++ (OASGpxUtilitiesPointsGroup *)pointsGroupForGroupName:(NSString *)groupName; ++ (BOOL)canUseGroupWithName:(NSString *)groupName; + ++ (nullable NSURL *)shareFavoriteItems:(NSArray *)favoriteItems; + ++ (BOOL)deleteFavoriteGroup:(NSString *)groupName; ++ (BOOL)deleteFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem; ++ (BOOL)deleteFavoriteItems:(NSArray *)favoriteItems; + ++ (void)openFavoritePointWithIdentifier:(NSString *)identifier; ++ (nullable OAEditPointViewController *)editPointViewControllerForFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem; ++ (void)addFavoriteItemsToMapMarkers:(NSArray *)favoriteItems; ++ (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(nullable NSString *)gpxFileName; ++ (void)addFavoriteGroupToNavigation:(NSString *)groupName; ++ (void)addFavoriteItemsToTrack:(NSArray *)favoriteItems gpxFileName:(nullable NSString *)gpxFileName; ++ (void)addFavoriteItemsToNavigation:(NSArray *)favoriteItems; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm new file mode 100644 index 0000000000..832b7b069d --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm @@ -0,0 +1,1063 @@ +// +// OAFavoritesBridgeHelper.mm +// OsmAnd Maps +// +// Created by Vladyslav Lysenko on 05.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +#import "OAFavoritesBridgeHelper.h" +#import "OAAppSettings.h" +#import "OAEditGroupViewController.h" +#import "OAEditPointViewController.h" +#import "OAFavoriteItem.h" +#import "OAFavoriteGroupEditorViewController.h" +#import "OAFavoritesHelper.h" +#import "OAGPXDatabase.h" +#import "OAIndexConstants.h" +#import "OALocationServices.h" +#import "OAMapActions.h" +#import "OAMapRendererView.h" +#import "OAMapViewController.h" +#import "OAMapPanelViewController.h" +#import "OAObservable.h" +#import "OAOpenAddTrackViewController.h" +#import "OADefaultFavorite.h" +#import "OAOsmAndFormatter.h" +#import "OAPointDescription.h" +#import "OARootViewController.h" +#import "OARTargetPoint.h" +#import "OASavingTrackHelper.h" +#import "OASelectedGPXHelper.h" +#import "OATargetPointsHelper.h" +#import "OAUtilities.h" +#import "OsmAndApp.h" +#import "OsmAndSharedWrapper.h" +#import +#import "OAFavoriteFolderBridgeItem.h" +#import "OAFavoritePointBridgeItem.h" + +#include + +static NSArray *favoriteFoldersCache = nil; + +@implementation OAFavoritesBridgeHelper + ++ (void)invalidateFavoriteFoldersCache +{ + favoriteFoldersCache = nil; +} + ++ (NSArray *)favoriteFolders +{ + if (favoriteFoldersCache) + return favoriteFoldersCache; + + NSArray *groups = [OAFavoritesHelper getFavoriteGroups] ?: @[]; + NSDictionary *> *fileAttributesByGroupName = [self favoriteStorageAttributesForGroups:groups]; + NSMutableArray *folders = [NSMutableArray arrayWithCapacity:groups.count]; + [groups enumerateObjectsUsingBlock:^(OAFavoriteGroup * _Nonnull group, NSUInteger index, BOOL * _Nonnull stop) { + NSString *groupName = group.name ?: @""; + NSDictionary *fileAttributes = fileAttributesByGroupName[groupName]; + NSDate *lastModifiedDate = [self lastModifiedDateForGroupName:groupName groups:groups fileAttributesByGroupName:fileAttributesByGroupName]; + long long fileSize = [fileAttributes[NSFileSize] longLongValue]; + NSUInteger subtreePointsCount = [self subtreePointsCountForGroupName:groupName groups:groups]; + [folders addObject:[[OAFavoriteFolderBridgeItem alloc] initWithGroup:group index:index lastModifiedDate:lastModifiedDate fileSize:fileSize subtreePointsCount:subtreePointsCount]]; + }]; + + favoriteFoldersCache = [folders copy]; + return favoriteFoldersCache; +} + ++ (NSArray *)favoritePointsForGroupName:(NSString *)groupName +{ + NSArray *points = [self sortedFavoritePointsForGroup:[self favoriteGroupWithName:groupName]]; + NSMutableArray *items = [NSMutableArray arrayWithCapacity:points.count]; + for (OAFavoriteItem *point in points) + [items addObject:[[OAFavoritePointBridgeItem alloc] initWithFavorite:point]]; + + return items.copy; +} + ++ (NSString *)sharePoiURLStringForFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem +{ + NSMutableArray *query = [NSMutableArray array]; + if (favoriteItem.encodedNameForLink.length > 0) + [query addObject:[NSString stringWithFormat:@"name=%@", favoriteItem.encodedNameForLink]]; + + NSString *pin = [NSString stringWithFormat:@"%.6f%%2C%.6f", favoriteItem.latitude, favoriteItem.longitude]; + [query addObject:[NSString stringWithFormat:@"pin=%@", pin]]; + + int zoom = [self currentMapZoomLevel]; + NSString *queryPart = [query componentsJoinedByString:@"&"]; + NSString *fragment = [NSString stringWithFormat:@"%d/%.4f/%.4f", zoom, favoriteItem.latitude, favoriteItem.longitude]; + return [NSString stringWithFormat:@"%@?%@#%@", kSharePoiBaseUrl, queryPart, fragment]; +} + ++ (NSString *)geoURLStringForFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem +{ + return [OAUtilities buildGeoUrl:favoriteItem.latitude + longitude:favoriteItem.longitude + zoom:[self currentMapZoomLevel] + label:favoriteItem.title]; +} + ++ (NSString *)formattedCoordinatesForFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem +{ + NSInteger format = [OAAppSettings.sharedManager.settingGeoFormat get]; + return [OAOsmAndFormatter getFormattedCoordinatesWithLat:favoriteItem.latitude + lon:favoriteItem.longitude + outputFormat:format]; +} + ++ (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible +{ + NSArray *groups = [self favoriteGroupsInsideOrEqualToGroupName:groupName]; + if (groups.count == 0) + return; + + for (OAFavoriteGroup *group in groups) + { + [OAFavoritesHelper updateGroup:group visible:visible saveImmediately:NO]; + } + + [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; +} + ++ (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return; + + [OAFavoritesHelper updateGroup:group pinned:pinned saveImmediately:YES]; + [self invalidateFavoriteFoldersCache]; +} + ++ (void)setFavoriteGroupsVisible:(NSArray *)groupNames visible:(BOOL)visible +{ + if (groupNames.count == 0) + return; + + BOOL changed = NO; + NSMutableSet *handledGroupNames = [NSMutableSet set]; + for (NSString *groupName in groupNames) + { + for (OAFavoriteGroup *group in [self favoriteGroupsInsideOrEqualToGroupName:groupName]) + { + NSString *currentGroupName = group.name ?: @""; + if ([handledGroupNames containsObject:currentGroupName]) + continue; + + [handledGroupNames addObject:currentGroupName]; + [OAFavoritesHelper updateGroup:group visible:visible saveImmediately:NO]; + changed = YES; + } + } + + if (changed) + { + [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; + } +} + ++ (void)setFavoriteGroupsPinned:(NSArray *)groupNames pinned:(BOOL)pinned +{ + if (groupNames.count == 0) + return; + + BOOL changed = NO; + NSMutableSet *handledGroupNames = [NSMutableSet set]; + for (NSString *groupName in groupNames) + { + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + continue; + + [handledGroupNames addObject:groupName]; + [OAFavoritesHelper updateGroup:group pinned:pinned saveImmediately:NO]; + changed = YES; + } + + if (changed) + { + [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; + } +} + ++ (BOOL)addFavoriteGroup:(NSString *)name + parentGroupName:(nullable NSString *)parentGroupName + iconName:(nullable NSString *)iconName + color:(nullable UIColor *)color + backgroundIconName:(nullable NSString *)backgroundIconName +{ + NSString *trimmedName = [(name ?: @"") stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; + NSString *parent = parentGroupName ?: @""; + NSString *groupName = parent.length > 0 && trimmedName.length > 0 ? [NSString stringWithFormat:@"%@/%@", parent, trimmedName] : trimmedName; + if (groupName.length == 0 || [self favoriteGroupWithName:groupName]) + return NO; + + [OAFavoritesHelper addFavoriteGroup:groupName + color:color + iconName:iconName + backgroundIconName:backgroundIconName]; + [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; + return YES; +} + ++ (void)renameFavoriteGroup:(NSString *)groupName newName:(NSString *)newName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + NSString *trimmedName = [newName stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; + if (!group || trimmedName.length == 0) + return; + + NSString *sourceGroupName = group.name ?: @""; + if ([sourceGroupName isEqualToString:trimmedName]) + return; + + [self renameFavoriteGroupTreeFromGroupName:sourceGroupName toGroupName:trimmedName]; +} + ++ (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return NO; + + NSString *sourceGroupName = group.name ?: @""; + NSString *parentGroupName = targetGroupName ?: @""; + if (sourceGroupName.length == 0 || [self isGroupName:parentGroupName insideOrEqualToGroupName:sourceGroupName]) + return NO; + + NSString *newGroupName = [self groupNameByMovingGroupName:sourceGroupName toParentGroupName:parentGroupName]; + if ([sourceGroupName isEqualToString:newGroupName]) + return NO; + + return [self renameFavoriteGroupTreeFromGroupName:sourceGroupName toGroupName:newGroupName]; +} + ++ (void)moveFavoriteItems:(NSArray *)favoriteItems toGroupName:(NSString *)targetGroupName +{ + if (favoriteItems.count == 0) + return; + + NSString *groupName = targetGroupName ?: @""; + NSMutableSet *movedGroupNames = [NSMutableSet set]; + NSMutableSet *movedItemKeys = [NSMutableSet set]; + + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoriteFolderBridgeItem.class]) + continue; + + OAFavoriteFolderBridgeItem *folderItem = (OAFavoriteFolderBridgeItem *) item; + OAFavoriteGroup *group = [self favoriteGroupWithName:folderItem.groupName]; + if (!group) + continue; + + NSString *sourceGroupName = group.name ?: @""; + if ([self moveFavoriteGroup:sourceGroupName toGroupName:groupName]) + [movedGroupNames addObject:sourceGroupName]; + } + + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoritePointBridgeItem.class]) + continue; + + OAFavoritePointBridgeItem *pointItem = (OAFavoritePointBridgeItem *) item; + BOOL isInsideMovedGroup = NO; + for (NSString *movedGroupName in movedGroupNames) + { + if ([self isGroupName:pointItem.groupName insideOrEqualToGroupName:movedGroupName]) + { + isInsideMovedGroup = YES; + break; + } + } + + if (isInsideMovedGroup) + continue; + + OAFavoriteItem *favorite = [self favoritePointWithIdentifier:pointItem.identifier]; + if (!favorite) + continue; + + NSString *itemKey = [favorite getKey] ?: pointItem.identifier; + if ([movedItemKeys containsObject:itemKey]) + continue; + + if ([OAFavoritesHelper editFavoriteName:favorite + newName:[favorite getDisplayName] + group:groupName + descr:[favorite getDescription] + address:[favorite getAddress]]) + [movedItemKeys addObject:itemKey]; + } + + if (movedItemKeys.count > 0) + [self invalidateFavoriteFoldersCache]; +} + ++ (NSArray *)favoriteGroupNamesForMovingFavoriteItems:(NSArray *)favoriteItems +{ + NSMutableSet *selectedGroupNames = [NSMutableSet set]; + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoriteFolderBridgeItem.class]) + continue; + + OAFavoriteFolderBridgeItem *folderItem = (OAFavoriteFolderBridgeItem *) item; + OAFavoriteGroup *group = [self favoriteGroupWithName:folderItem.groupName]; + if (group) + [selectedGroupNames addObject:group.name ?: @""]; + } + + NSMutableArray *groupNames = [NSMutableArray array]; + for (OAFavoriteGroup *favoriteGroup in [OAFavoritesHelper getFavoriteGroups]) + { + NSString *favoriteGroupName = favoriteGroup.name ?: @""; + BOOL isInsideSelectedGroup = NO; + for (NSString *selectedGroupName in selectedGroupNames) + { + if ([self isGroupName:favoriteGroupName insideOrEqualToGroupName:selectedGroupName]) + { + isInsideSelectedGroup = YES; + break; + } + } + + if (!isInsideSelectedGroup) + [groupNames addObject:favoriteGroupName]; + } + + if (![groupNames containsObject:@""]) + [groupNames addObject:@""]; + + return [groupNames copy]; +} + ++ (void)changeFavoriteItems:(NSArray *)favoriteItems colorIndex:(NSInteger)colorIndex +{ + if (favoriteItems.count == 0) + return; + + NSArray *builtinColors = [OADefaultFavorite builtinColors]; + if (colorIndex < 0 || colorIndex >= builtinColors.count) + return; + + UIColor *color = builtinColors[colorIndex].color; + BOOL changed = NO; + NSMutableSet *changedPointKeys = [NSMutableSet set]; + + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoriteFolderBridgeItem.class]) + continue; + + OAFavoriteFolderBridgeItem *folderItem = (OAFavoriteFolderBridgeItem *) item; + OAFavoriteGroup *group = [self favoriteGroupWithName:folderItem.groupName]; + if (!group) + continue; + + group.color = color; + changed = YES; + } + + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoritePointBridgeItem.class]) + continue; + + OAFavoritePointBridgeItem *pointItem = (OAFavoritePointBridgeItem *) item; + OAFavoriteItem *favorite = [self favoritePointWithIdentifier:pointItem.identifier]; + if (!favorite) + continue; + + NSString *pointKey = [favorite getKey] ?: pointItem.identifier; + if ([changedPointKeys containsObject:pointKey]) + continue; + + [favorite setColor:color]; + [changedPointKeys addObject:pointKey]; + changed = YES; + + OAFavoriteGroup *group = [self favoriteGroupWithName:[favorite getCategory]]; + OAFavoriteItem *firstPoint = group.points.firstObject; + if (firstPoint && ([[firstPoint getKey] isEqualToString:pointKey])) + group.color = color; + } + + if (changed) + { + [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; + } +} + ++ (OASGpxUtilitiesPointsGroup *)pointsGroupForGroupName:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return nil; + return group ? [group toPointsGroup] : nil; +} + ++ (BOOL)canUseGroupWithName:(NSString *)groupName +{ + return [self favoriteItemsInsideOrEqualToGroupName:groupName].count > 0; +} + ++ (NSURL *)shareFavoriteItems:(NSArray *)favoriteItems +{ + if (favoriteItems.count == 0) + return nil; + + NSMutableDictionary *groupsByName = [NSMutableDictionary dictionary]; + NSMutableSet *sharedGroupNames = [NSMutableSet set]; + NSMutableSet *sharedPointKeys = [NSMutableSet set]; + + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoriteFolderBridgeItem.class]) + continue; + + OAFavoriteFolderBridgeItem *folderItem = (OAFavoriteFolderBridgeItem *) item; + OAFavoriteGroup *group = [self favoriteGroupWithName:folderItem.groupName]; + if (!group) + continue; + + for (OAFavoriteGroup *groupToShare in [self favoriteGroupsInsideOrEqualToGroupName:group.name ?: @""]) + { + NSString *sourceGroupName = groupToShare.name ?: @""; + if (groupsByName[sourceGroupName]) + continue; + + groupsByName[sourceGroupName] = [self favoriteGroupForSharingGroup:groupToShare points:groupToShare.points.copy]; + [sharedGroupNames addObject:sourceGroupName]; + for (OAFavoriteItem *point in groupToShare.points) + { + NSString *pointKey = [point getKey]; + if (pointKey.length > 0) + [sharedPointKeys addObject:pointKey]; + } + } + } + + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoritePointBridgeItem.class]) + continue; + + OAFavoritePointBridgeItem *pointItem = (OAFavoritePointBridgeItem *) item; + OAFavoriteItem *favorite = [self favoritePointWithIdentifier:pointItem.identifier]; + if (!favorite) + continue; + + NSString *pointKey = [favorite getKey] ?: pointItem.identifier; + if ([sharedPointKeys containsObject:pointKey]) + continue; + + NSString *groupName = [favorite getCategory] ?: @""; + BOOL isInsideSharedGroup = NO; + for (NSString *sharedGroupName in sharedGroupNames) + { + if ([self isGroupName:groupName insideOrEqualToGroupName:sharedGroupName]) + { + isInsideSharedGroup = YES; + break; + } + } + + if (isInsideSharedGroup) + continue; + + OAFavoriteGroup *groupToShare = groupsByName[groupName]; + if (!groupToShare) + { + OAFavoriteGroup *sourceGroup = [self favoriteGroupWithName:groupName]; + groupToShare = sourceGroup ? [self favoriteGroupForSharingGroup:sourceGroup points:@[]] : [[OAFavoriteGroup alloc] initWithPoint:favorite]; + groupsByName[groupName] = groupToShare; + } + + [groupToShare addPoint:favorite]; + if (pointKey.length > 0) + [sharedPointKeys addObject:pointKey]; + } + + return [self fileURLForSharingFavoriteGroups:groupsByName.allValues]; +} + ++ (BOOL)deleteFavoriteGroup:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return NO; + + NSArray *groupsToDelete = [self favoriteGroupsInsideOrEqualToGroupName:group.name ?: @""]; + if (groupsToDelete.count == 0) + return NO; + + BOOL didDelete = [OAFavoritesHelper deleteFavoriteGroups:groupsToDelete andFavoritesItems:nil]; + if (didDelete) + [self invalidateFavoriteFoldersCache]; + return didDelete; +} + ++ (BOOL)deleteFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem +{ + OAFavoriteItem *favorite = [self favoritePointWithIdentifier:favoriteItem.identifier]; + if (!favorite) + return NO; + + [OAFavoritesHelper deleteFavorites:@[favorite] saveImmediately:YES]; + [self invalidateFavoriteFoldersCache]; + return YES; +} + ++ (BOOL)deleteFavoriteItems:(NSArray *)favoriteItems +{ + if (favoriteItems.count == 0) + return NO; + + NSMutableArray *groupsToDelete = [NSMutableArray array]; + NSMutableSet *deletedGroupNames = [NSMutableSet set]; + NSMutableArray *itemsToDelete = [NSMutableArray array]; + NSMutableSet *deletedItemKeys = [NSMutableSet set]; + + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoriteFolderBridgeItem.class]) + continue; + + OAFavoriteFolderBridgeItem *folderItem = (OAFavoriteFolderBridgeItem *) item; + OAFavoriteGroup *group = [self favoriteGroupWithName:folderItem.groupName]; + if (!group) + continue; + + for (OAFavoriteGroup *groupToDelete in [self favoriteGroupsInsideOrEqualToGroupName:group.name ?: @""]) + { + NSString *groupName = groupToDelete.name ?: @""; + if ([deletedGroupNames containsObject:groupName]) + continue; + + [deletedGroupNames addObject:groupName]; + [groupsToDelete addObject:groupToDelete]; + } + } + + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoritePointBridgeItem.class]) + continue; + + OAFavoritePointBridgeItem *pointItem = (OAFavoritePointBridgeItem *) item; + BOOL isInsideDeletedGroup = NO; + for (NSString *groupName in deletedGroupNames) + { + if ([self isGroupName:pointItem.groupName insideOrEqualToGroupName:groupName]) + { + isInsideDeletedGroup = YES; + break; + } + } + + if (isInsideDeletedGroup) + continue; + + OAFavoriteItem *favorite = [self favoritePointWithIdentifier:pointItem.identifier]; + if (!favorite) + continue; + + NSString *itemKey = [favorite getKey] ?: pointItem.identifier; + if ([deletedItemKeys containsObject:itemKey]) + continue; + + [deletedItemKeys addObject:itemKey]; + [itemsToDelete addObject:favorite]; + } + + if (groupsToDelete.count == 0 && itemsToDelete.count == 0) + return NO; + + BOOL didDelete = NO; + if (groupsToDelete.count > 0) + didDelete = [OAFavoritesHelper deleteFavoriteGroups:groupsToDelete andFavoritesItems:nil] || didDelete; + + if (itemsToDelete.count > 0) + { + [OAFavoritesHelper deleteFavorites:itemsToDelete saveImmediately:YES]; + didDelete = YES; + } + + if (didDelete) + [self invalidateFavoriteFoldersCache]; + return didDelete; +} + ++ (void)openFavoritePointWithIdentifier:(NSString *)identifier +{ + OAFavoriteItem *favorite = [self favoritePointWithIdentifier:identifier]; + if (!favorite) + return; + + CATransition *transition = [CATransition animation]; + transition.duration = 0.4; + transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + transition.type = kCATransitionPush; + transition.subtype = kCATransitionFromRight; + OARootViewController *rootViewController = [OARootViewController instance]; + [rootViewController.navigationController.view.layer addAnimation:transition forKey:nil]; + [rootViewController.navigationController popToRootViewControllerAnimated:NO]; + [rootViewController.navigationController setNavigationBarHidden:YES animated:NO]; + [rootViewController.mapPanel openTargetViewWithFavorite:favorite pushed:YES]; +} + ++ (OAEditPointViewController *)editPointViewControllerForFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem +{ + OAFavoriteItem *favorite = [self favoritePointWithIdentifier:favoriteItem.identifier]; + if (!favorite) + return nil; + + return [[OAEditPointViewController alloc] initWithFavorite:favorite]; +} + ++ (void)addFavoriteItemsToMapMarkers:(NSArray *)favoriteItems +{ + if (favoriteItems.count == 0) + return; + + NSArray *itemsToAdd = [self favoriteItemsForBridgeItemsToAdd:favoriteItems standalonePointItems:NO]; + if (itemsToAdd.count == 0) + return; + + OAMapPanelViewController *mapPanel = [OARootViewController instance].mapPanel; + for (OAFavoriteItem *favorite in itemsToAdd) + { + CLLocation *location = [self locationForFavorite:favorite]; + if (!location) + continue; + + [mapPanel addMapMarker:location.coordinate.latitude lon:location.coordinate.longitude description:[favorite getDisplayName]]; + } +} + ++ (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(nullable NSString *)gpxFileName +{ + NSArray *points = [self favoriteItemsInsideOrEqualToGroupName:groupName]; + if (points.count == 0) + return; + + if (gpxFileName.length == 0) + { + OASavingTrackHelper *savingTrackHelper = OASavingTrackHelper.sharedInstance; + for (OAFavoriteItem *favorite in points) + [savingTrackHelper addWpt:[favorite toWpt]]; + + if (![OAAppSettings.sharedManager.mapSettingShowRecordingTrack get]) + [OAAppSettings.sharedManager.mapSettingShowRecordingTrack set:YES]; + return; + } + + OAGPXDatabase *gpxDatabase = OAGPXDatabase.sharedDb; + OASGpxDataItem *dataItem = [gpxDatabase getGPXItem:gpxFileName]; + if (!dataItem) + dataItem = [gpxDatabase getGPXItemByFileName:gpxFileName]; + if (!dataItem) + return; + + OASGpxFile *gpxFile = [OASGpxUtilities.shared loadGpxFileFile:dataItem.file]; + if (!gpxFile) + return; + + for (OAFavoriteItem *favorite in points) + [gpxFile addPointPoint:[favorite toWpt]]; + + [OASGpxUtilities.shared writeGpxFileFile:dataItem.file gpxFile:gpxFile]; + [gpxDatabase updateDataItem:dataItem]; + [OASelectedGPXHelper.instance markTrackForReload:dataItem.file.absolutePath]; + [OsmAndApp.instance.updateGpxTracksOnMapObservable notifyEvent]; +} + ++ (void)addFavoriteGroupToNavigation:(NSString *)groupName +{ + NSArray *points = [self favoriteItemsInsideOrEqualToGroupName:groupName]; + NSMutableArray *items = [NSMutableArray arrayWithCapacity:points.count]; + for (OAFavoriteItem *point in points) + { + [items addObject:[[OAFavoritePointBridgeItem alloc] initWithFavorite:point]]; + } + + [self addFavoriteItemsToNavigation:items]; +} + ++ (void)addFavoriteItemsToTrack:(NSArray *)favoriteItems gpxFileName:(nullable NSString *)gpxFileName +{ + if (favoriteItems.count == 0) + return; + + NSArray *itemsToAdd = [self favoriteItemsForBridgeItemsToAdd:favoriteItems standalonePointItems:YES]; + if (itemsToAdd.count == 0) + return; + + if (gpxFileName.length == 0) + { + OASavingTrackHelper *savingTrackHelper = [OASavingTrackHelper sharedInstance]; + for (OAFavoriteItem *favorite in itemsToAdd) + [savingTrackHelper addWpt:[favorite toWpt]]; + + if (![OAAppSettings.sharedManager.mapSettingShowRecordingTrack get]) + [OAAppSettings.sharedManager.mapSettingShowRecordingTrack set:YES]; + return; + } + + OAGPXDatabase *gpxDatabase = OAGPXDatabase.sharedDb; + OASGpxDataItem *dataItem = [gpxDatabase getGPXItem:gpxFileName]; + if (!dataItem) + dataItem = [gpxDatabase getGPXItemByFileName:gpxFileName]; + if (!dataItem) + return; + + OASGpxFile *gpxFile = [OASGpxUtilities.shared loadGpxFileFile:dataItem.file]; + if (!gpxFile) + return; + + for (OAFavoriteItem *favorite in itemsToAdd) + [gpxFile addPointPoint:[favorite toWpt]]; + + [OASGpxUtilities.shared writeGpxFileFile:dataItem.file gpxFile:gpxFile]; + [gpxDatabase updateDataItem:dataItem]; + [OASelectedGPXHelper.instance markTrackForReload:dataItem.file.absolutePath]; + [[[OsmAndApp instance] updateGpxTracksOnMapObservable] notifyEvent]; +} + ++ (void)addFavoriteItemsToNavigation:(NSArray *)favoriteItems +{ + if (favoriteItems.count == 0) + return; + + NSArray *itemsToAdd = [self favoriteItemsForBridgeItemsToAdd:favoriteItems standalonePointItems:NO]; + if (itemsToAdd.count == 0) + return; + + NSMutableArray *targetPoints = [NSMutableArray arrayWithCapacity:itemsToAdd.count]; + for (OAFavoriteItem *favorite in itemsToAdd) + { + CLLocation *location = [self locationForFavorite:favorite]; + if (!location) + continue; + + OAPointDescription *description = [[OAPointDescription alloc] initWithType:POINT_TYPE_FAVORITE name:[favorite getDisplayName]]; + OARTargetPoint *targetPoint = [OARTargetPoint create:location name:description]; + if (targetPoint) + [targetPoints addObject:targetPoint]; + } + + if (targetPoints.count == 0) + return; + + OATargetPointsHelper *targetPointsHelper = [OATargetPointsHelper sharedInstance]; + [targetPointsHelper clearAllPoints:NO]; + [targetPointsHelper reorderAllTargetPoints:targetPoints updateRoute:NO]; + OARootViewController *rootViewController = [OARootViewController instance]; + [rootViewController.navigationController popToRootViewControllerAnimated:YES]; + [rootViewController.mapPanel.mapActions enterRoutePlanningModeGivenGpx:nil + from:nil + fromName:nil + useIntermediatePointsByDefault:YES + showDialog:YES]; +} + ++ (NSArray *)sortedFavoritePoints:(NSArray *)points +{ + return [points sortedArrayUsingComparator:^NSComparisonResult(OAFavoriteItem *obj1, OAFavoriteItem *obj2) { + BOOL obj1Visible = obj1.isVisible; + BOOL obj2Visible = obj2.isVisible; + if (obj1Visible != obj2Visible) + return obj1Visible ? NSOrderedAscending : NSOrderedDescending; + + return [[[obj1 getDisplayName] lowercaseString] compare:[[obj2 getDisplayName] lowercaseString]]; + }]; +} + ++ (NSArray *)sortedFavoritePointsForGroup:(OAFavoriteGroup *)group +{ + return [self sortedFavoritePoints:group.points ?: @[]]; +} + ++ (NSArray *)favoriteItemsForBridgeItemsToAdd:(NSArray *)favoriteItems standalonePointItems:(BOOL)standalonePointItems +{ + NSMutableArray *result = [NSMutableArray array]; + NSMutableSet *addedPointKeys = [NSMutableSet set]; + + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoriteFolderBridgeItem.class]) + continue; + + OAFavoriteFolderBridgeItem *folderItem = (OAFavoriteFolderBridgeItem *) item; + [self addFavoriteItemsInsideOrEqualToGroupName:folderItem.groupName toArray:result addedPointKeys:addedPointKeys]; + } + + for (id item in favoriteItems) + { + if (![item isKindOfClass:OAFavoritePointBridgeItem.class]) + continue; + + OAFavoritePointBridgeItem *pointItem = (OAFavoritePointBridgeItem *) item; + OAFavoriteItem *favorite = [self favoritePointWithIdentifier:pointItem.identifier]; + if (!favorite) + continue; + + NSString *pointKey = [favorite getKey] ?: pointItem.identifier; + OAFavoriteItem *favoriteToAdd = standalonePointItems ? [self standaloneFavoritePoint:favorite] : favorite; + [self addFavoriteItem:favoriteToAdd pointKey:pointKey toArray:result addedPointKeys:addedPointKeys]; + } + + return [result copy]; +} + ++ (NSArray *)favoriteItemsInsideOrEqualToGroupName:(NSString *)groupName +{ + NSMutableArray *result = [NSMutableArray array]; + NSMutableSet *addedPointKeys = [NSMutableSet set]; + [self addFavoriteItemsInsideOrEqualToGroupName:groupName toArray:result addedPointKeys:addedPointKeys]; + return [result copy]; +} + ++ (void)addFavoriteItemsInsideOrEqualToGroupName:(NSString *)groupName + toArray:(NSMutableArray *)result + addedPointKeys:(NSMutableSet *)addedPointKeys +{ + for (OAFavoriteGroup *group in [self favoriteGroupsInsideOrEqualToGroupName:groupName]) + { + for (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) + [self addFavoriteItem:favorite pointKey:[favorite getKey] toArray:result addedPointKeys:addedPointKeys]; + } +} + ++ (void)addFavoriteItem:(OAFavoriteItem *)favorite + pointKey:(NSString *)pointKey + toArray:(NSMutableArray *)result + addedPointKeys:(NSMutableSet *)addedPointKeys +{ + if (!favorite) + return; + + if (pointKey.length > 0 && [addedPointKeys containsObject:pointKey]) + return; + + if (pointKey.length > 0) + [addedPointKeys addObject:pointKey]; + [result addObject:favorite]; +} + ++ (NSDate *)lastModifiedDateForGroupName:(NSString *)groupName groups:(NSArray *)groups fileAttributesByGroupName:(NSDictionary *> *)fileAttributesByGroupName +{ + NSDate *lastModifiedDate = nil; + NSString *parentGroupName = groupName ?: @""; + for (OAFavoriteGroup *favoriteGroup in groups) + { + NSString *currentGroupName = favoriteGroup.name ?: @""; + if (![self isGroupName:currentGroupName insideOrEqualToGroupName:parentGroupName]) + continue; + + NSDate *fileModifiedDate = (NSDate *)fileAttributesByGroupName[currentGroupName][NSFileModificationDate]; + if (fileModifiedDate && (!lastModifiedDate || [fileModifiedDate compare:lastModifiedDate] == NSOrderedDescending)) + lastModifiedDate = fileModifiedDate; + + for (OAFavoriteItem *point in favoriteGroup.points) + { + NSDate *timestamp = [point getTimestamp]; + if (timestamp && (!lastModifiedDate || [timestamp compare:lastModifiedDate] == NSOrderedDescending)) + lastModifiedDate = timestamp; + } + } + + return lastModifiedDate; +} + ++ (NSUInteger)subtreePointsCountForGroupName:(NSString *)groupName groups:(NSArray *)groups +{ + NSUInteger pointsCount = 0; + NSString *parentGroupName = groupName ?: @""; + for (OAFavoriteGroup *favoriteGroup in groups) + { + if ([self isGroupName:favoriteGroup.name ?: @"" insideOrEqualToGroupName:parentGroupName]) + pointsCount += favoriteGroup.points.count; + } + + return pointsCount; +} + ++ (NSDictionary *> *)favoriteStorageAttributesForGroups:(NSArray *)groups +{ + NSMutableDictionary *> *result = [NSMutableDictionary dictionaryWithCapacity:groups.count]; + for (OAFavoriteGroup *group in groups) + { + NSString *groupName = group.name ?: @""; + NSString *filePath = [OsmAndApp.instance favoritesStorageFilename:groupName]; + NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:filePath error:nil]; + if (attributes) + result[groupName] = attributes; + } + + return result.copy; +} + ++ (NSArray *)favoriteGroupsInsideOrEqualToGroupName:(NSString *)groupName +{ + NSMutableArray *result = [NSMutableArray array]; + NSString *parentGroupName = groupName ?: @""; + for (OAFavoriteGroup *favoriteGroup in [[OAFavoritesHelper getFavoriteGroups] copy]) + { + if ([self isGroupName:favoriteGroup.name ?: @"" insideOrEqualToGroupName:parentGroupName]) + [result addObject:favoriteGroup]; + } + + return result.copy; +} + ++ (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier +{ + if (identifier.length == 0) + return nil; + + for (OAFavoriteGroup *group in [OAFavoritesHelper getFavoriteGroups]) + { + for (OAFavoriteItem *point in group.points) + { + if ([[point getKey] isEqualToString:identifier]) + return point; + } + } + + return nil; +} + ++ (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName +{ + return [OAFavoritesHelper getGroupByName:groupName ?: @""]; +} + ++ (BOOL)renameFavoriteGroupTreeFromGroupName:(NSString *)sourceGroupName toGroupName:(NSString *)targetGroupName +{ + NSString *source = sourceGroupName ?: @""; + NSString *target = targetGroupName ?: @""; + if ([source isEqualToString:target]) + return NO; + + BOOL changed = NO; + for (OAFavoriteGroup *favoriteGroup in [self favoriteGroupsInsideOrEqualToGroupName:source]) + { + NSString *currentGroupName = favoriteGroup.name ?: @""; + NSString *renamedGroupName = [target stringByAppendingString:[self suffixForGroupName:currentGroupName parentGroupName:source]]; + if ([currentGroupName isEqualToString:renamedGroupName]) + continue; + + [OAFavoritesHelper updateGroup:favoriteGroup newName:renamedGroupName saveImmediately:NO]; + changed = YES; + } + + if (changed) + { + [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; + } + + return changed; +} + ++ (BOOL)isGroupName:(NSString *)groupName insideOrEqualToGroupName:(NSString *)parentGroupName +{ + NSString *name = groupName ?: @""; + NSString *parent = parentGroupName ?: @""; + if ([name isEqualToString:parent]) + return YES; + + if (parent.length == 0) + return NO; + + return [name hasPrefix:[parent stringByAppendingString:@"/"]]; +} + ++ (NSString *)groupNameByMovingGroupName:(NSString *)groupName toParentGroupName:(NSString *)parentGroupName +{ + NSString *name = groupName ?: @""; + NSString *parent = parentGroupName ?: @""; + NSString *lastComponent = [self lastComponentForGroupName:name]; + return parent.length > 0 ? [NSString stringWithFormat:@"%@/%@", parent, lastComponent] : lastComponent; +} + ++ (NSString *)suffixForGroupName:(NSString *)groupName parentGroupName:(NSString *)parentGroupName +{ + NSString *name = groupName ?: @""; + NSString *parent = parentGroupName ?: @""; + return name.length > parent.length ? [name substringFromIndex:parent.length] : @""; +} + ++ (NSString *)lastComponentForGroupName:(NSString *)groupName +{ + NSArray *components = [(groupName ?: @"") componentsSeparatedByString:@"/"]; + return components.lastObject ?: @""; +} + ++ (OAFavoriteGroup *)favoriteGroupForSharingGroup:(OAFavoriteGroup *)group points:(NSArray *)points +{ + OAFavoriteGroup *groupToShare = [[OAFavoriteGroup alloc] initWithPoints:points ?: @[] + name:group.name ?: @"" + isVisible:group.isVisible + color:group.color]; + groupToShare.isPinned = group.isPinned; + groupToShare.iconName = group.iconName; + groupToShare.backgroundType = group.backgroundType; + return groupToShare; +} + ++ (NSURL *)fileURLForSharingFavoriteGroups:(NSArray *)favoriteGroups +{ + if (favoriteGroups.count == 0) + return nil; + + OsmAndAppInstance app = [OsmAndApp instance]; + NSString *filename = app.favoritesFilePrefix ?: @""; + if (favoriteGroups.count == 1) + { + NSString *groupFileName = [app getGroupFileName:favoriteGroups.firstObject.name ?: @""]; + filename = [NSString stringWithFormat:@"%@%@%@", + filename, + groupFileName.length > 0 ? app.favoritesGroupNameSeparator : @"", + groupFileName ?: @""]; + } + filename = [filename stringByAppendingString:GPX_FILE_EXT]; + NSString *fullFilename = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; + [OAFavoritesHelper saveFile:favoriteGroups file:fullFilename]; + return [NSURL fileURLWithPath:fullFilename]; +} + ++ (CLLocation *)locationForFavorite:(OAFavoriteItem *)favorite +{ + if (!favorite.favorite) + return nil; + + return [[CLLocation alloc] initWithLatitude:[favorite getLatitude] longitude:[favorite getLongitude]]; +} + ++ (int)currentMapZoomLevel +{ + return [OARootViewController instance].mapPanel.mapViewController.mapView.zoomLevel; +} + ++ (OAFavoriteItem *)standaloneFavoritePoint:(OAFavoriteItem *)favorite +{ + OASWptPt *point = [favorite toWpt]; + point.category = nil; + return [OAFavoriteItem fromWpt:point category:@""]; +} + +@end diff --git a/Sources/Controllers/MyPlaces/MyPlaces.storyboard b/Sources/Controllers/MyPlaces/MyPlaces.storyboard deleted file mode 100644 index c95c42e29c..0000000000 --- a/Sources/Controllers/MyPlaces/MyPlaces.storyboard +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Sources/Controllers/MyPlaces/MyPlacesContainerViewController.swift b/Sources/Controllers/MyPlaces/MyPlacesContainerViewController.swift index 0883d432a3..4f43e51454 100644 --- a/Sources/Controllers/MyPlaces/MyPlacesContainerViewController.swift +++ b/Sources/Controllers/MyPlaces/MyPlacesContainerViewController.swift @@ -12,6 +12,7 @@ protocol MyPlacesDelegate: AnyObject { func updateSegmentedControlVisibility(_ isVisible: Bool) func updateEditMode(_ edit: Bool) @objc optional func updateTitle(_ title: String, hideSubtitle: Bool) + @objc optional func updateTitle(_ title: String, subtitle: String, hideSubtitle: Bool) @objc optional func updateToolbar(with items: [UIBarButtonItem]?) } @@ -49,7 +50,7 @@ final class MyPlacesContainerViewController: OACompoundViewController { var controllerType: AnyClass { switch self { - case .favorites: OAFavoriteListViewController.self + case .favorites: FavoriteListViewController.self case .tracks: TracksViewController.self case .osm: OsmEditsListViewController.self case .travel: SavedArticlesTabViewController.self @@ -114,11 +115,10 @@ final class MyPlacesContainerViewController: OACompoundViewController { func viewController(for tab: Tab) -> UIViewController? { guard let pageViewController else { return nil } - let storyboard = UIStoryboard(name: "MyPlaces", bundle: nil) switch tab { case .favorites: - if !availableViewControllers.contains(where: { $0.key == .favorites }), - let favoritesViewController = storyboard.instantiateViewController(withIdentifier: "OAFavoriteListViewController") as? OAFavoriteListViewController { + if !availableViewControllers.contains(where: { $0.key == .favorites }) { + let favoritesViewController = FavoriteListViewController(frame: pageViewController.view.frame) favoritesViewController.myPlacesDelegate = self availableViewControllers[tab] = favoritesViewController } @@ -203,10 +203,14 @@ final class MyPlacesContainerViewController: OACompoundViewController { } private func setupNavbarTitle(_ title: String, hideSubtitle: Bool) { + setupNavbarTitle(title, subtitle: localizedString("shared_string_my_places"), hideSubtitle: hideSubtitle) + } + + private func setupNavbarTitle(_ title: String, subtitle: String, hideSubtitle: Bool) { navigationItem.setStackViewWithTitle(title, titleColor: .textColorPrimary, titleFont: .scaledSystemFont(ofSize: 17.0, weight: .semibold, maximumSize: 22.0), - subtitle: hideSubtitle ? "" : localizedString("shared_string_my_places"), + subtitle: hideSubtitle ? "" : subtitle, subtitleColor: .textColorSecondary, subtitleFont: .scaledSystemFont(ofSize: 12.0, maximumSize: 18.0)) } @@ -313,6 +317,10 @@ extension MyPlacesContainerViewController: MyPlacesDelegate { func updateTitle(_ title: String, hideSubtitle: Bool) { setupNavbarTitle(title, hideSubtitle: hideSubtitle) } + + func updateTitle(_ title: String, subtitle: String, hideSubtitle: Bool) { + setupNavbarTitle(title, subtitle: subtitle, hideSubtitle: hideSubtitle) + } func updateToolbar(with items: [UIBarButtonItem]?) { toolbarItems = items diff --git a/Sources/Controllers/MyPlaces/OAFavoriteListViewController.h b/Sources/Controllers/MyPlaces/OAFavoriteListViewController.h deleted file mode 100644 index bb7532d57b..0000000000 --- a/Sources/Controllers/MyPlaces/OAFavoriteListViewController.h +++ /dev/null @@ -1,30 +0,0 @@ -// -// OAFavoriteListViewController.h -// OsmAnd -// -// Created by Anton Rogachevskiy on 07.11.14. -// Copyright (c) 2014 OsmAnd. All rights reserved. -// - -#import "OACompoundViewController.h" -#import "OAObservable.h" -#import "OAAutoObserverProxy.h" - -@protocol MyPlacesDelegate; - -@interface OAFavoriteListViewController : OACompoundViewController - -@property (weak, nonatomic) IBOutlet UITableView *favoriteTableView; -@property (weak, nonatomic) IBOutlet UIView *editToolbarView; -@property (weak, nonatomic) IBOutlet UIButton *exportButton; -@property (weak, nonatomic) IBOutlet UIButton *groupButton; -@property (weak, nonatomic) IBOutlet UIButton *colorButton; -@property (weak, nonatomic) IBOutlet UIButton *deleteButton; - -@property (nonatomic, weak) id myPlacesDelegate; - -@property CGFloat azimuthDirection; - -+ (BOOL)popToParent; - -@end diff --git a/Sources/Controllers/MyPlaces/OAFavoriteListViewController.mm b/Sources/Controllers/MyPlaces/OAFavoriteListViewController.mm deleted file mode 100644 index 7297cb3ec7..0000000000 --- a/Sources/Controllers/MyPlaces/OAFavoriteListViewController.mm +++ /dev/null @@ -1,2109 +0,0 @@ -// -// OAFavoriteListViewController.m -// OsmAnd -// -// Created by Anton Rogachevskiy on 07.11.14. -// Copyright (c) 2014 OsmAnd. All rights reserved. -// - -#import "OAFavoriteListViewController.h" -#import "OAPointTableViewCell.h" -#import "OAPointHeaderTableViewCell.h" -#import "OASimpleTableViewCell.h" -#import "OAFavoriteItem.h" -#import "OAFavoritesHelper.h" -#import "OALocationServices.h" -#import "OAMapViewController.h" -#import "OAMapPanelViewController.h" -#import "OADefaultFavorite.h" -#import "OAUtilities.h" -#import "OANativeUtilities.h" -#import "OAMultiselectableHeaderView.h" -#import "OAEditColorViewController.h" -#import "OAEditGroupViewController.h" -#import "OARootViewController.h" -#import "OATargetInfoViewController.h" -#import "OAFavoriteGroupEditorViewController.h" -#import "OASizes.h" -#import "OAColors.h" -#import "OAObservable.h" -#import "OAOsmAndFormatter.h" -#import "OAIndexConstants.h" -#import "OAGPXAppearanceCollection.h" -#import "OsmAndApp.h" -#import "OsmAnd_Maps-Swift.h" -#import "OAChoosePlanHelper.h" -#import "OACloudIntroductionViewController.h" -#import "OAAppSettings.h" -#import "OABackupHelper.h" -#import "OAFavoriteImportViewController.h" -#import "OABaseNavbarViewController.h" -#import "Localization.h" -#import -#import "GeneratedAssetSymbols.h" - -#include -#include -#include - -#define _(name) OAFavoriteListViewController__##name -#define kWasClosedFreeBackupFavoritesBannerKey @"wasClosedFreeBackupFavoritesBanner" - -#define FavoriteTableGroup _(FavoriteTableGroup) - -static const NSInteger _exportButtonIndex = 1; -static const NSStringCompareOptions searchOptions = NSCaseInsensitiveSearch | NSDiacriticInsensitiveSearch | NSWidthInsensitiveSearch; - -@interface FavoriteTableGroup : NSObject - @property BOOL isOpen; - @property OAFavoriteGroup *favoriteGroup; -@end - -@implementation FavoriteTableGroup - -- (instancetype)init -{ - self = [super init]; - if (self) - { - self.isOpen = NO; - } - return self; -} - -@end - -@interface OAFavoriteListViewController () - -@property (strong, nonatomic) NSArray *menuItems; -@property (strong, nonatomic) NSMutableArray *sortedFavoriteItems; -@property NSUInteger sortingType; -@property CFTimeInterval lastUpdate; - -@end - -@implementation OAFavoriteListViewController -{ - OAAutoObserverProxy *_locationUpdateObserver; - OAAutoObserverProxy *_headingUpdateObserver; - BOOL _decelerating; - - OAMultiselectableHeaderView *_sortedHeaderView; - OAMultiselectableHeaderView *_menuHeaderView; - NSArray *_unsortedHeaderViews; - NSMutableArray *_data; - NSMutableArray *_filteredItems; - - OAEditColorViewController *_colorController; - OAEditGroupViewController *_groupController; - - CALayer *_horizontalLine; - NSMutableArray *_selectedItems; - - UIBarButtonItem *_actionsButton; - UIBarButtonItem *_editButton; - FreeBackupBanner *_freeBackupBanner; - - BOOL _isSearchActive; - BOOL _isFiltered; - OAGPXAppearanceCollection *_appearanceCollection; - BOOL _contextMenuVisible; - - UIFont *_originalGroupFont; - UIFont *_italicGroupFont; - dispatch_queue_t _favoritesQueue; -} - -static UIViewController *parentController; - -+ (BOOL)popToParent -{ - if (!parentController) - return NO; - - [OAFavoriteListViewController doPop]; - - return YES; -} - -- (void)viewDidLoad -{ - [super viewDidLoad]; - _favoritesQueue = dispatch_queue_create("com.osmand.favorites", DISPATCH_QUEUE_SERIAL); - - _appearanceCollection = [OAGPXAppearanceCollection sharedInstance]; - _decelerating = NO; - self.sortingType = 0; - self.view.backgroundColor = [UIColor colorNamed:ACColorNameViewBg]; - - _sortedHeaderView = [[OAMultiselectableHeaderView alloc] initWithFrame:CGRectMake(0.0, 1.0, 100.0, 44.0)]; - _sortedHeaderView.delegate = self; - [_sortedHeaderView setTitleText:OALocalizedString(@"favorites_item")]; - - _menuHeaderView = [[OAMultiselectableHeaderView alloc] initWithFrame:CGRectMake(0.0, 1.0, 100.0, 44.0)]; - _menuHeaderView.editable = NO; - [_menuHeaderView setTitleText:OALocalizedString(@"import_export")]; - - _editToolbarView.hidden = YES; - - _horizontalLine = [CALayer layer]; - _horizontalLine.backgroundColor = [[UIColor colorNamed:ACColorNameCustomSeparator] CGColor]; - self.editToolbarView.backgroundColor = [UIColor colorNamed:ACColorNameGroupBg]; - [self.editToolbarView.layer addSublayer:_horizontalLine]; - - _selectedItems = [[NSMutableArray alloc] init]; - - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(productPurchased:) name:OAIAPProductPurchasedNotification object:nil]; - [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(favoriteImportViewControllerDidDismiss:) name:OAFavoriteImportViewControllerDidDismissNotification object:nil]; -} - -- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection -{ - [super traitCollectionDidChange:previousTraitCollection]; - - if ([self.traitCollection hasDifferentColorAppearanceComparedToTraitCollection:previousTraitCollection]) - _horizontalLine.backgroundColor = [[UIColor colorNamed:ACColorNameCustomSeparator] CGColor]; -} - -- (void)favoriteImportViewControllerDidDismiss:(NSNotification *)notification -{ - if (self.isViewLoaded && self.view.window != nil) - { - [self generateData]; - } -} - -- (void)productPurchased:(NSNotification *)notification -{ - dispatch_async(dispatch_get_main_queue(), ^{ - [self configurePaymentBanner]; - }); -} - -- (BOOL)isAvailablePaymentBanner -{ - return ![[NSUserDefaults standardUserDefaults] boolForKey:kWasClosedFreeBackupFavoritesBannerKey] - && ![OAIAPHelper isOsmAndProAvailable] - && !OABackupHelper.sharedInstance.isRegistered; -} - -- (void)resizeHeaderBanner -{ - if ([self isAvailablePaymentBanner] && _freeBackupBanner) - { - CGFloat titleHeight = [OAUtilities calculateTextBounds:_freeBackupBanner.titleLabel.text width:self.favoriteTableView.frame.size.width - _freeBackupBanner.leadingTrailingOffset font:[UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]].height; - - CGFloat descriptionHeight = [OAUtilities calculateTextBounds:_freeBackupBanner.descriptionLabel.text width:self.favoriteTableView.frame.size.width - _freeBackupBanner.leadingTrailingOffset font:[UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]].height; - _freeBackupBanner.frame = CGRectMake(0, - 0, - self.favoriteTableView.frame.size.width, - _freeBackupBanner.defaultFrameHeight + titleHeight + descriptionHeight); - self.favoriteTableView.tableHeaderView = _freeBackupBanner; - UIEdgeInsets insets = self.favoriteTableView.layoutMargins; - if (insets.left != 0 || insets.right != 0) - { - _freeBackupBanner.leadingSubviewConstraint.constant = insets.left; - _freeBackupBanner.trailingSubviewConstraint.constant = insets.right; - } - } -} - -- (void)configurePaymentBanner -{ - if ([self isAvailablePaymentBanner]) - { - if (!_freeBackupBanner) { - NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"FreeBackupBanner" owner:self options:nil]; - _freeBackupBanner = (FreeBackupBanner *)nib[0]; - __weak OAFavoriteListViewController *weakSelf = self; - _freeBackupBanner.didOsmAndCloudButtonAction = ^{ - [weakSelf.navigationController pushViewController:[OACloudIntroductionViewController new] animated:YES]; - }; - _freeBackupBanner.didCloseButtonAction = ^{ - [weakSelf closeFreeBackupBanner]; - }; - [_freeBackupBanner configureWithBannerType:BannerTypeFavorite]; - - [self configureSeparator:[UIView new] top:YES]; - [self configureSeparator:[UIView new] top:NO]; - - [self changeContentInsetTop:20]; - } - } - else if (_freeBackupBanner) { - [self closeFreeBackupBanner]; - } -} - -- (void)configureSeparator:(UIView *)view top:(BOOL)top -{ - view.translatesAutoresizingMaskIntoConstraints = NO; - view.backgroundColor = [UIColor colorNamed:ACColorNameCustomSeparator]; - [_freeBackupBanner addSubview:view]; - - [NSLayoutConstraint activateConstraints:@[ - [view.leadingAnchor constraintEqualToAnchor:_freeBackupBanner.leadingAnchor], - [view.trailingAnchor constraintEqualToAnchor:_freeBackupBanner.trailingAnchor], - [view.heightAnchor constraintEqualToConstant:1.0 / [UIScreen mainScreen].scale] - ]]; - - if (top) - [view.topAnchor constraintEqualToAnchor:_freeBackupBanner.topAnchor].active = YES; - else - [view.bottomAnchor constraintEqualToAnchor:_freeBackupBanner.bottomAnchor].active = YES; - -} - -- (void)closeFreeBackupBanner -{ - self.favoriteTableView.tableHeaderView = nil; - [self changeContentInsetTop:-20]; - _freeBackupBanner = nil; - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:kWasClosedFreeBackupFavoritesBannerKey]; -} - -- (void)changeContentInsetTop:(CGFloat)top -{ - UIEdgeInsets insets = [self.favoriteTableView contentInset]; - [self.favoriteTableView setContentInset:UIEdgeInsetsMake(insets.top + top, insets.left, insets.bottom, insets.right)]; -} - - --(void)viewWillLayoutSubviews -{ - [super viewWillLayoutSubviews]; - _horizontalLine.frame = CGRectMake(0.0, 0.0, DeviceScreenWidth, 0.5); -} - -- (void)viewDidLayoutSubviews -{ - [super viewDidLayoutSubviews]; - [self resizeHeaderBanner]; -} - --(UIView *) getMiddleView -{ - return _favoriteTableView; -} - --(UIView *) getBottomView -{ - return [self.favoriteTableView isEditing] ? _editToolbarView : nil; -} - --(CGFloat) getToolBarHeight -{ - return favoritesToolBarHeight; -} - -- (void)updateDistanceAndDirection -{ - dispatch_async(dispatch_get_main_queue(), ^{ - [self updateDistanceAndDirection:NO]; - }); -} - -- (void)updateDistanceAndDirection:(BOOL)forceUpdate -{ - if ([self.favoriteTableView isEditing]) - return; - - CFTimeInterval currentTime = CACurrentMediaTime(); - if (currentTime - self.lastUpdate < 0.3 && !forceUpdate) - return; - self.lastUpdate = currentTime; - - OsmAndAppInstance app = [OsmAndApp instance]; - // Obtain fresh location and heading - CLLocation *newLocation = app.locationServices.lastKnownLocation; - if (!newLocation) - return; - - CLLocationDirection newHeading = app.locationServices.lastKnownHeading; - CLLocationDirection newDirection = - (newLocation.speed >= 1 /* 3.7 km/h */ && newLocation.course >= 0.0f) - ? newLocation.course - : newHeading; - - __weak __typeof(self) weakSelf = self; - dispatch_async(_favoritesQueue, ^{ - __strong __typeof(self) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - NSMutableArray *itemsToProcess = strongSelf->_isFiltered - ? strongSelf->_filteredItems - : strongSelf.sortedFavoriteItems; - - [itemsToProcess enumerateObjectsUsingBlock:^(OAFavoriteItem *itemData, NSUInteger idx, BOOL *stop) { - const auto& favoritePosition31 = itemData.favorite->getPosition31(); - const auto favoriteLon = OsmAnd::Utilities::get31LongitudeX(favoritePosition31.x); - const auto favoriteLat = OsmAnd::Utilities::get31LatitudeY(favoritePosition31.y); - const auto distance = OsmAnd::Utilities::distance(newLocation.coordinate.longitude, - newLocation.coordinate.latitude, - favoriteLon, favoriteLat); - - itemData.distance = [OAOsmAndFormatter getFormattedDistance:distance]; - itemData.distanceMeters = distance; - CGFloat itemDirection = [app.locationServices radiusFromBearingToLocation:[[CLLocation alloc] initWithLatitude:favoriteLat longitude:favoriteLon]]; - itemData.direction = OsmAnd::Utilities::normalizedAngleDegrees(itemDirection - newDirection) * (M_PI / 180); - }]; - - if (strongSelf.sortingType == 1 && [itemsToProcess count] > 0) - { - [strongSelf sortItemsByDistance:itemsToProcess]; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - __strong __typeof(self) strongSelf = weakSelf; - if (!strongSelf) { - return; - } - if (strongSelf->_decelerating || strongSelf->_contextMenuVisible) - return; - [strongSelf refreshVisibleRows]; - }); - }); -} - -- (void)sortItemsByDistance:(NSMutableArray *)items -{ - NSArray *sortedArray = [items sortedArrayUsingComparator:^NSComparisonResult(OAFavoriteItem *obj1, OAFavoriteItem *obj2) { - return obj1.distanceMeters > obj2.distanceMeters ? NSOrderedDescending : - (obj1.distanceMeters < obj2.distanceMeters ? NSOrderedAscending : NSOrderedSame); - }]; - [items setArray:sortedArray]; -} - -- (void)refreshVisibleRows -{ - if ([self.favoriteTableView isEditing]) - return; - - NSArray *visibleIndexPaths = [self.favoriteTableView indexPathsForVisibleRows]; - for (NSIndexPath *i in visibleIndexPaths) - { - UITableViewCell *cell = [self.favoriteTableView cellForRowAtIndexPath:i]; - if ([cell isKindOfClass:[OAPointTableViewCell class]]) - { - OAFavoriteItem* item; - if (_isSearchActive) - { - if (i.section == 0) - item = [_isFiltered ? _filteredItems : self.sortedFavoriteItems objectAtIndex:i.row]; - } - else - { - NSDictionary *groupData = _data[i.section][0]; - NSString *cellType = groupData[@"type"]; - if ([cellType isEqualToString:@"group"]) - { - FavoriteTableGroup *group = groupData[@"group"]; - if (group.favoriteGroup.points != nil && [group.favoriteGroup.points count] > (i.row - 1)) { - item = [group.favoriteGroup.points objectAtIndex:i.row - 1]; - } - } - } - - if (item) - { - OAPointTableViewCell *c = (OAPointTableViewCell *)cell; - - [c.titleView setText:[item getDisplayName]]; - c = [self setupPoiIconForCell:c withFavoriteItem:item]; - - [c.distanceView setText:item.distance]; - c.directionImageView.transform = CGAffineTransformMakeRotation(item.direction); - } - } - } - - [self.favoriteTableView reloadData]; -} - -- (void)viewWillAppear:(BOOL)animated -{ - [super viewWillAppear:animated]; - - [self setupView]; - [self generateData]; - [self setupView]; - [self updateDistanceAndDirection:YES]; - - OsmAndAppInstance app = [OsmAndApp instance]; - _locationUpdateObserver = [[OAAutoObserverProxy alloc] initWith:self - withHandler:@selector(updateDistanceAndDirection) - andObserve:app.locationServices.updateLocationObserver]; - _headingUpdateObserver = [[OAAutoObserverProxy alloc] initWith:self - withHandler:@selector(updateDistanceAndDirection) - andObserve:app.locationServices.updateHeadingObserver]; - [self applySafeAreaMargins]; - [self.navigationController setNavigationBarHidden:NO animated:NO]; - _editButton = [OABaseNavbarViewController createRightNavbarButton:OALocalizedString(@"shared_string_select") - icon:nil - color:[UIColor labelColor] - action:@selector(editButtonClicked:) - target:self - menu:nil]; - if (@available(iOS 26.0, *)) - { - _editButton.style = UIBarButtonItemStyleProminent; - _editButton.tintColor = [[UIColor colorNamed:ACColorNameNavBarTextColorPrimary] colorWithAlphaComponent:.3]; - } - _actionsButton = [[UIBarButtonItem alloc] initWithImage:[UIImage systemImageNamed:@"ellipsis.circle"] menu:[self actionsMenu]]; - [self.navigationController.navigationBar.topItem setRightBarButtonItems:@[_actionsButton, _editButton] animated:YES]; - self.definesPresentationContext = YES; - [self addAccessibilityLabels]; - [self configurePaymentBanner]; - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; -} - -- (void)viewWillDisappear:(BOOL)animated -{ - [super viewWillDisappear:animated]; - - if (_locationUpdateObserver) - { - [_locationUpdateObserver detach]; - _locationUpdateObserver = nil; - } - if (_headingUpdateObserver) - { - [_headingUpdateObserver detach]; - _headingUpdateObserver = nil; - } - - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; - [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; - self.definesPresentationContext = NO; -} - --(void) addAccessibilityLabels -{ - _editButton.accessibilityLabel = OALocalizedString(@"shared_string_select"); - self.exportButton.accessibilityLabel = OALocalizedString(@"shared_string_export"); - self.deleteButton.accessibilityLabel = OALocalizedString(@"shared_string_delete"); -} - --(void)generateData -{ - NSMutableArray *allGroups = [[NSMutableArray alloc] init]; - self.menuItems = [[NSArray alloc] init]; - self.sortedFavoriteItems = [[NSMutableArray alloc] init]; - - NSMutableArray *headerViews = [NSMutableArray array]; - NSMutableArray *tableData = [NSMutableArray array]; - - NSArray *favoriteGroups = [OAFavoritesHelper getFavoriteGroups]; - for (OAFavoriteGroup *group in favoriteGroups) - { - FavoriteTableGroup* itemData = [[FavoriteTableGroup alloc] init]; - itemData.favoriteGroup = group; - - // Sort items - NSArray *sortedArrayItems = [itemData.favoriteGroup.points sortedArrayUsingComparator:^NSComparisonResult(OAFavoriteItem* obj1, OAFavoriteItem* obj2) { - BOOL obj1Visible = obj1.isVisible; - BOOL obj2Visible = obj2.isVisible; - if (obj1Visible != obj2Visible) - return obj1Visible ? NSOrderedAscending : NSOrderedDescending; - - return [[[obj1 getDisplayName] lowercaseString] compare:[[obj2 getDisplayName] lowercaseString]]; - }]; - [itemData.favoriteGroup.points setArray:sortedArrayItems]; - - for (OAFavoriteItem *item in group.points) - [self.sortedFavoriteItems addObject:item]; - [allGroups addObject:itemData]; - } - [allGroups sortUsingComparator:^NSComparisonResult(FavoriteTableGroup * _Nonnull obj1, FavoriteTableGroup * _Nonnull obj2) { - BOOL group1Visible = [obj1.favoriteGroup isVisible]; - BOOL group2Visible = [obj2.favoriteGroup isVisible]; - return group1Visible == group2Visible - ? NSOrderedSame - : group1Visible ? NSOrderedAscending : NSOrderedDescending; - }]; - - if (!_isSearchActive) - { - NSArray *sortedArray = [self.sortedFavoriteItems sortedArrayUsingComparator:^NSComparisonResult(OAFavoriteItem* obj1, OAFavoriteItem* obj2) { - return obj1.distanceMeters > obj2.distanceMeters ? NSOrderedDescending : obj1.distanceMeters < obj2.distanceMeters ? NSOrderedAscending : NSOrderedSame; - }]; - [self.sortedFavoriteItems setArray:sortedArray]; - } - for (FavoriteTableGroup *group in allGroups) - { - NSMutableArray *groupData = [NSMutableArray array]; - [groupData addObject:@{ - @"type" : @"group", - @"group" : group - }]; - [tableData addObject:groupData]; - } - - for (int i = 0; i < tableData.count;) - { - OAMultiselectableHeaderView *headerView = [[OAMultiselectableHeaderView alloc] initWithFrame:CGRectMake(0.0, 1.0, 100.0, 44.0)]; - [headerView.selectAllBtn setHidden:YES]; - headerView.section = i++; - headerView.delegate = self; - [headerViews addObject:headerView]; - } - - // Generate menu items - self.menuItems = @[@{@"type" : @"actionItem", - @"text": OALocalizedString(@"fav_import_title"), - @"icon": @"ic_custom_import", - @"action": @"onImportClicked"}, - @{@"type" : @"actionItem", - @"text": OALocalizedString(@"fav_export_title"), - @"icon": @"ic_custom_export", - @"action": @"onExportClicked"}]; - [tableData addObject:self.menuItems]; - - OAMultiselectableHeaderView *headerView = [[OAMultiselectableHeaderView alloc] initWithFrame:CGRectMake(0.0, 1.0, 100.0, 44.0)]; - [headerView setTitleText:OALocalizedString(@"import_export")]; - headerView.editable = NO; - [headerViews addObject:headerView]; - - _data = [NSMutableArray arrayWithArray:tableData]; - - [self.favoriteTableView reloadData]; - - _unsortedHeaderViews = [NSArray arrayWithArray:headerViews]; -} - --(void)setupView -{ - self.favoriteTableView.separatorInset = UIEdgeInsetsMake(0.0, 62.0, 0.0, 0.0); - [self.favoriteTableView setDataSource:self]; - [self.favoriteTableView setDelegate:self]; - self.favoriteTableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero]; - [self.favoriteTableView reloadData]; - _isSearchActive = NO; - _isFiltered = NO; -} - -- (UIMenu *)actionsMenu -{ - __weak __typeof(self) weakSelf = self; - UIAction *importAction = [UIAction actionWithTitle:OALocalizedString(@"shared_string_import") - image:[[UIImage imageNamed:ACImageNameIcCustomImportOutlined] resizedMenuImage] - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - [weakSelf onImportClicked]; - }]; - return [UIMenu menuWithTitle:@"" - image:nil - identifier:nil - options:UIMenuOptionsDisplayInline - children:@[importAction]]; -} - -- (void)didReceiveMemoryWarning -{ - [super didReceiveMemoryWarning]; - // Dispose of any resources that can be recreated. -} - -- (void)alertTextFieldDidChange:(UITextField *)textField -{ - UIAlertController *alert = (UIAlertController *)self.presentedViewController; - if (alert) - { - UIAlertAction *applyAction = alert.actions.firstObject; - applyAction.enabled = [OAFavoritesHelper isGroupNameValidWithText:[textField.text trim]]; - } -} - -#pragma mark - Actions - -- (IBAction) deletePressed:(id)sender -{ - if ([_selectedItems count] == 0) - { - UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"" - message:OALocalizedString(@"fav_select_remove") - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:OALocalizedString(@"shared_string_ok") style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {}]; - - [alert addAction:defaultAction]; - [self presentViewController:alert animated:YES completion:nil]; - - return; - } - - UIAlertController *alert = [UIAlertController - alertControllerWithTitle:nil - message:OALocalizedString(@"fav_remove_q") - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *yesButton = [UIAlertAction - actionWithTitle:OALocalizedString(@"shared_string_yes") - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * _Nonnull action) { - [self removeFavoriteItems]; - }]; - UIAlertAction *cancelButton = [UIAlertAction - actionWithTitle:OALocalizedString(@"shared_string_no") - style:UIAlertActionStyleCancel - handler:nil]; - [alert addAction:yesButton]; - [alert addAction:cancelButton]; - [self presentViewController:alert animated:YES completion:nil]; - -} - -- (IBAction) favoriteChangeColorClicked:(id)sender -{ - if ([_selectedItems count] == 0) - { - UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"" - message:OALocalizedString(@"fav_select") - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:OALocalizedString(@"shared_string_ok") style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {}]; - - [alert addAction:defaultAction]; - [self presentViewController:alert animated:YES completion:nil]; - - return; - } - - _colorController = [[OAEditColorViewController alloc] init]; - _colorController.delegate = self; - UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:_colorController]; - [self.navigationController presentViewController:navigationController animated:YES completion:nil]; -} - -- (IBAction) favoriteChangeGroupClicked:(id)sender -{ - if ([_selectedItems count] == 0) - { - UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"" - message:OALocalizedString(@"fav_select") - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction* defaultAction = [UIAlertAction actionWithTitle:OALocalizedString(@"shared_string_ok") style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) {}]; - - [alert addAction:defaultAction]; - [self presentViewController:alert animated:YES completion:nil]; - - return; - } - - NSMutableArray *groupNames = [NSMutableArray array]; - for (OAFavoriteGroup *group in [OAFavoritesHelper getFavoriteGroups]) - { - [groupNames addObject:group.name]; - } - _groupController = [[OAEditGroupViewController alloc] initWithGroupName:nil groups:groupNames]; - _groupController.delegate = self; - UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:_groupController]; - [self.navigationController presentViewController:navigationController animated:YES completion:nil]; -} - -#pragma mark - OAEditColorViewControllerDelegate - -- (void)colorChanged -{ - if ([_selectedItems count] == 0) - return; - - if (_colorController.saveChanges) - { - OAFavoriteColor *favCol = [[OADefaultFavorite builtinColors] objectAtIndex:_colorController.colorIndex]; - NSMutableSet *groupNames = [NSMutableSet set]; - - for (NSIndexPath *indexPath in _selectedItems) - { - OAFavoriteItem* item; - if (_isSearchActive) - { - if (indexPath.section == 0) - item = [self.sortedFavoriteItems objectAtIndex:indexPath.row]; - } - else - { - NSDictionary *groupData = _data[indexPath.section][0]; - NSString *cellType = groupData[@"type"]; - if ([cellType isEqualToString:@"group"]) - { - FavoriteTableGroup* tableGroup = groupData[@"group"]; - if (indexPath.row != 0) - item = [tableGroup.favoriteGroup.points objectAtIndex:indexPath.row - 1]; - else - tableGroup.favoriteGroup.color = favCol.color; - } - } - - if (item) - { - [item setColor:favCol.color]; - [groupNames addObject:item.getCategory]; - - if (indexPath.row == 1) - { - OAFavoriteGroup *group = [OAFavoritesHelper getGroupByName:[item getCategory]]; - group.color = favCol.color; - } - } - } - [OAFavoritesHelper saveCurrentPointsIntoFile]; - } - [self finishEditing]; - [self.favoriteTableView reloadData]; -} - -#pragma mark - OAEditGroupViewControllerDelegate - -- (void)groupChanged -{ - if ([_selectedItems count] == 0) - return; - - if (_groupController.saveChanges) - { - NSMutableArray * sortedSelectedItems = [NSMutableArray arrayWithArray:_selectedItems]; - [sortedSelectedItems sortUsingComparator:^NSComparisonResult(NSIndexPath* obj1, NSIndexPath* obj2) { - NSNumber *row1 = [NSNumber numberWithInteger:obj1.row]; - NSNumber *row2 = [NSNumber numberWithInteger:obj2.row]; - return [row2 compare:row1]; - }]; - - NSMutableSet *groupNames = [NSMutableSet set]; - for (NSIndexPath *indexPath in sortedSelectedItems) - { - OAFavoriteItem* item; - if (_isSearchActive) - { - if (indexPath.section == 0) - item = [self.sortedFavoriteItems objectAtIndex:indexPath.row]; - } - else - { - NSDictionary *groupData = _data[indexPath.section][0]; - NSString *cellType = groupData[@"type"]; - if ([cellType isEqualToString:@"group"]) - { - if (indexPath.row != 0) - { - FavoriteTableGroup *group = groupData[@"group"]; - NSArray *points = [group.favoriteGroup.points copy]; - NSInteger index = indexPath.row - 1; - if (index >= 0 && index < points.count) - item = points[index]; - else - NSLog(@"OAFavoriteListViewController [ERROR] Invalid index %ld for points.count=%lu (section=%ld)", - (long)index, (unsigned long)points.count, (long)indexPath.section); - } - } - } - - if (item) - { - [groupNames addObject:item.getCategory]; - [OAFavoritesHelper editFavoriteName:item newName:[item getDisplayName] group:_groupController.groupName descr:[item getDescription] address:[item getAddress]]; - } - } - } - [self finishEditing]; - [self generateData]; -} - -- (NSArray *)getItemsForRows:(NSArray *)indexPath -{ - NSMutableArray *itemList = [[NSMutableArray alloc] init]; - if (_isSearchActive) - { // Sorted - [indexPath enumerateObjectsUsingBlock:^(NSIndexPath* path, NSUInteger idx, BOOL *stop) { - [itemList addObject:[self.sortedFavoriteItems objectAtIndex:path.row]]; - }]; - } - else - { - [indexPath enumerateObjectsUsingBlock:^(NSIndexPath* path, NSUInteger idx, BOOL *stop) { - NSDictionary *groupData = _data[path.section][0]; - FavoriteTableGroup* group = groupData[@"group"]; - if (path.row != 0) - { - [itemList addObject:[group.favoriteGroup.points objectAtIndex:path.row - 1]]; - } - }]; - } - return itemList; -} - -- (void) startEditing -{ - [self setEdit:YES]; - _editToolbarView.frame = CGRectMake(0.0, DeviceScreenHeight + 1.0, DeviceScreenWidth, _editToolbarView.bounds.size.height); - _editToolbarView.hidden = NO; - [self applySafeAreaMargins]; - [_myPlacesDelegate showBackButton:NO]; - [self.navigationController.navigationBar.topItem setRightBarButtonItems:@[_editButton] animated:YES]; - [self.favoriteTableView reloadData]; -} - -- (void) finishEditing -{ - _editToolbarView.frame = CGRectMake(0.0, DeviceScreenHeight - _editToolbarView.bounds.size.height, DeviceScreenWidth, _editToolbarView.bounds.size.height); - [UIView animateWithDuration:.3 animations:^{ - _editToolbarView.frame = CGRectMake(0.0, DeviceScreenHeight + 1.0, DeviceScreenWidth, _editToolbarView.bounds.size.height); - } completion:^(BOOL finished) { - _editToolbarView.hidden = YES; - [self applySafeAreaMargins]; - }]; - - [_myPlacesDelegate showBackButton:YES]; - - [self.navigationController.navigationBar.topItem setRightBarButtonItems:@[_actionsButton, _editButton] animated:YES]; - [self setEdit:NO]; - [_selectedItems removeAllObjects]; -} - -- (void)setEdit:(BOOL)isEdit -{ - [self.favoriteTableView setEditing:isEdit animated:YES]; - [_myPlacesDelegate updateEditMode:isEdit]; -} - -- (IBAction)editButtonClicked:(id)sender -{ - [self.favoriteTableView beginUpdates]; - if ([self.favoriteTableView isEditing]) - [self finishEditing]; - else - [self startEditing]; - [self.favoriteTableView endUpdates]; -} - -- (IBAction) shareButtonClicked:(id)sender -{ - // Share selected favorites - UIButton *clickedButton = (UIButton *)sender; - [self shareItems:_selectedItems sounceItem:clickedButton]; - [self finishEditing]; - [self generateData]; -} - -- (void)shareItems:(NSArray *)selectedItems sounceItem:(UIView *)sounceItem -{ - if ([selectedItems count] == 0) - { - UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"" - message:OALocalizedString(@"fav_export_select") - preferredStyle:UIAlertControllerStyleAlert]; - UIAlertAction *defaultAction = [UIAlertAction actionWithTitle:OALocalizedString(@"shared_string_ok") style:UIAlertActionStyleDefault handler:nil]; - [alert addAction:defaultAction]; - [self presentViewController:alert animated:YES completion:nil]; - return; - } - - NSArray *selectedFavoriteItems = [self getItemsForRows:selectedItems]; - NSMutableDictionary *groups = [NSMutableDictionary dictionary]; - for (OAFavoriteItem *point in selectedFavoriteItems) - { - OAFavoriteGroup *group = groups[[point getCategory]]; - if (!group) - { - group = [[OAFavoriteGroup alloc] initWithPoint:point]; - for (OAFavoriteGroup *favoriteGroup in [OAFavoritesHelper getFavoriteGroups]) - { - if ([favoriteGroup.name isEqualToString:group.name]) - { - group.color = favoriteGroup.color; - group.isVisible = favoriteGroup.isVisible; - group.iconName = favoriteGroup.iconName; - group.backgroundType = favoriteGroup.backgroundType; - break; - } - } - groups[[point getCategory]] = group; - } - [group.points addObject:point]; - } - - OsmAndAppInstance app = [OsmAndApp instance]; - NSString *filename = app.favoritesFilePrefix; - if (groups.count == 1) - { - NSString *groupName = [OsmAndApp.instance getGroupFileName:groups.allKeys.firstObject]; - filename = [NSString stringWithFormat:@"%@%@%@", - filename, - groupName.length > 0 ? app.favoritesGroupNameSeparator : @"", - groupName]; - } - filename = [filename stringByAppendingString:GPX_FILE_EXT]; - - NSString *fullFilename = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; - [OAFavoritesHelper saveFile:groups.allValues file:fullFilename]; - - NSURL *favoritesUrl = [NSURL fileURLWithPath:fullFilename]; - - [self showActivity:@[favoritesUrl] sourceView:sounceItem barButtonItem:nil completionWithItemsHandler:^{ - [NSFileManager.defaultManager removeItemAtURL:favoritesUrl error:nil]; - }]; -} - -- (IBAction)goRootScreen:(id)sender -{ - [self.navigationController popToRootViewControllerAnimated:YES]; -} - --(void)onImportClicked -{ - NSArray *contentTypes = @[[UTType importedTypeWithIdentifier:@"com.topografix.gpx" conformingToType:UTTypeXML]]; - UIDocumentPickerViewController *documentPickerVC = [[UIDocumentPickerViewController alloc] initForOpeningContentTypes:contentTypes asCopy:YES]; - documentPickerVC.allowsMultipleSelection = NO; - documentPickerVC.delegate = self; - [self presentViewController:documentPickerVC animated:YES completion:nil]; -} - -- (void)onExportClicked -{ - if (self.sortedFavoriteItems.count == 0) - return; - - NSString *filename = [[OsmAndApp instance].favoritesFilePrefix stringByAppendingString:GPX_FILE_EXT]; - NSString *fullFilename = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; - [OAFavoritesHelper saveFile:[OAFavoritesHelper getFavoriteGroups] file:fullFilename]; - - NSURL *favoritesUrl = [NSURL fileURLWithPath:fullFilename]; - - //export button is on last section, last row - NSIndexPath *exportButtonIndex = [NSIndexPath indexPathForRow:_exportButtonIndex inSection:_data.count - 1]; - UITableViewCell *cell = [self.favoriteTableView cellForRowAtIndexPath:exportButtonIndex]; - [self showActivity:@[favoritesUrl] sourceView:cell barButtonItem:nil completionWithItemsHandler:^{ - [NSFileManager.defaultManager removeItemAtURL:favoritesUrl error:nil]; - }]; -} - -#pragma mark - UITableViewDataSource -- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView -{ - return _isSearchActive ? [self getSortedNumberOfSectionsInTableView] : [self getUnsortedNumberOfSectionsInTableView]; -} - --(NSInteger)getSortedNumberOfSectionsInTableView -{ - return _isSearchActive ? 1 : 2; -} - --(NSInteger)getUnsortedNumberOfSectionsInTableView -{ - return _data.count; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section -{ - if (_isSearchActive) - return 0; - else if (_data.count == 1) - return 44; - NSDictionary *item = _data[section][0]; - NSString *cellType = item[@"type"]; - return [cellType isEqualToString:@"actionItem"] || _isSearchActive ? 44 : 16; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section -{ - return 0.01; -} - -- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section -{ - return _isSearchActive ? nil : _unsortedHeaderViews[section]; -} - -- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (indexPath.section != self.favoriteTableView.numberOfSections - 1 || _isSearchActive) - return 60.; - return 44.; -} - -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section -{ - return _isSearchActive ? [self getSortedNumberOfRowsInSection:section] : [self getUnsortedNumberOfRowsInSection:section]; -} - --(NSInteger)getSortedNumberOfRowsInSection:(NSInteger)section -{ - if (section == 0 || _isSearchActive) - return _isFiltered ? [_filteredItems count] : [self.sortedFavoriteItems count]; - return _data.lastObject.count; -} - --(NSInteger)getUnsortedNumberOfRowsInSection:(NSInteger)section -{ - NSDictionary *item = _data[section][0]; - NSString *cellType = item[@"type"]; - if ([cellType isEqualToString:@"group"]) - { - FavoriteTableGroup* groupData = item[@"group"]; - if (groupData.isOpen) - return [groupData.favoriteGroup.points count] + 1; - return 1; - } - return _data[section].count; -} - -- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point -{ - if (!(_data && indexPath.section >= 0 && indexPath.section < _data.count && [_data[indexPath.section] count] > 0)) - return nil; - - NSDictionary *item = _data[indexPath.section][0]; - NSString *cellType = item[@"type"]; - if (indexPath.row == 0 &&[cellType isEqualToString:@"group"]) - { - NSMutableArray *menuElements = [NSMutableArray array]; - FavoriteTableGroup *groupData = item[@"group"]; - NSInteger section = indexPath.section; - BOOL isVisible = groupData.favoriteGroup.isVisible; - NSString *showHideCaption = isVisible ? OALocalizedString(@"shared_string_hide_from_map") : OALocalizedString(@"shared_string_show_on_map"); - UIImage *showHideImage = [[UIImage imageNamed:isVisible ? ACImageNameIcCustomHideOutlined : ACImageNameIcCustomShowOutlined] resizedMenuImage]; - UIAction *showHideAction = [UIAction actionWithTitle:showHideCaption - image:showHideImage - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - [OAFavoritesHelper updateGroup:groupData.favoriteGroup visible:!isVisible saveImmediately:YES]; - [self generateData]; - }]; - showHideAction.accessibilityLabel = showHideCaption; - - UIImage *renameImage = [[UIImage imageNamed:ACImageNameIcCustomEdit] resizedMenuImage]; - UIAction *renameAction = [UIAction actionWithTitle:OALocalizedString(@"shared_string_rename") - image:renameImage - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - [self openRenameAlertWith:groupData.favoriteGroup]; - }]; - renameAction.accessibilityLabel = OALocalizedString(@"shared_string_rename"); - [menuElements addObject:[UIMenu menuWithTitle:@"" image:nil identifier:nil options:UIMenuOptionsDisplayInline children:@[showHideAction, renameAction]]]; - - NSString *appearanceName = OALocalizedString(@"default_appearance"); - UIAction *appearanceAction = [UIAction actionWithTitle:appearanceName - image:[[UIImage systemImageNamed:@"paintpalette"] resizedMenuImage] - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - OAFavoriteGroupEditorViewController *viewController = [[OAFavoriteGroupEditorViewController alloc] initWithGroup:[groupData.favoriteGroup toPointsGroup]]; - viewController.delegate = self; - [self.navigationController pushViewController:viewController animated:YES]; - }]; - appearanceAction.accessibilityLabel = appearanceName; - [menuElements addObject:appearanceAction]; - - UIAction *shareAction = [UIAction actionWithTitle:OALocalizedString(@"shared_string_share") - image:[[UIImage systemImageNamed:@"square.and.arrow.up"] resizedMenuImage] - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - NSMutableArray *indexPaths = [NSMutableArray array]; - for (NSInteger i = 0; i <= groupData.favoriteGroup.points.count; i++) - { - [indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:section]]; - } - - UITableViewCell *cell = [self.favoriteTableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:section]]; - [self shareItems:indexPaths sounceItem:(cell ?: self.view)]; - }]; - shareAction.accessibilityLabel = OALocalizedString(@"shared_string_share"); - [menuElements addObject:shareAction]; - - UIAction *deleteAction = [UIAction actionWithTitle:OALocalizedString(@"shared_string_delete") - image:[[UIImage systemImageNamed:@"trash"] resizedMenuImage] - identifier:nil - handler:^(__kindof UIAction * _Nonnull action) { - - UIAlertController *alert = [UIAlertController - alertControllerWithTitle:nil - message:OALocalizedString(@"fav_remove_q") - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *yesButton = [UIAlertAction - actionWithTitle:OALocalizedString(@"shared_string_yes") - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * _Nonnull action) { - for (NSInteger i = 0; i <= groupData.favoriteGroup.points.count; i++) - { - [self addIndexPathToSelectedCellsArray:[NSIndexPath indexPathForRow:i inSection:section]]; - } - [self removeFavoriteItems]; - }]; - UIAlertAction *cancelButton = [UIAlertAction actionWithTitle:OALocalizedString(@"shared_string_no") style:UIAlertActionStyleCancel handler:nil]; - [alert addAction:yesButton]; - [alert addAction:cancelButton]; - [self presentViewController:alert animated:YES completion:nil]; - }]; - deleteAction.accessibilityLabel = OALocalizedString(@"shared_string_delete"); - deleteAction.attributes = UIMenuElementAttributesDestructive; - [menuElements addObject:[UIMenu menuWithTitle:@"" image:nil identifier:nil options:UIMenuOptionsDisplayInline children:@[deleteAction]]]; - - UIMenu *contextMenu = [UIMenu menuWithChildren:menuElements]; - return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil actionProvider:^UIMenu * _Nullable(NSArray * _Nonnull suggestedActions) { - return contextMenu; - }]; - } - - return nil; -} - -- (UITargetedPreview *)tableView:(UITableView *)tableView previewForHighlightingContextMenuWithConfiguration:(UIContextMenuConfiguration *)configuration -{ - _contextMenuVisible = YES; - return nil; -} - -- (void)tableView:(UITableView *)tableView willEndContextMenuInteractionWithConfiguration:(UIContextMenuConfiguration *)configuration animator:(id)animator -{ - [animator addCompletion:^{ - _contextMenuVisible = NO; - }]; -} - -- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - return _isSearchActive ? [self getSortedcellForRowAtIndexPath:indexPath] : [self getUnsortedcellForRowAtIndexPath:indexPath]; -} - -- (void)openRenameAlertWith:(OAFavoriteGroup *)favoriteGroup -{ - UIAlertController *alert = [UIAlertController alertControllerWithTitle:OALocalizedString(@"shared_string_rename") - message:OALocalizedString(@"enter_new_name") - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *applyAction = [UIAlertAction actionWithTitle:OALocalizedString(@"shared_string_apply") - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * _Nonnull action) { - NSString *name = [alert.textFields.firstObject.text trim]; - if (name.length > 0) - { - [OAFavoritesHelper updateGroup:favoriteGroup - newName:name - saveImmediately:YES]; - [self generateData]; - } - }]; - [alert addAction:applyAction]; - [alert addAction:[UIAlertAction actionWithTitle:OALocalizedString(@"shared_string_cancel") - style:UIAlertActionStyleCancel - handler:nil]]; - - [alert addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) { - textField.placeholder = OALocalizedString(@"enter_new_name"); - textField.text = favoriteGroup.name; - - applyAction.enabled = textField.text.length > 0; - [textField addTarget:self - action:@selector(alertTextFieldDidChange:) - forControlEvents:UIControlEventEditingChanged]; - }]; - - [alert setPreferredAction:applyAction]; - [self presentViewController:alert animated:YES completion:nil]; -} - --(UITableViewCell*)getSortedcellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (indexPath.section == 0 || _isSearchActive) - { - OAPointTableViewCell* cell; - cell = (OAPointTableViewCell *)[self.favoriteTableView dequeueReusableCellWithIdentifier:[OAPointTableViewCell getCellIdentifier]]; - if (cell == nil) - { - NSArray *nib = [[NSBundle mainBundle] loadNibNamed:[OAPointTableViewCell getCellIdentifier] owner:self options:nil]; - cell = (OAPointTableViewCell *)[nib objectAtIndex:0]; - } - - if (cell) - { - OAFavoriteItem* item = _isFiltered ? [_filteredItems objectAtIndex:indexPath.row] : [self.sortedFavoriteItems objectAtIndex:indexPath.row]; - [cell.titleView setText:[item getDisplayName]]; - cell = [self setupPoiIconForCell:cell withFavoriteItem:item]; - - [cell.distanceView setText:item.distance]; - cell.directionImageView.image = [UIImage templateImageNamed:@"ic_small_direction"]; - cell.directionImageView.tintColor = UIColorFromRGB(color_elevation_chart); - cell.directionImageView.transform = CGAffineTransformMakeRotation(item.direction); - } - - return cell; - - } - else - { - OASimpleTableViewCell* cell; - cell = (OASimpleTableViewCell *)[self.favoriteTableView dequeueReusableCellWithIdentifier:[OASimpleTableViewCell getCellIdentifier]]; - if (cell == nil) - { - NSArray *nib = [[NSBundle mainBundle] loadNibNamed:[OASimpleTableViewCell getCellIdentifier] owner:self options:nil]; - cell = (OASimpleTableViewCell *)[nib objectAtIndex:0]; - [cell descriptionVisibility:NO]; - } - - if (cell) - { - NSDictionary* item = [self.menuItems objectAtIndex:indexPath.row]; - [cell.titleLabel setText:[item objectForKey:@"text"]]; - [cell.leftIconView setImage:[UIImage imageNamed:[item objectForKey:@"icon"]]]; - } - return cell; - } -} - -- (UITableViewCell*)getUnsortedcellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSDictionary *item = _data[indexPath.section][0]; - NSString *cellType = item[@"type"]; - if ([cellType isEqualToString:@"group"]) - { - if (indexPath.row == 0) - return [self getGroupHeaderCellForRowAtIndexPath:indexPath]; - else - return [self getGroupElementCellForRowAtIndexPath:indexPath]; - } - return [self getActionCellForRowAtIndexPath:indexPath]; -} - -- (UITableViewCell*)getGroupHeaderCellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSDictionary *item = _data[indexPath.section][0]; - FavoriteTableGroup* groupData = item[@"group"]; - - OAPointHeaderTableViewCell* cell; - cell = (OAPointHeaderTableViewCell *)[self.favoriteTableView dequeueReusableCellWithIdentifier:[OAPointHeaderTableViewCell getCellIdentifier]]; - if (cell == nil) - { - NSArray *nib = [[NSBundle mainBundle] loadNibNamed:[OAPointHeaderTableViewCell getCellIdentifier] owner:self options:nil]; - cell = (OAPointHeaderTableViewCell *)[nib objectAtIndex:0]; - _originalGroupFont = cell.groupTitle.font; - UIFontDescriptor * italicDescriptor = [cell.groupTitle.font.fontDescriptor fontDescriptorWithSymbolicTraits:UIFontDescriptorTraitItalic]; - _italicGroupFont = [UIFont fontWithDescriptor:italicDescriptor size:0]; - [cell.valueLabel setHidden:YES]; - } - if (cell) - { - OAFavoriteGroup* group = groupData.favoriteGroup; - [cell.groupTitle setText:[OAFavoriteGroup getDisplayName:group.name]]; - BOOL visible = group.isVisible; - if (visible) - { - cell.groupTitle.font = _originalGroupFont; - cell.groupTitle.textColor = [UIColor colorNamed:ACColorNameTextColorPrimary]; - cell.folderIcon.image = [UIImage templateImageNamed:@"ic_custom_folder"]; - cell.folderIcon.tintColor = groupData.favoriteGroup.color; - } - else - { - cell.groupTitle.font = _italicGroupFont; - cell.groupTitle.textColor = [UIColor colorNamed:ACColorNameTextColorSecondary]; - cell.folderIcon.image = [UIImage templateImageNamed:@"ic_custom_folder_hidden_outlined"]; - cell.folderIcon.tintColor = [UIColor colorNamed:ACColorNameIconColorSecondary]; - } - - cell.openCloseGroupButton.tag = indexPath.section << 10 | indexPath.row; - [cell.openCloseGroupButton removeTarget:nil action:NULL forControlEvents:UIControlEventTouchUpInside]; - [cell.openCloseGroupButton addTarget:self action:@selector(openCloseGroupButtonAction:) forControlEvents:UIControlEventTouchUpInside]; - if ([self.favoriteTableView isEditing]) - [cell.openCloseGroupButton setHidden:NO]; - else - [cell.openCloseGroupButton setHidden:YES]; - - if (groupData.isOpen) - { - cell.arrowImage.image = [UIImage templateImageNamed:ACImageNameIcCustomArrowDown]; - } - else - { - cell.arrowImage.image = [UIImage templateImageNamed:@"ic_custom_arrow_right"].imageFlippedForRightToLeftLayoutDirection; - if ([cell isDirectionRTL]) - [cell.arrowImage setImage:cell.arrowImage.image.imageFlippedForRightToLeftLayoutDirection]; - } - cell.arrowImage.tintColor = [UIColor colorNamed:ACColorNameIconColorDefault]; - } - return cell; -} - -- (UITableViewCell*)getGroupElementCellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSDictionary *item = _data[indexPath.section][0]; - FavoriteTableGroup* groupData = item[@"group"]; - - NSInteger dataIndex = indexPath.row - 1; - OAPointTableViewCell* cell; - cell = (OAPointTableViewCell *)[self.favoriteTableView dequeueReusableCellWithIdentifier:[OAPointTableViewCell getCellIdentifier]]; - if (cell == nil) - { - NSArray *nib = [[NSBundle mainBundle] loadNibNamed:[OAPointTableViewCell getCellIdentifier] owner:self options:nil]; - cell = (OAPointTableViewCell *)[nib objectAtIndex:0]; - cell.directionImageView.image = [UIImage templateImageNamed:@"ic_small_direction"]; - } - if (cell) - { - if (dataIndex >= groupData.favoriteGroup.points.count) - { - // TODO: Hot fix to avoid crash. Should be fixed properly! - [self generateData]; - return cell; - } - OAFavoriteItem* item = [groupData.favoriteGroup.points objectAtIndex:dataIndex]; - [cell.titleView setText:[item getDisplayName]]; - cell = [self setupPoiIconForCell:cell withFavoriteItem:item]; - - [cell.distanceView setText:item.distance]; - - cell.directionImageView.tintColor = UIColorFromRGB(color_elevation_chart); - cell.directionImageView.transform = CGAffineTransformMakeRotation(item.direction); - } - return cell; -} - -- (OAPointTableViewCell *) setupPoiIconForCell:(OAPointTableViewCell *)cell withFavoriteItem:(OAFavoriteItem*)item -{ - cell.titleIcon.image = [item getCompositeIcon]; - return cell; -} - -- (UITableViewCell*)getActionCellForRowAtIndexPath:(NSIndexPath *)indexPath -{ - NSDictionary *item = _data[indexPath.section][indexPath.row]; - OASimpleTableViewCell* cell; - cell = (OASimpleTableViewCell *)[self.favoriteTableView dequeueReusableCellWithIdentifier:[OASimpleTableViewCell getCellIdentifier]]; - if (cell == nil) - { - NSArray *nib = [[NSBundle mainBundle] loadNibNamed:[OASimpleTableViewCell getCellIdentifier] owner:self options:nil]; - cell = (OASimpleTableViewCell *)[nib objectAtIndex:0]; - [cell descriptionVisibility:NO]; - } - - if (cell) - { - [cell.titleLabel setText:[item objectForKey:@"text"]]; - [cell.leftIconView setImage:[UIImage templateImageNamed:[item objectForKey:@"icon"]]]; - cell.leftIconView.tintColor = [UIColor colorNamed:ACColorNameIconColorSelected]; - } - return cell; -} - -- (NSIndexPath *) tableView:(UITableView *)tableView willSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - if ([self.favoriteTableView isEditing]) - { - if (!_isSearchActive) - { - NSDictionary *item = _data[indexPath.section][0]; - NSString *cellType = item[@"type"]; - if ([cellType isEqualToString:@"group"]) - return indexPath; - return nil; - } - else if (indexPath.section > 0) - { - return nil; - } - } - return indexPath; -} - -- (BOOL) tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath -{ - return _isSearchActive ? [self canEditSortedRowAtIndexPath:indexPath] : [self canEditUnsortedRowAtIndexPath:indexPath]; -} - -- (BOOL) canEditSortedRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (indexPath.section == 0) - return YES; - else - return NO; -} - --(BOOL)canEditUnsortedRowAtIndexPath:(NSIndexPath *)indexPath { - NSDictionary *item = _data[indexPath.section][0]; - NSString *cellType = item[@"type"]; - return [cellType isEqualToString:@"group"]; -} - -- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath -{ - if ((indexPath.row != 0 && !_isSearchActive) || _isSearchActive) - return UITableViewCellEditingStyleDelete; - else - return UITableViewCellEditingStyleNone; -} - -- (void)removeItemFromSortedFavoriteItems:(NSIndexPath *)indexPath -{ - OAFavoriteItem *item = _isFiltered ? _filteredItems[indexPath.row] : self.sortedFavoriteItems[indexPath.row]; - if (item) - { - NSInteger itemIndex = _isFiltered ? [_filteredItems indexOfObject:item] : [self.sortedFavoriteItems indexOfObject:item]; - if (itemIndex != NSNotFound) - { - [self.favoriteTableView beginUpdates]; - if (!_isFiltered) - { - [OAFavoritesHelper deleteFavoriteGroups:nil andFavoritesItems:@[self.sortedFavoriteItems[itemIndex]]]; - [self.sortedFavoriteItems removeObjectAtIndex:itemIndex]; - } - else - { - [OAFavoritesHelper deleteFavoriteGroups:nil andFavoritesItems:@[_filteredItems[itemIndex]]]; - [_filteredItems removeObjectAtIndex:itemIndex]; - } - - [self.favoriteTableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft]; - [self.favoriteTableView endUpdates]; - } - } -} - -- (void)removeItemFromUnsortedFavoriteItems:(NSIndexPath *)indexPath -{ - NSInteger dataIndex = indexPath.row - 1; - NSDictionary *groupData = _data[indexPath.section][0]; - FavoriteTableGroup *group = groupData[@"group"]; - OAFavoriteItem *item = group.favoriteGroup.points[dataIndex]; - if (item) - { - NSInteger itemIndex = [group.favoriteGroup.points indexOfObject:item]; - if (itemIndex != NSNotFound) - { - NSMutableArray *indexPaths = [NSMutableArray array]; - [self.favoriteTableView beginUpdates]; - if (group.favoriteGroup.points.count == 1) - { - NSMutableArray *unsortedHeaderViews = [_unsortedHeaderViews mutableCopy]; - [unsortedHeaderViews removeObject:_unsortedHeaderViews[indexPath.section]]; - _unsortedHeaderViews = unsortedHeaderViews; - - for (NSInteger i = indexPath.section; i < _unsortedHeaderViews.count - 1; i++) - { - ((OAMultiselectableHeaderView *) _unsortedHeaderViews[i]).section--; - [indexPaths addObject:[NSIndexPath indexPathForRow:0 inSection:i]]; - } - } - [OAFavoritesHelper deleteFavoriteGroups:nil andFavoritesItems:@[group.favoriteGroup.points[itemIndex]]]; - - NSInteger sortedItemIndex = _isFiltered ? [_filteredItems indexOfObject:item] : [self.sortedFavoriteItems indexOfObject:item]; - if (sortedItemIndex != NSNotFound) - _isFiltered ? [_filteredItems removeObjectAtIndex:sortedItemIndex] : [self.sortedFavoriteItems removeObjectAtIndex:sortedItemIndex]; - - if (group.favoriteGroup.points.count == 0) - { - [_data removeObjectAtIndex:indexPath.section]; - [self.favoriteTableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationFade]; - } - else - { - [self.favoriteTableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft]; - } - [self.favoriteTableView endUpdates]; - - if (indexPaths.count > 0) - [self.favoriteTableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - } - } -} - -- (void)removeItemsFromSortedFavoriteItems -{ - NSSortDescriptor *rowDescriptor = [[NSSortDescriptor alloc] initWithKey:@"row" ascending:NO]; - NSSortDescriptor *sectionDescriptor = [[NSSortDescriptor alloc] initWithKey:@"section" ascending:NO]; - NSArray *sortedArray = [_selectedItems sortedArrayUsingDescriptors:@[sectionDescriptor, rowDescriptor]]; - - for (NSIndexPath *selectedItem in sortedArray) - { - OAFavoriteItem* item = _isFiltered ? _filteredItems[selectedItem.row] : self.sortedFavoriteItems[selectedItem.row]; - if (item) - { - NSInteger itemIndex = _isFiltered ? [_filteredItems indexOfObject:item] : [self.sortedFavoriteItems indexOfObject:item]; - if (itemIndex != NSNotFound) - { - [self.favoriteTableView beginUpdates]; - if (!_isFiltered) - { - [OAFavoritesHelper deleteFavoriteGroups:nil andFavoritesItems:@[self.sortedFavoriteItems[itemIndex]]]; - [self.sortedFavoriteItems removeObjectAtIndex:itemIndex]; - } - else - { - [OAFavoritesHelper deleteFavoriteGroups:nil andFavoritesItems:@[_filteredItems[itemIndex]]]; - [_filteredItems removeObjectAtIndex:itemIndex]; - } - [self.favoriteTableView deleteRowsAtIndexPaths:@[selectedItem] withRowAnimation:UITableViewRowAnimationLeft]; - [self.favoriteTableView endUpdates]; - } - } - } - [self finishEditing]; -} - -- (void)removeItemsFromUnsortedFavoriteItems -{ - NSSortDescriptor *rowDescriptor = [[NSSortDescriptor alloc] initWithKey:@"row" ascending:NO]; - NSSortDescriptor *sectionDescriptor = [[NSSortDescriptor alloc] initWithKey:@"section" ascending:NO]; - NSArray *sortedArray = [_selectedItems sortedArrayUsingDescriptors:@[sectionDescriptor, rowDescriptor]]; - - for (NSIndexPath *selectedItem in sortedArray) - { - if (selectedItem.row == 0) - { - [self removeGroupHeader:selectedItem]; - } - else - { - NSDictionary *groupData = _data[selectedItem.section][0]; - FavoriteTableGroup *group = groupData[@"group"]; - NSInteger index = selectedItem.row - 1; - OAFavoriteItem* item = group.favoriteGroup.points[index]; - if (item && group.isOpen) - { - [self.favoriteTableView beginUpdates]; - [OAFavoritesHelper deleteFavoriteGroups:nil andFavoritesItems:@[item]]; - NSInteger sortedItemIndex = _isFiltered ? [_filteredItems indexOfObject:item] : [self.sortedFavoriteItems indexOfObject:item]; - if (sortedItemIndex != NSNotFound) - _isFiltered ? [_filteredItems removeObjectAtIndex:sortedItemIndex] : [self.sortedFavoriteItems removeObjectAtIndex:sortedItemIndex]; - [self.favoriteTableView deleteRowsAtIndexPaths:@[selectedItem] withRowAnimation:UITableViewRowAnimationLeft]; - [self.favoriteTableView endUpdates]; - } - } - } - [self finishEditing]; -} - -- (void)removeGroupHeader:(NSIndexPath *)indexPath -{ - NSInteger numberOfRows = [self.favoriteTableView numberOfRowsInSection:[indexPath section]]; - - if (numberOfRows == 1) - { - [self.favoriteTableView beginUpdates]; - - NSDictionary *groupData = _data[indexPath.section][0]; - FavoriteTableGroup* group = groupData[@"group"]; - [OAFavoritesHelper deleteFavoriteGroups:@[group.favoriteGroup] andFavoritesItems:nil]; - - [_data removeObjectAtIndex:indexPath.section]; - - NSMutableArray *indexPaths = [NSMutableArray array]; - NSMutableArray *unsortedHeaderViews = [_unsortedHeaderViews mutableCopy]; - [unsortedHeaderViews removeObject:_unsortedHeaderViews[indexPath.section]]; - _unsortedHeaderViews = unsortedHeaderViews; - - for (NSInteger i = indexPath.section; i < _unsortedHeaderViews.count - 1; i++) - { - ((OAMultiselectableHeaderView *) _unsortedHeaderViews[i]).section--; - [indexPaths addObject:[NSIndexPath indexPathForRow:0 inSection:i]]; - } - - [self.favoriteTableView deleteSections:[NSIndexSet indexSetWithIndex:indexPath.section] - withRowAnimation:UITableViewRowAnimationFade]; - [self.favoriteTableView endUpdates]; - if (indexPaths.count > 0) - [self.favoriteTableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone]; - } -} - -- (void) removeFavoriteItems -{ - if (!_isSearchActive) - [self removeItemsFromUnsortedFavoriteItems]; - else - [self removeItemsFromSortedFavoriteItems]; -} - -- (void)removeFavoriteItem:(NSIndexPath *)indexPath -{ - if (!_isSearchActive) - [self removeItemFromUnsortedFavoriteItems:indexPath]; - else - [self removeItemFromSortedFavoriteItems:indexPath]; -} - -- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (editingStyle == UITableViewCellEditingStyleDelete) - { - UIAlertController *alert = [UIAlertController - alertControllerWithTitle:nil - message:OALocalizedString(@"fav_remove_q") - preferredStyle:UIAlertControllerStyleAlert]; - - UIAlertAction *yesButton = [UIAlertAction - actionWithTitle:OALocalizedString(@"shared_string_yes") - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * _Nonnull action) { - [self removeFavoriteItem:indexPath]; - }]; - UIAlertAction *cancelButton = [UIAlertAction - actionWithTitle:OALocalizedString(@"shared_string_no") - style:UIAlertActionStyleCancel - handler:nil]; - [alert addAction:yesButton]; - [alert addAction:cancelButton]; - [self presentViewController:alert animated:YES completion:nil]; - } -} - -#pragma mark - -#pragma mark Deferred image loading (UIScrollViewDelegate) - -- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView -{ - _decelerating = YES; -} - -// Load images for all onscreen rows when scrolling is finished -- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate -{ - if (!decelerate) - { - _decelerating = NO; - //[self refreshVisibleRows]; - } -} - -- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView -{ - _decelerating = NO; - //[self refreshVisibleRows]; -} - -#pragma mark - Favorite group's item editing operations - -- (void) addIndexPathToSelectedCellsArray:(NSIndexPath *)indexPath -{ - if (![_selectedItems containsObject:indexPath]) - [_selectedItems addObject:indexPath]; -} - -- (void) removeIndexPathFromSelectedCellsArray:(NSIndexPath *)indexPath -{ - if ([_selectedItems containsObject:indexPath]) - [_selectedItems removeObject:indexPath]; -} - -- (void)openCloseGroupButtonAction:(id)sender -{ - UIButton *button = (UIButton *)sender; - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:button.tag & 0x3FF inSection:button.tag >> 10]; - - [self openCloseFavoriteGroup:indexPath]; -} - -- (void) selectAllItemsInGroup:(NSIndexPath *)indexPath selectHeader:(BOOL)selectHeader -{ - NSInteger rowsCount = [self.favoriteTableView numberOfRowsInSection:indexPath.section]; - - [self.favoriteTableView beginUpdates]; - if (selectHeader) - for (int i = 0; i < rowsCount; i++) - { - [self.favoriteTableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:indexPath.section] animated:YES scrollPosition:UITableViewScrollPositionNone]; - [self addIndexPathToSelectedCellsArray:[NSIndexPath indexPathForRow:i inSection:indexPath.section]]; - } - else - for (int i = 0; i < rowsCount; i++) - { - [self removeIndexPathFromSelectedCellsArray:[NSIndexPath indexPathForRow:i inSection:indexPath.section]]; - [self.favoriteTableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:indexPath.section] animated:YES]; - } - [self.favoriteTableView endUpdates]; -} - -- (void) selectGroupForEditing:(NSIndexPath *)indexPath -{ - NSDictionary *item = _data[indexPath.section][0]; - FavoriteTableGroup* groupData = item[@"group"]; - if (groupData.isOpen) - [self selectAllItemsInGroup:indexPath selectHeader:YES]; - else - for (NSInteger i = 0; i <= groupData.favoriteGroup.points.count; i++) - [self addIndexPathToSelectedCellsArray:[NSIndexPath indexPathForRow:i inSection:indexPath.section]]; -} - -- (void) deselectGroupForEditing:(NSIndexPath *)indexPath -{ - BOOL isGroupHeaderSelected = [self.favoriteTableView.indexPathsForSelectedRows containsObject:[NSIndexPath indexPathForRow:0 inSection:indexPath.section]]; - NSDictionary *item = _data[indexPath.section][0]; - FavoriteTableGroup* groupData = item[@"group"]; - - if (groupData.isOpen) - { - NSArray *selectedRows = [self.favoriteTableView indexPathsForSelectedRows]; - NSInteger rowsCount = [self.favoriteTableView numberOfRowsInSection:indexPath.section]; - [self selectAllItemsInGroup:indexPath selectHeader:(rowsCount != selectedRows.count && isGroupHeaderSelected)]; - } - else - { - NSMutableArray *tmp = [[NSMutableArray alloc] initWithArray:_selectedItems]; - for (NSUInteger i = 0; i < tmp.count; i++) - [self removeIndexPathFromSelectedCellsArray:[NSIndexPath indexPathForRow:i inSection:indexPath.section]]; - [self.favoriteTableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:indexPath.section] animated:YES]; - } -} - -- (void) selectPreselectedCells:(NSIndexPath *)indexPath -{ - for (NSIndexPath *itemPath in _selectedItems) - if (itemPath.section == indexPath.section) - [self.favoriteTableView selectRowAtIndexPath:itemPath animated:YES scrollPosition:UITableViewScrollPositionNone]; -} - -- (void) openCloseFavoriteGroup:(NSIndexPath *)indexPath -{ - NSDictionary *item = _data[indexPath.section][0]; - FavoriteTableGroup* groupData = item[@"group"]; - if (groupData.isOpen) - { - groupData.isOpen = NO; - [self.favoriteTableView beginUpdates]; - [self.favoriteTableView reloadSections:[[NSIndexSet alloc] initWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationNone]; - [self.favoriteTableView endUpdates]; - if ([_selectedItems containsObject: [NSIndexPath indexPathForRow:0 inSection:indexPath.section]]) - [self.favoriteTableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:indexPath.section] animated:YES scrollPosition:UITableViewScrollPositionNone]; - } - else - { - groupData.isOpen = YES; - [self.favoriteTableView beginUpdates]; - [self.favoriteTableView reloadSections:[[NSIndexSet alloc] initWithIndex:indexPath.section] withRowAnimation:UITableViewRowAnimationNone]; - [self.favoriteTableView endUpdates]; - - [self selectPreselectedCells:indexPath]; - } -} - -#pragma mark - UITableViewDelegate - -- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (_isSearchActive) - { - [self didSelectRowAtIndexPathSorter:indexPath]; - } - else - { - NSDictionary *item = _data[indexPath.section][0]; - NSString *cellType = item[@"type"]; - if ([cellType isEqualToString:@"group"]) - { - if (indexPath.row == 0 && ![self.favoriteTableView isEditing]) - [self openCloseFavoriteGroup:indexPath]; - else if (indexPath.row == 0 && [self.favoriteTableView isEditing]) - [self selectGroupForEditing:indexPath]; - else - [self didSelectRowAtIndexPathUnsorted:indexPath]; - } - else - { - [self didSelectRowAtIndexPathUnsorted:indexPath]; - } - } -} - -- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath -{ - if (_isSearchActive) - { - [self didDeselectRowAtIndexPathSorted:indexPath]; - } - else - { - NSDictionary *item = _data[indexPath.section][0]; - NSString *cellType = item[@"type"]; - if ([cellType isEqualToString:@"group"]) - { - if (indexPath.row == 0 && ![self.favoriteTableView isEditing]) - [self openCloseFavoriteGroup:indexPath]; - else if (indexPath.row == 0 && [self.favoriteTableView isEditing]) - [self deselectGroupForEditing:indexPath]; - else - [self didDeselectRowAtIndexPathUnsorted:indexPath]; - } - } -} - -- (void) didSelectRowAtIndexPathSorter:(NSIndexPath *)indexPath -{ - if ([self.favoriteTableView isEditing]) - { - [self addIndexPathToSelectedCellsArray:indexPath]; - return; - } - - if (indexPath.section == 0) - { - OAFavoriteItem* item = _isFiltered ? [_filteredItems objectAtIndex:indexPath.row] : [self.sortedFavoriteItems objectAtIndex:indexPath.row]; - [self doPush]; - [[OARootViewController instance].mapPanel openTargetViewWithFavorite:item pushed:YES]; - - } - else - { - NSDictionary* item = [_data.lastObject objectAtIndex:indexPath.row]; - SEL action = NSSelectorFromString([item objectForKey:@"action"]); - [self performSelector:action]; - [self removeIndexPathFromSelectedCellsArray:indexPath]; - [self.favoriteTableView deselectRowAtIndexPath:indexPath animated:YES]; - } -} - -- (void) didDeselectRowAtIndexPathSorted:(NSIndexPath *)indexPath -{ - if ([self.favoriteTableView isEditing]) - { - [self removeIndexPathFromSelectedCellsArray:indexPath]; - return; - } -} - -- (void) didDeselectRowAtIndexPathUnsorted:(NSIndexPath *)indexPath -{ - if ([self.favoriteTableView isEditing]) - { - BOOL isGroupHeaderSelected = [self.favoriteTableView.indexPathsForSelectedRows containsObject:[NSIndexPath indexPathForRow:0 inSection:indexPath.section]]; - NSArray *selectedRows = [self.favoriteTableView indexPathsForSelectedRows]; - NSInteger numberOfRowsInSection = [self.favoriteTableView numberOfRowsInSection:indexPath.section] - 1; - NSInteger numberOfSelectedRowsInSection = 0; - for (NSIndexPath *item in selectedRows) - { - if(item.section == indexPath.section) - numberOfSelectedRowsInSection++; - } - [self removeIndexPathFromSelectedCellsArray:indexPath]; - - if (indexPath.row == 0) - { - [self removeIndexPathFromSelectedCellsArray:[NSIndexPath indexPathForRow:0 inSection:indexPath.section]]; - [self.favoriteTableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:indexPath.section] animated:YES]; - } - else if (numberOfSelectedRowsInSection == numberOfRowsInSection && isGroupHeaderSelected) - { - [self removeIndexPathFromSelectedCellsArray:[NSIndexPath indexPathForRow:0 inSection:indexPath.section]]; - [self.favoriteTableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:indexPath.section] animated:YES]; - } - return; - } -} - -- (void) didSelectRowAtIndexPathUnsorted:(NSIndexPath *)indexPath -{ - if ([self.favoriteTableView isEditing]) - { - BOOL isGroupHeaderSelected = [self.favoriteTableView.indexPathsForSelectedRows containsObject:[NSIndexPath indexPathForRow:0 inSection:indexPath.section]]; - NSArray *selectedRows = [self.favoriteTableView indexPathsForSelectedRows]; - NSInteger numberOfRowsInSection = [self.favoriteTableView numberOfRowsInSection:indexPath.section] - 1; - NSInteger numberOfSelectedRowsInSection = 0; - for (NSIndexPath *item in selectedRows) - { - if(item.section == indexPath.section) - numberOfSelectedRowsInSection++; - [self addIndexPathToSelectedCellsArray:item]; - } - if (numberOfSelectedRowsInSection == numberOfRowsInSection && !isGroupHeaderSelected) - { - [self.favoriteTableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:indexPath.section] animated:YES scrollPosition:UITableViewScrollPositionNone]; - [self addIndexPathToSelectedCellsArray:[NSIndexPath indexPathForRow:0 inSection:indexPath.section]]; - } - else - { - [self removeIndexPathFromSelectedCellsArray:[NSIndexPath indexPathForRow:0 inSection:indexPath.section]]; - [self.favoriteTableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:indexPath.section] animated:YES]; - } - return; - } - NSDictionary *item = _data[indexPath.section][0]; - NSString *cellType = item[@"type"]; - if ([cellType isEqualToString:@"group"]) - { - FavoriteTableGroup* groupData = item[@"group"]; - OAFavoriteItem* item = [groupData.favoriteGroup.points objectAtIndex:indexPath.row - 1]; - [self doPush]; - [[OARootViewController instance].mapPanel openTargetViewWithFavorite:item pushed:YES]; - - } - else - { - item = _data[indexPath.section][indexPath.row]; - SEL action = NSSelectorFromString([item objectForKey:@"action"]); - [self performSelector:action]; - [self removeIndexPathFromSelectedCellsArray:indexPath]; - [self.favoriteTableView deselectRowAtIndexPath:indexPath animated:YES]; - } -} - -- (void)doPush -{ - parentController = self.parentViewController; - - CATransition* transition = [CATransition animation]; - transition.duration = 0.4; - transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; - transition.type = kCATransitionPush; - transition.subtype = kCATransitionFromRight; - [[OARootViewController instance].navigationController.view.layer addAnimation:transition forKey:nil]; - [[OARootViewController instance].navigationController popToRootViewControllerAnimated:NO]; -} - -+ (void)doPop -{ - CATransition* transition = [CATransition animation]; - transition.duration = 0.4; - transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; - transition.type = kCATransitionReveal; - transition.subtype = kCATransitionFromLeft; - [[OARootViewController instance].navigationController.view.layer addAnimation:transition forKey:nil]; - [[OARootViewController instance].navigationController pushViewController:parentController animated:NO]; - - parentController = nil; -} - -#pragma mark - OAMultiselectableHeaderDelegate - --(void)headerCheckboxChanged:(id)sender value:(BOOL)value -{ - OAMultiselectableHeaderView *headerView = (OAMultiselectableHeaderView *)sender; - NSInteger section = headerView.section; - NSInteger rowsCount = [self.favoriteTableView numberOfRowsInSection:section]; - - [self.favoriteTableView beginUpdates]; - if (value) - { - for (NSInteger i = 0; i < rowsCount; i++) - [self.favoriteTableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:section] animated:YES scrollPosition:UITableViewScrollPositionNone]; - } - else - { - for (NSInteger i = 0; i < rowsCount; i++) - [self.favoriteTableView deselectRowAtIndexPath:[NSIndexPath indexPathForRow:i inSection:section] animated:YES]; - } - [self.favoriteTableView endUpdates]; -} - -#pragma mark - OAEditorDelegate - -- (void)addNewItemWithName:(NSString *)name - iconName:(NSString *)iconName - color:(UIColor *)color - backgroundIconName:(NSString *)backgroundIconName -{ -} - -- (void)onEditorUpdated; -{ - [self generateData]; -} - -- (void)selectColorItem:(OASPaletteItemSolid *)colorItem -{ -} - -- (OASPaletteItemSolid *)addAndGetNewColorItem:(UIColor *)color -{ - return [_appearanceCollection addNewSelectedColor:color]; -} - -- (void)changeColorItem:(OASPaletteItemSolid *)colorItem withColor:(UIColor *)color -{ - [_appearanceCollection changeColor:colorItem newColor:color]; -} - -- (OASPaletteItemSolid *)duplicateColorItem:(OASPaletteItemSolid *)colorItem -{ - return [_appearanceCollection duplicateColor:colorItem]; -} - -- (void)deleteColorItem:(OASPaletteItemSolid *)colorItem -{ - [_appearanceCollection deleteColor:colorItem]; -} - -#pragma mark - UIDocumentPickerDelegate - -- (void)documentPicker:(UIDocumentPickerViewController *)controller didPickDocumentsAtURLs:(NSArray *)urls -{ - if (urls.count == 0) - return; - - NSURL *url = urls.firstObject; - [OARootViewController.instance importAsFavorites:url]; -} - -#pragma mark - Keyboard Notifications - -- (void) keyboardWillShow:(NSNotification *)notification; -{ - NSDictionary *userInfo = [notification userInfo]; - CGRect keyboardBounds; - [[userInfo valueForKey:UIKeyboardFrameEndUserInfoKey] getValue: &keyboardBounds]; - CGFloat duration = [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]; - NSInteger animationCurve = [[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue]; - [UIView animateWithDuration:duration delay:0. options:animationCurve animations:^{ - UIEdgeInsets insets = [self.favoriteTableView contentInset]; - [self.favoriteTableView setContentInset:UIEdgeInsetsMake(insets.top, insets.left, keyboardBounds.size.height, insets.right)]; - [self.favoriteTableView setScrollIndicatorInsets:self.favoriteTableView.contentInset]; - } completion:nil]; -} - -- (void) keyboardWillHide:(NSNotification *)notification; -{ - NSDictionary *userInfo = [notification userInfo]; - CGFloat duration = [[userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] floatValue]; - NSInteger animationCurve = [[userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue]; - [UIView animateWithDuration:duration delay:0. options:animationCurve animations:^{ - UIEdgeInsets insets = [self.favoriteTableView contentInset]; - [self.favoriteTableView setContentInset:UIEdgeInsetsMake(insets.top, insets.left, 0.0, insets.right)]; - [self.favoriteTableView setScrollIndicatorInsets:self.favoriteTableView.contentInset]; - } completion:nil]; -} - -// MARK: - MyPlacesSearchable -- (void)searchResultsFor:(UISearchController *)searchController -{ - if (searchController.isActive && searchController.searchBar.searchTextField.text.length == 0) - { - _isSearchActive = YES; - _isFiltered = NO; - [self generateData]; - [self.favoriteTableView reloadData]; - } - else if (searchController.isActive && searchController.searchBar.searchTextField.text.length > 0) - { - _isFiltered = YES; - - NSString *searchText = searchController.searchBar.searchTextField.text; - NSLocale *currentLocale = [NSLocale currentLocale]; - - _filteredItems = [NSMutableArray new]; - for (OAFavoriteItem *item in self.sortedFavoriteItems) - { - NSString *displayName = [item getDisplayName]; - - if (displayName.length > 0) - { - NSRange range = [displayName rangeOfString:searchText - options:searchOptions - range:NSMakeRange(0, displayName.length) - locale:currentLocale]; - - if (range.location != NSNotFound) - [_filteredItems addObject:item]; - } - } - [self.favoriteTableView reloadData]; - } - else - { - _isSearchActive = NO; - _isFiltered = NO; - [self.favoriteTableView reloadData]; - } - [_myPlacesDelegate updateSegmentedControlVisibility:!_isSearchActive]; -} - -@end diff --git a/Sources/Controllers/Settings/ImportExport/OAImportCompleteViewController.mm b/Sources/Controllers/Settings/ImportExport/OAImportCompleteViewController.mm index ed3bfec2fe..80f004fbc1 100644 --- a/Sources/Controllers/Settings/ImportExport/OAImportCompleteViewController.mm +++ b/Sources/Controllers/Settings/ImportExport/OAImportCompleteViewController.mm @@ -443,8 +443,8 @@ - (void)onRowSelected:(NSIndexPath *)indexPath } else if (dataType == EOAImportDataTypeGpxTrips) { - UITabBarController* myPlacesViewController = [[UIStoryboard storyboardWithName:@"MyPlaces" bundle:nil] instantiateInitialViewController]; - [myPlacesViewController setSelectedIndex:1]; + MyPlacesContainerViewController *myPlacesViewController = [[MyPlacesContainerViewController alloc] init]; + [myPlacesViewController setSelectedTab:TabTracks]; [rootController.navigationController pushViewController:myPlacesViewController animated:YES]; } else if (dataType == EOAImportDataTypeAvoidRoads) @@ -460,16 +460,17 @@ - (void)onRowSelected:(NSIndexPath *)indexPath } else if (dataType == EOAImportDataTypeFavorites) { - UIViewController* favoritesViewController = [[UIStoryboard storyboardWithName:@"MyPlaces" bundle:nil] instantiateInitialViewController]; - [rootController.navigationController pushViewController:favoritesViewController animated:YES]; + MyPlacesContainerViewController *myPlacesViewController = [[MyPlacesContainerViewController alloc] init]; + [myPlacesViewController setSelectedTab:TabFavorites]; + [rootController.navigationController pushViewController:myPlacesViewController animated:YES]; } else if (dataType == EOAImportDataTypeOsmNotes || dataType == EOAImportDataTypeOsmEdits) { BOOL isOsmEditingEnabled = [[OAIAPHelper sharedInstance].osmEditing isActive]; if (isOsmEditingEnabled) { - UITabBarController* myPlacesViewController = [[UIStoryboard storyboardWithName:@"MyPlaces" bundle:nil] instantiateInitialViewController]; - [myPlacesViewController setSelectedIndex:2]; + MyPlacesContainerViewController *myPlacesViewController = [[MyPlacesContainerViewController alloc] init]; + [myPlacesViewController setSelectedTab:TabOsm]; [rootController.navigationController pushViewController:myPlacesViewController animated:YES]; } else diff --git a/Sources/Controllers/Settings/OATripRecordingSettingsViewController.mm b/Sources/Controllers/Settings/OATripRecordingSettingsViewController.mm index e7b8da4ddc..b840ae6e7c 100644 --- a/Sources/Controllers/Settings/OATripRecordingSettingsViewController.mm +++ b/Sources/Controllers/Settings/OATripRecordingSettingsViewController.mm @@ -769,13 +769,8 @@ - (void) selectGeneral:(NSDictionary *)item } else if ([@"open_trips" isEqualToString:name]) { - UITabBarController* myPlacesViewController = [[UIStoryboard storyboardWithName:@"MyPlaces" bundle:nil] instantiateInitialViewController]; - [myPlacesViewController setSelectedIndex:1]; - - TracksViewController *gpxController = myPlacesViewController.viewControllers[1]; - if (gpxController == nil) - return; - + MyPlacesContainerViewController *myPlacesViewController = [[MyPlacesContainerViewController alloc] init]; + [myPlacesViewController setSelectedTab:TabTracks]; [self.navigationController pushViewController:myPlacesViewController animated:YES]; } else if ([@"reset_plugin" isEqualToString:name]) diff --git a/Sources/Controllers/TargetMenu/Editors/OAEditGroupViewController.h b/Sources/Controllers/TargetMenu/Editors/OAEditGroupViewController.h index 36097b91ff..4a71396234 100644 --- a/Sources/Controllers/TargetMenu/Editors/OAEditGroupViewController.h +++ b/Sources/Controllers/TargetMenu/Editors/OAEditGroupViewController.h @@ -22,6 +22,6 @@ @property (nonatomic, weak) id delegate; --(instancetype)initWithGroupName:(NSString *)groupName groups:(NSArray *)groups; +-(instancetype)initWithGroupName:(nullable NSString *)groupName groups:(NSArray *)groups; @end diff --git a/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm b/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm index df62feab1a..c1b9437cb2 100644 --- a/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm +++ b/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm @@ -159,7 +159,6 @@ - (void)onRightNavbarButtonPressed { if (_gpxFile && _gpxFile.pointsGroups.count > 0) { - // IOS-214 if (![self isFavoritesValid]) return; diff --git a/Sources/Controllers/TargetMenu/OACollapsableWaypointsView.mm b/Sources/Controllers/TargetMenu/OACollapsableWaypointsView.mm index 129770db5e..0e161a6d74 100644 --- a/Sources/Controllers/TargetMenu/OACollapsableWaypointsView.mm +++ b/Sources/Controllers/TargetMenu/OACollapsableWaypointsView.mm @@ -178,8 +178,9 @@ - (void) onShowMorePressed:(id) sender } else if (_type == EOAWaypointFavorite) { - UIViewController* resourcesViewController = [[UIStoryboard storyboardWithName:@"MyPlaces" bundle:nil] instantiateInitialViewController]; - [[OARootViewController instance].navigationController pushViewController:resourcesViewController animated:YES]; + MyPlacesContainerViewController *myPlacesViewController = [[MyPlacesContainerViewController alloc] init]; + [myPlacesViewController setSelectedTab:TabFavorites]; + [[OARootViewController instance].navigationController pushViewController:myPlacesViewController animated:YES]; } } diff --git a/Sources/Controllers/TargetMenu/OAEditPointViewController.h b/Sources/Controllers/TargetMenu/OAEditPointViewController.h index 73cbc03247..70983ef8d1 100644 --- a/Sources/Controllers/TargetMenu/OAEditPointViewController.h +++ b/Sources/Controllers/TargetMenu/OAEditPointViewController.h @@ -28,6 +28,12 @@ typedef NS_ENUM(NSInteger, EOAEditPointType) { @end +@protocol OAEditPointViewControllerDelegate + +- (void)saveTapped; + +@end + @interface OAEditPointViewController : OABaseNavbarSubviewViewController @property (nonatomic, copy) NSString *name; @@ -38,6 +44,7 @@ typedef NS_ENUM(NSInteger, EOAEditPointType) { @property (nonatomic, copy) NSString *groupTitle; @property (nonatomic, weak) id gpxWptDelegate; +@property (nonatomic, weak) id delegate; - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite; - (instancetype)initWithGpxWpt:(OAGpxWptItem *)gpxWpt; diff --git a/Sources/Controllers/TargetMenu/OAEditPointViewController.mm b/Sources/Controllers/TargetMenu/OAEditPointViewController.mm index 93335f2eb2..f3a4aef119 100644 --- a/Sources/Controllers/TargetMenu/OAEditPointViewController.mm +++ b/Sources/Controllers/TargetMenu/OAEditPointViewController.mm @@ -1044,6 +1044,7 @@ - (void)onRightNavbarButtonPressed if (_editPointType == EOAEditPointTypeFavorite) [OAAppSettings.sharedManager.lastFavCategoryEntered set:savingGroup]; } + [self.delegate saveTapped]; [self dismissViewController]; } diff --git a/Sources/Helpers/Extensions/Extensions.swift b/Sources/Helpers/Extensions/Extensions.swift index c98445dd15..b88307c0b2 100644 --- a/Sources/Helpers/Extensions/Extensions.swift +++ b/Sources/Helpers/Extensions/Extensions.swift @@ -30,6 +30,20 @@ extension UIImage { } return self } + + func rotatedForAttributedString(with radians: CGFloat) -> UIImage { + let format = UIGraphicsImageRendererFormat() + format.scale = scale + format.opaque = false + let renderer = UIGraphicsImageRenderer(size: size, format: format) + return renderer.image { context in + let rect = CGRect(origin: CGPoint(x: -size.width / 2.0, y: -size.height / 2.0), + size: size) + context.cgContext.translateBy(x: size.width / 2.0, y: size.height / 2.0) + context.cgContext.rotate(by: radians) + draw(in: rect) + } + } func rotateWithDiagonalSize(radians: CGFloat) -> UIImage? { let diagonalSize = sqrt(size.width * size.width + size.height * size.height) diff --git a/Sources/Helpers/OAAppSettings.h b/Sources/Helpers/OAAppSettings.h index e08fdc4cb8..d5914e936d 100644 --- a/Sources/Helpers/OAAppSettings.h +++ b/Sources/Helpers/OAAppSettings.h @@ -1231,6 +1231,8 @@ typedef NS_ENUM(NSInteger, EOAWikiDataSourceType) @property (nonatomic) OACommonStringList *customWidgetKeys; @property (nonatomic) OACommonStringList *tracksSortModes; @property (nonatomic) OACommonString *searchTracksSortModes; +@property (nonatomic) OACommonStringList *favoriteSortModes; +@property (nonatomic) OACommonString *searchFavoriteSortMode; @property (nonatomic) OACommonString *travelGuidesSortMode; @property (nonatomic) OACommonString *osmEditsSortMode; @@ -1328,6 +1330,8 @@ typedef NS_ENUM(NSInteger, EOAWikiDataSourceType) - (void)saveTracksSortModes:(NSDictionary *)tabsSortModes; - (NSDictionary *)getTracksSortModes; +- (void)saveFavoriteSortModes:(NSDictionary *)favoriteSortModes; +- (NSDictionary *)getFavoriteSortModes; - (NSString *) getFormattedTrackInterval:(int)value; diff --git a/Sources/Helpers/OAAppSettings.m b/Sources/Helpers/OAAppSettings.m index 118a714ceb..7d034a1aed 100644 --- a/Sources/Helpers/OAAppSettings.m +++ b/Sources/Helpers/OAAppSettings.m @@ -261,6 +261,8 @@ + (void)postPreferenceNotificationWithObject:(id)object keys:(NSSet static NSString * const customWidgetKeys = @"custom_widgets_keys"; static NSString * const tracksSortModesKey = @"tracks_tabs_sort_modes"; static NSString * const searchTracksSortModesKey = @"search_tracks_sort_mode"; +static NSString * const favoriteSortModesKey = @"favorite_sort_modes"; +static NSString * const searchFavoriteSortModeKey = @"search_favorite_sort_mode"; static NSString * const travelGuidesSortModeKey = @"travel_guides_tabs_sort_mode"; static NSString * const osmEditsSortModeKey = @"osm_edits_tabs_sort_mode"; static NSString * const showSpeedometerKey = @"show_speedometer"; @@ -6180,6 +6182,12 @@ - (instancetype) init _searchTracksSortModes = [OACommonString withKey:searchTracksSortModesKey defValue:[TracksSortModeHelper getDefaultSortModeTitleFor:nil]]; [_globalPreferences setObject:_searchTracksSortModes forKey:searchTracksSortModesKey]; + _favoriteSortModes = [[[OACommonStringList withKey:favoriteSortModesKey defValue:@[]] makeGlobal] makeShared]; + [_globalPreferences setObject:_favoriteSortModes forKey:favoriteSortModesKey]; + + _searchFavoriteSortMode = [OACommonString withKey:searchFavoriteSortModeKey defValue:[FavoriteSortModeHelper defaultSortModeTitle]]; + [_globalPreferences setObject:_searchFavoriteSortMode forKey:searchFavoriteSortModeKey]; + _travelGuidesSortMode = [OACommonString withKey:travelGuidesSortModeKey defValue:[MyPlacesSortModeHelper defaultTravelGuidesSortModeTitle]]; [_globalPreferences setObject:_travelGuidesSortMode forKey:travelGuidesSortModeKey]; @@ -7667,6 +7675,11 @@ - (OACommonString *)getCustomRenderProperty:(NSString *)attrName defaultValue:(N return [self getTrackSortModesWithArray:[_tracksSortModes get]]; } +- (NSDictionary *)getFavoriteSortModes +{ + return [self getTrackSortModesWithArray:[_favoriteSortModes get]]; +} + - (NSDictionary *)getTrackSortModesWithArray:(NSArray *)modes { NSMutableDictionary *sortModes = [NSMutableDictionary dictionary]; @@ -7689,6 +7702,12 @@ - (void)saveTracksSortModes:(NSDictionary *)tabsSortMode [_tracksSortModes set:sortModes]; } +- (void)saveFavoriteSortModes:(NSDictionary *)favoriteSortModes +{ + NSArray *sortModes = [self getPlainSortModesFromDictionary:favoriteSortModes]; + [_favoriteSortModes set:sortModes]; +} + - (NSArray *)getPlainSortModesFromDictionary:(NSDictionary *)tabsSortModes { NSMutableArray *sortTypes = [NSMutableArray array]; diff --git a/Sources/Helpers/OAFavoritesHelper.h b/Sources/Helpers/OAFavoritesHelper.h index c49212ef3e..dd7eb0f3d9 100644 --- a/Sources/Helpers/OAFavoritesHelper.h +++ b/Sources/Helpers/OAFavoritesHelper.h @@ -79,6 +79,10 @@ visible:(BOOL)visible saveImmediately:(BOOL)saveImmediately; ++ (void)updateGroup:(OAFavoriteGroup *)group + pinned:(BOOL)pinned + saveImmediately:(BOOL)saveImmediately; + + (NSArray *)getFavoriteGroups; + (void) addFavoriteGroup:(NSString *)name @@ -123,6 +127,7 @@ @property (nonatomic) NSString* name; @property (nonatomic) UIColor* color; @property (nonatomic) BOOL isVisible; +@property (nonatomic) BOOL isPinned; @property (nonatomic) NSString *iconName; @property (nonatomic) NSString *backgroundType; @property (nonatomic) NSMutableArray *points; diff --git a/Sources/Helpers/OAFavoritesHelper.mm b/Sources/Helpers/OAFavoritesHelper.mm index ffc06f920f..335675a9c7 100644 --- a/Sources/Helpers/OAFavoritesHelper.mm +++ b/Sources/Helpers/OAFavoritesHelper.mm @@ -168,23 +168,72 @@ + (OASGpxFile *)loadGpxFile:(NSString *)file return [OASGpxUtilities.shared loadGpxFileFile:favoriteGPXFile]; } ++ (void)addParentGroupNamesForGroupName:(NSString *)groupName toSet:(NSMutableOrderedSet *)parentGroupNames +{ + if (groupName.length == 0 || ![groupName containsString:@"/"]) + return; + + NSString *parentGroupName = @""; + NSArray *components = [groupName componentsSeparatedByString:@"/"]; + for (NSUInteger i = 0; i + 1 < components.count; i++) + { + NSString *component = components[i]; + if (component.length == 0) + continue; + + parentGroupName = parentGroupName.length == 0 ? component : [parentGroupName stringByAppendingFormat:@"/%@", component]; + [parentGroupNames addObject:parentGroupName]; + } +} + ++ (void)createMissingParentGroupsForGpx:(OASGpxFile *)gpxFile +{ + NSMutableOrderedSet *parentGroupNames = [NSMutableOrderedSet orderedSet]; + for (OASGpxUtilitiesPointsGroup *pointsGroup in gpxFile.pointsGroups.allValues) + { + for (OASWptPt *point in pointsGroup.points) + { + [self addParentGroupNamesForGroupName:point.category toSet:parentGroupNames]; + } + } + + for (NSString *groupName in parentGroupNames) + { + if (groupName.length == 0 || _flatGroups[groupName]) + continue; + + OAFavoriteGroup *group = [[OAFavoriteGroup alloc] initWithName:groupName isVisible:YES color:nil]; + _flatGroups[group.name] = group; + _favoriteGroups = [_favoriteGroups arrayByAddingObject:group]; + } +} + + (void)importFavoritesFromGpx:(OASGpxFile *)gpxFile { NSString *defCategory = @""; OAParkingPositionPlugin *plugin = (OAParkingPositionPlugin *)[OAPluginsHelper getPlugin:OAParkingPositionPlugin.class]; + [self createMissingParentGroupsForGpx:gpxFile]; NSArray *pointsGroups = gpxFile.pointsGroups.allValues; + BOOL favoritesImported = NO; for (OASGpxUtilitiesPointsGroup *pointsGroup in pointsGroups) { NSArray *favorites = [self wptAsFavorites:pointsGroup.points defaultCategory:defCategory]; [self checkDuplicateNames:favorites]; [self deleteFavorites:favorites.copy saveImmediately:NO]; - [self addFavorites:favorites lookupAddress:YES sortAndSave:pointsGroup == pointsGroups.lastObject pointsGroup:pointsGroup]; + if ([self addFavorites:favorites lookupAddress:YES sortAndSave:NO pointsGroup:pointsGroup]) + favoritesImported = YES; for (OAFavoriteItem *favorite in favorites) { if (plugin && favorite.specialPointType == OASpecialPointType.PARKING) [plugin updateParkingPoint:favorite]; } } + + if (favoritesImported) + { + [self sortAll]; + [self saveCurrentPointsIntoFile]; + } } + (void)checkDuplicateNames:(NSArray *)favorites @@ -582,6 +631,15 @@ + (void)updateGroup:(OAFavoriteGroup *)group [self saveCurrentPointsIntoFile]; } ++ (void)updateGroup:(OAFavoriteGroup *)group + pinned:(BOOL)pinned + saveImmediately:(BOOL)saveImmediately +{ + group.isPinned = pinned; + if (saveImmediately) + [self saveCurrentPointsIntoFile]; +} + + (void) saveCurrentPointsIntoFile { [self saveCurrentPointsIntoFile:YES]; @@ -913,6 +971,7 @@ + (void)updateGroupAppearance:(OAFavoriteGroup *)favoriteGroup favoriteGroup.iconName = pointsGroup.iconName; favoriteGroup.backgroundType = pointsGroup.backgroundType; favoriteGroup.isVisible = ![pointsGroup isHidden]; + favoriteGroup.isPinned = pointsGroup.isPinned.boolValue; } } @@ -1008,7 +1067,10 @@ + (BOOL)deleteFavoriteGroups:(NSArray *)groupsToDelete } if (!isNewFavorite) + { + [self recalculateCachedFavPoints]; [self saveCurrentPointsIntoFile]; + } return YES; } @@ -1218,6 +1280,14 @@ + (BOOL) hasFavoriteAt:(CLLocationCoordinate2D)location @end +@interface OAFavoriteGroup () + +- (void)updateMissingAppearanceFromPoint:(OAFavoriteItem *)point; +- (void)applyMissingAppearanceToPoint:(OAFavoriteItem *)point; ++ (BOOL)isBaseFavoriteOrPersonalGroup:(NSString *)name; + +@end + @implementation OAFavoriteGroup - (instancetype)init @@ -1244,6 +1314,7 @@ - (BOOL)isEqual:(id)object { return ([self.name isEqualToString:otherGroup.name] && [self.color isEqual:otherGroup.color] && self.isVisible == otherGroup.isVisible && + self.isPinned == otherGroup.isPinned && [self.iconName isEqualToString:otherGroup.iconName] && [self.backgroundType isEqualToString:otherGroup.backgroundType] && [self.points isEqualToArray:otherGroup.points]); @@ -1291,9 +1362,37 @@ - (instancetype) initWithPoints:(NSArray *)points name:(NSStri - (void) addPoint:(OAFavoriteItem *)point { + [self updateMissingAppearanceFromPoint:point]; [_points addObject:point]; } +- (void)updateMissingAppearanceFromPoint:(OAFavoriteItem *)point +{ + UIColor *pointColor = [point getInternalColor]; + if ((_color == nil || [_color toRGBNumber] == 0) && pointColor) + _color = pointColor; + + NSString *pointIcon = [point getInternalIcon]; + if (_iconName.length == 0 && pointIcon.length > 0) + _iconName = pointIcon; + + NSString *pointBackground = [point getInternalBackgroundIcon]; + if (_backgroundType.length == 0 && pointBackground.length > 0) + _backgroundType = pointBackground; +} + +- (void)applyMissingAppearanceToPoint:(OAFavoriteItem *)point +{ + if (_color != nil && [_color toRGBNumber] != 0 && ![point getInternalColor]) + [point setColor:_color]; + + if (_iconName.length > 0 && [point getInternalIcon].length == 0) + [point setIcon:_iconName]; + + if (_backgroundType.length > 0 && [point getInternalBackgroundIcon].length == 0) + [point setBackgroundIcon:_backgroundType]; +} + - (UIColor *) color { if (_color != nil && [_color toRGBNumber] != 0) @@ -1312,6 +1411,11 @@ + (BOOL) isPersonal:(NSString *)name return [name isEqualToString:kPersonalCategory]; } ++ (BOOL)isBaseFavoriteOrPersonalGroup:(NSString *)name +{ + return name.length == 0 || [self isPersonal:name]; +} + + (BOOL) isPersonalCategoryDisplayName:(NSString *)name { return [name isEqualToString:OALocalizedString(@"personal_category_name")]; @@ -1340,7 +1444,8 @@ - (OASGpxUtilitiesPointsGroup *)toPointsGroup { NSString *mxPrefix = @"mx_"; _iconName = [self removePrefix:mxPrefix fromValue:_iconName]; - OASGpxUtilitiesPointsGroup *pointsGroup = [[OASGpxUtilitiesPointsGroup alloc] initWithName:_name iconName:_iconName backgroundType:_backgroundType color:[self color].toARGBNumber hidden:!_isVisible]; + OASBoolean *pinned = [OASBoolean numberWithBool:_isPinned]; + OASGpxUtilitiesPointsGroup *pointsGroup = [[OASGpxUtilitiesPointsGroup alloc] initWithName:_name iconName:_iconName backgroundType:_backgroundType color:[self color].toARGBNumber hidden:!_isVisible pinned:pinned]; NSMutableArray *points = [NSMutableArray array]; for (OAFavoriteItem *point in _points) @@ -1362,16 +1467,20 @@ + (OAFavoriteGroup *)fromPointsGroup:(OASGpxUtilitiesPointsGroup *)pointsGroup { OAFavoriteGroup *favoriteGroup = [[OAFavoriteGroup alloc] init]; favoriteGroup.name = pointsGroup.name; - favoriteGroup.color = UIColorFromRGB(pointsGroup.color); + favoriteGroup.color = pointsGroup.color != 0 ? UIColorFromRGB(pointsGroup.color) : nil; favoriteGroup.iconName = pointsGroup.iconName; favoriteGroup.backgroundType = pointsGroup.backgroundType; + favoriteGroup.isPinned = pointsGroup.isPinned ? pointsGroup.isPinned.boolValue : [self isBaseFavoriteOrPersonalGroup:favoriteGroup.name ?: @""]; for (OASWptPt *point in pointsGroup.points) { - [favoriteGroup.points addObject:[OAFavoriteItem fromWpt:point - category:nil]]; + OAFavoriteItem *favorite = [OAFavoriteItem fromWpt:point category:nil]; + [favoriteGroup applyMissingAppearanceToPoint:favorite]; + [favoriteGroup addPoint:favorite]; } + if (favoriteGroup.points && favoriteGroup.points.count > 0) favoriteGroup.isVisible = favoriteGroup.points[0].isVisible; + return favoriteGroup; } @@ -1380,6 +1489,7 @@ + (OAFavoriteGroup *)fromPointsGroup:(OASGpxUtilitiesPointsGroup *)pointsGroup - (id) copyWithZone:(NSZone *)zone { OAFavoriteGroup *clone = [[OAFavoriteGroup alloc] initWithPoints:_points name:_name isVisible:_isVisible color:_color]; + clone.isPinned = _isPinned; clone.iconName = _iconName; clone.backgroundType = _backgroundType; return clone; diff --git a/Sources/Models/OAFavoriteItem.mm b/Sources/Models/OAFavoriteItem.mm index ce151678e6..ec267ef6d3 100644 --- a/Sources/Models/OAFavoriteItem.mm +++ b/Sources/Models/OAFavoriteItem.mm @@ -442,7 +442,12 @@ - (UIColor *) getInternalColor { _color = [UIColor colorWithRed:color.r/255.0 green:color.g/255.0 blue:color.b/255.0 alpha:color.a/255.0]; } - return nil; + else + { + _color = nil; + } + + return _color; } - (UIColor *) getColor diff --git a/Sources/OsmAnd Maps-Bridging-Header.h b/Sources/OsmAnd Maps-Bridging-Header.h index 263de86323..e990304fce 100644 --- a/Sources/OsmAnd Maps-Bridging-Header.h +++ b/Sources/OsmAnd Maps-Bridging-Header.h @@ -64,6 +64,11 @@ #import "OACommonTypes.h" #import "OABaseCollectionHandler.h" #import "OAResourcesUISwiftHelper.h" +#import "OAEditGroupViewController.h" +#import "OAFavoriteGroupEditorViewController.h" +#import "OAFavoritesBridgeHelper.h" +#import "OAOpenAddTrackViewController.h" +#import "OAEditColorViewController.h" #import "OAResourcesInstaller.h" #import "OATravelGuidesHelper.h" #import "OAGPXDocumentAdapter.h" @@ -111,6 +116,9 @@ #import "OAOsmBugsDBHelper.h" #import "OAOpenStreetMapPoint.h" #import "OAOsmNotePoint.h" +#import "OAShareMenuActivity.h" +#import "OAFavoriteFolderBridgeItem.h" +#import "OAFavoritePointBridgeItem.h" #import "OATrackPreviewMapRenderer.h" // Widgets @@ -215,7 +223,6 @@ #import "OACloudIntroductionViewController.h" #import "OAHelpViewController.h" #import "InitialRoutePlanningBottomSheetViewController.h" -#import "OAFavoriteListViewController.h" #import "OATripRecordingSettingsViewController.h" #import "OAPluginsViewController.h" #import "OAPluginDetailsViewController.h" @@ -223,6 +230,7 @@ #import "OAOsmNoteViewController.h" #import "OAOsmEditingViewController.h" #import "OAOsmUploadPOIViewController.h" +#import "OAEditPointViewController.h" #import "OAAddTrackFolderViewController.h" // Cells diff --git a/Sources/SwiftExtensions/DateFormatter+Extension.swift b/Sources/SwiftExtensions/DateFormatter+Extension.swift index c3021aedc7..5d9d84370b 100644 --- a/Sources/SwiftExtensions/DateFormatter+Extension.swift +++ b/Sources/SwiftExtensions/DateFormatter+Extension.swift @@ -30,4 +30,12 @@ extension DateFormatter { formatter.dateFormat = "HH:mm:ss" return formatter }() + + // 09.09.2013 + static let detailsDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.timeZone = .current + formatter.dateFormat = "dd.MM.yyyy" + return formatter + }() }