From a278e112eac4d4b87bde1ed6d5218b7a3e628db7 Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Thu, 4 Jun 2026 14:20:43 +0300 Subject: [PATCH 01/41] Add FavoriteListViewController --- OsmAnd.xcodeproj/project.pbxproj | 14 +- .../en.lproj/Localizable.strings | 7 + .../MyPlaces/FavoriteListViewController.swift | 637 ++++++++++++++++++ .../MyPlacesContainerViewController.swift | 7 +- .../MyPlaces/OAFavoriteFoldersBridge.h | 60 ++ .../MyPlaces/OAFavoriteFoldersBridge.mm | 581 ++++++++++++++++ Sources/Helpers/OAFavoritesHelper.h | 5 + Sources/Helpers/OAFavoritesHelper.mm | 16 +- Sources/OsmAnd Maps-Bridging-Header.h | 1 + 9 files changed, 1321 insertions(+), 7 deletions(-) create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController.swift create mode 100644 Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h create mode 100644 Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index c1645665fa..d638c87c62 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -1582,6 +1582,8 @@ 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 */; }; + C5EB56EC2FD188A900D01657 /* OAFavoriteFoldersBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56EB2FD188A900D01657 /* OAFavoriteFoldersBridge.mm */; }; + C5EB56EE2FD18B8A00D01657 /* FavoriteListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.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 */; }; @@ -3356,7 +3358,6 @@ FA9628E42AFCFCE1004B7DEF /* SectionHeaderFooterButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9628E32AFCFCE1004B7DEF /* SectionHeaderFooterButton.swift */; }; FA9628E62AFD2F2E004B7DEF /* BLETemperatureSensor.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9628E52AFD2F2E004B7DEF /* BLETemperatureSensor.swift */; }; FA9F416F2E6F10B7001A49E9 /* ImageCacheInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9F416E2E6F10B7001A49E9 /* ImageCacheInfoViewController.swift */; }; - FADEC0DE000000000000A001 /* OATouchIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADEC0DE000000000000A002 /* OATouchIndicatorController.swift */; }; FA9FA5992E97D11400376C47 /* MapScrollHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA9FA5982E97D11400376C47 /* MapScrollHelper.swift */; }; FAA055762E02C51400F84EDF /* VehicleMetricsTripRecordingCommandsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA055752E02C51200F84EDF /* VehicleMetricsTripRecordingCommandsViewController.swift */; }; FAA0557A2E0301AA00F84EDF /* SelectionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAA055792E0301AA00F84EDF /* SelectionManager.swift */; }; @@ -3421,6 +3422,7 @@ FAD8489A2CAD6F950018AF92 /* WptPtExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD848992CAD6F950018AF92 /* WptPtExtension.swift */; }; FAD848A02CAE9FDD0018AF92 /* GpxFileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAD8489F2CAE9FDD0018AF92 /* GpxFileExtension.swift */; }; FADD74092FC0433400004A51 /* RenderedObjectAmenityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADD74082FC0433400004A51 /* RenderedObjectAmenityProvider.swift */; }; + FADEC0DE000000000000A001 /* OATouchIndicatorController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FADEC0DE000000000000A002 /* OATouchIndicatorController.swift */; }; FAE0B7112AFCDAD400C02438 /* BLEPairedSensorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE0B7102AFCDAD400C02438 /* BLEPairedSensorsViewController.swift */; }; FAE0B7132AFCDC6B00C02438 /* BLEPairedSensors.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FAE0B7122AFCDC6B00C02438 /* BLEPairedSensors.storyboard */; }; FAE0B7162AFCEF4D00C02438 /* СhoicePairedDeviceTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAE0B7152AFCEF4D00C02438 /* СhoicePairedDeviceTableViewCell.swift */; }; @@ -5474,6 +5476,9 @@ 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 = ""; }; + C5EB56EA2FD188A900D01657 /* OAFavoriteFoldersBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OAFavoriteFoldersBridge.h; sourceTree = ""; }; + C5EB56EB2FD188A900D01657 /* OAFavoriteFoldersBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OAFavoriteFoldersBridge.mm; sourceTree = ""; }; + C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteListViewController.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 = ""; }; @@ -8127,7 +8132,6 @@ FA9628E32AFCFCE1004B7DEF /* SectionHeaderFooterButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SectionHeaderFooterButton.swift; sourceTree = ""; }; FA9628E52AFD2F2E004B7DEF /* BLETemperatureSensor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLETemperatureSensor.swift; sourceTree = ""; }; FA9F416E2E6F10B7001A49E9 /* ImageCacheInfoViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCacheInfoViewController.swift; sourceTree = ""; }; - FADEC0DE000000000000A002 /* OATouchIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATouchIndicatorController.swift; sourceTree = ""; }; FA9FA5982E97D11400376C47 /* MapScrollHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapScrollHelper.swift; sourceTree = ""; }; FAA055752E02C51200F84EDF /* VehicleMetricsTripRecordingCommandsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleMetricsTripRecordingCommandsViewController.swift; sourceTree = ""; }; FAA055792E0301AA00F84EDF /* SelectionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionManager.swift; sourceTree = ""; }; @@ -8194,6 +8198,7 @@ FAD848992CAD6F950018AF92 /* WptPtExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WptPtExtension.swift; sourceTree = ""; }; FAD8489F2CAE9FDD0018AF92 /* GpxFileExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GpxFileExtension.swift; sourceTree = ""; }; FADD74082FC0433400004A51 /* RenderedObjectAmenityProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderedObjectAmenityProvider.swift; sourceTree = ""; }; + FADEC0DE000000000000A002 /* OATouchIndicatorController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OATouchIndicatorController.swift; sourceTree = ""; }; FADFF8872C885AFD00F68E4F /* OsmAndSharedWrapper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OsmAndSharedWrapper.h; sourceTree = ""; }; FAE0B7102AFCDAD400C02438 /* BLEPairedSensorsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BLEPairedSensorsViewController.swift; sourceTree = ""; }; FAE0B7122AFCDC6B00C02438 /* BLEPairedSensors.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = BLEPairedSensors.storyboard; sourceTree = ""; }; @@ -12653,6 +12658,9 @@ DA5A7DC426C563A300F274C7 /* MyPlaces.storyboard */, DA5A7E0126C563A300F274C7 /* OAFavoriteListViewController.h */, DA5A7E0C26C563A300F274C7 /* OAFavoriteListViewController.mm */, + C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */, + C5EB56EA2FD188A900D01657 /* OAFavoriteFoldersBridge.h */, + C5EB56EB2FD188A900D01657 /* OAFavoriteFoldersBridge.mm */, 327D68A42A8F9BA80076D1E0 /* SavedArticlesTabViewController.swift */, 325CFBEA2B5052E000090DF2 /* TracksViewController.swift */, C50E32812CA3FBDF00EEC41F /* TracksFiltersViewController.swift */, @@ -17725,6 +17733,7 @@ 46C841412C32F3CA00E284B0 /* OANavStartStopAction.m in Sources */, DA5A81A926C563A700F274C7 /* OAAbbreviations.mm in Sources */, 32AB48692C9C50CB005EF1D4 /* DownloadingListHelper.swift in Sources */, + C5EB56EC2FD188A900D01657 /* OAFavoriteFoldersBridge.mm in Sources */, DAD3FD242A4ACB2E00BFA03A /* WidgetGroupListViewController.swift in Sources */, FAA650532ADD42C50020DCEA /* Sensor.swift in Sources */, FAC166282B21BEF900D63755 /* Central.swift in Sources */, @@ -17738,6 +17747,7 @@ DA5A848F26C563A900F274C7 /* OABaseTableViewController.m in Sources */, DA5A849326C563A900F274C7 /* OAGpxApproximationViewController.mm in Sources */, 32C21CB727E0A66B00DE4266 /* OARecordSettingsBottomSheetViewController.m in Sources */, + C5EB56EE2FD18B8A00D01657 /* FavoriteListViewController.swift in Sources */, DA5A850926C563A900F274C7 /* UITableViewCell+getTableView.m in Sources */, DA5A83FF26C563A800F274C7 /* OAQuickSearchEmptyResultListItem.mm in Sources */, DA5A838926C563A800F274C7 /* OADonationSettingsViewController.mm in Sources */, diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index 8229a7b23c..0b8151ed07 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -1009,6 +1009,13 @@ "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"; +"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"; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift new file mode 100644 index 0000000000..e8fb4f902a --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -0,0 +1,637 @@ +// +// FavoriteListViewController.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import UIKit +import UniformTypeIdentifiers + +private enum ScreenMode { + case root + case folder(FavoriteFolderRow, previousTitle: String) +} + +private 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") + } + } +} + +private enum FavoriteListSection: Hashable { + case folderSection(FavoriteFolderSection) + case content +} + +private enum FavoriteListItem: Hashable { + case header(FavoriteFolderSection) + case folder(FavoriteFolderRow) + case favorite(FavoritePointRow) +} + +private struct FavoriteFolderRow: Hashable { + let identifier: String + let groupName: String + let title: String + let pointsCount: Int + let isVisible: Bool + let isPinned: Bool + let color: UIColor? + + var iconName: String { + isVisible ? "ic_custom_folder" : "ic_custom_folder_hidden_outlined" + } + + var iconColor: UIColor { + isVisible ? (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) { + identifier = item.identifier + groupName = item.groupName + title = Self.title(for: item.groupName, fallback: item.title) + pointsCount = Int(item.pointsCount) + isVisible = item.isVisible + isPinned = item.isPinned + color = item.color + } + + private static func title(for groupName: String, fallback: String) -> String { + guard !groupName.isEmpty else { return fallback } + return groupName.components(separatedBy: "/").last ?? fallback + } +} + +private struct FavoritePointRow: Hashable { + let identifier: String + let title: String + let subtitle: String? + let icon: UIImage? + let isVisible: Bool + + 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: OAFavoritePointBridgeItem) { + identifier = item.identifier + title = item.title + subtitle = item.subtitle + icon = item.icon + isVisible = item.isVisible + } +} + +final class FavoriteListViewController: UIViewController { + private typealias DataSource = UICollectionViewDiffableDataSource + private typealias Snapshot = NSDiffableDataSourceSnapshot + private typealias CellRegistration = UICollectionView.CellRegistration + + weak var myPlacesDelegate: MyPlacesDelegate? + + private static let imageSize: CGFloat = 30 + private static let navigationTitleFontSize: CGFloat = 17.0 + private static let navigationTitleMaximumSize: CGFloat = 22.0 + private static let navigationSubtitleFontSize: CGFloat = 12.0 + private static let navigationSubtitleMaximumSize: CGFloat = 18.0 + + private let screenMode: ScreenMode + + private var searchText = "" + private var isSearchActive = false + private var isRootFolder: Bool { + guard case .root = screenMode else { return false } + return true + } + private var normalTitle: String { + switch screenMode { + case .root: + localizedString("shared_string_favorites") + case .folder(let folder, _): + folder.title + } + } + private var normalSubtitle: String { + switch screenMode { + case .root: + localizedString("shared_string_my_places") + case .folder(_, let previousTitle): + previousTitle + } + } + + private 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 + }() + private lazy var headerCellRegistration = 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 + } + private lazy var folderCellRegistration = CellRegistration { [weak self] cell, _, folder in + var content = cell.defaultContentConfiguration() + content.image = 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.secondaryText = "\(localizedString("points_count")) \(folder.pointsCount)" + content.secondaryTextProperties.color = .textColorSecondary + cell.contentConfiguration = content + cell.accessories = self?.collectionView.isEditing == true ? [.multiselect()] : [.multiselect(), .disclosureIndicator()] + } + private lazy var favoriteCellRegistration = CellRegistration { cell, _, favorite in + var content = cell.defaultContentConfiguration() + content.image = favorite.icon + content.text = favorite.title + content.textProperties.color = favorite.titleColor + content.textProperties.font = favorite.titleFont + content.secondaryText = favorite.subtitle + content.secondaryTextProperties.color = .textColorSecondary + cell.contentConfiguration = content + cell.accessories = [.multiselect()] + } + private 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 + }() + private lazy var dataSource: DataSource = makeDataSource() + + convenience init(frame: CGRect) { + self.init(frame: frame, screenMode: .root) + } + + private 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) + } + + deinit { + NotificationCenter.default.removeObserver(self, name: .favoriteImportViewControllerDidDismiss, object: nil) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + configureNavigation() + applySnapshot() + } + + 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 { + var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + configuration.headerMode = isRootFolder ? .firstItemInSection : .none + return UICollectionViewCompositionalLayout.list(using: configuration) + } + + private func configureNavigation() { + navigationController?.setNavigationBarHidden(false, animated: false) + navigationController?.setToolbarHidden(true, animated: false) + navigationController?.navigationBar.prefersLargeTitles = false + configureNavigationButtons() + configureSearchVisibility() + updateNavigationBarTitle() + updateSegmentedControlVisibility() + myPlacesDelegate?.updateToolbar?(with: nil) + } + + 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 selectAllButton = UIBarButtonItem(title: localizedString("shared_string_select_all"), style: .plain, target: self, action: #selector(selectAllButtonPressed)) + selectAllButton.accessibilityLabel = localizedString("shared_string_select_all") + 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: UIImage(systemName: "ellipsis.circle"), 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 updateNavigationBarTitle() { + if collectionView.isEditing { + let selectedItemsCount = collectionView.indexPathsForSelectedItems?.count ?? 0 + let itemText = localizedString(selectedItemsCount > 1 ? "shared_string_items" : "shared_string_item").lowercased() + let title = selectedItemsCount == 0 ? localizedString("select_items") : "\(selectedItemsCount) \(itemText)" + setNavigationTitle(title, subtitle: "", hideSubtitle: true) + } else { + setNavigationTitle(normalTitle, subtitle: normalSubtitle, hideSubtitle: false) + } + } + + private func setNavigationTitle(_ title: String, subtitle: String, hideSubtitle: Bool) { + if isRootFolder { + 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)) + } + } + + private func updateSegmentedControlVisibility() { + myPlacesDelegate?.updateSegmentedControlVisibility(isRootFolder && !collectionView.isEditing && !isSearchActive) + } + + private func makeDataSource() -> DataSource { + let folderCellRegistration = folderCellRegistration + let favoriteCellRegistration = favoriteCellRegistration + let headerCellRegistration = headerCellRegistration + return DataSource(collectionView: collectionView) { collectionView, indexPath, item in + switch 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) + } + } + } + + private func applySnapshot(animatingDifferences: Bool = false) { + switch screenMode { + case .root: + applyRootSnapshot(animatingDifferences: animatingDifferences) + case .folder(let folder, _): + applyFolderSnapshot(folder: folder, animatingDifferences: animatingDifferences) + } + } + + private func applyRootSnapshot(animatingDifferences: Bool) { + let foldersBySection = favoriteFoldersBySection() + let folderSections = rootSections(foldersBySection: foldersBySection) + var snapshot = Snapshot() + snapshot.appendSections(folderSections.map { .folderSection($0) }) + 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 folders = directFavoriteFolders(parentGroupName: folder.groupName).filter { matchesSearch($0.title) } + let favorites = OAFavoriteFoldersBridge.favoritePoints(forGroupName: folder.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.subtitle) } + var snapshot = Snapshot() + snapshot.appendSections([.content]) + snapshot.appendItems(folders.map(FavoriteListItem.folder), toSection: .content) + snapshot.appendItems(favorites.map(FavoriteListItem.favorite), toSection: .content) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + } + + private func favoriteFoldersBySection() -> [FavoriteFolderSection: [FavoriteFolderRow]] { + let folders = directFavoriteFolders(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 !isSearchActive || !(foldersBySection[.visible] ?? []).isEmpty { + sections.append(.visible) + } + + if !(foldersBySection[.hidden] ?? []).isEmpty { + sections.append(.hidden) + } + + return sections + } + + private func directFavoriteFolders(parentGroupName: String?) -> [FavoriteFolderRow] { + OAFavoriteFoldersBridge.favoriteFolders() + .map { FavoriteFolderRow(item: $0) } + .filter { isDirectFolder($0.groupName, parentGroupName: parentGroupName) } + .sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } + } + + private func isDirectFolder(_ groupName: String, parentGroupName: String?) -> Bool { + guard let parentGroupName else { return groupName.isEmpty || !groupName.contains("/") } + guard !parentGroupName.isEmpty else { return false } + guard 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?.localizedCaseInsensitiveContains(searchText) ?? false + } + + private func makeActionsMenu() -> UIMenu { + let importAction = UIAction(title: localizedString("shared_string_import"), image: menuImage("ic_custom_import_outlined")) { [weak self] _ in + guard let self else { return } + let gpxType = UTType(filenameExtension: "gpx") ?? UTType(importedAs: "com.topografix.gpx", conformingTo: .xml) + let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [gpxType], asCopy: true) + documentPicker.allowsMultipleSelection = false + documentPicker.delegate = self + present(documentPicker, animated: true) + } + + return UIMenu(title: "", options: .displayInline, children: [importAction]) + } + + private func setEdit(_ isEdit: Bool) { + if !isEdit { + collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) } + } + + collectionView.isEditing = isEdit + collectionView.reloadData() + myPlacesDelegate?.updateEditMode(isEdit) + configureNavigation() + } + + private func makeFolderContextMenu(for folder: FavoriteFolderRow, indexPath: IndexPath) -> UIMenu { + let showHideAction = UIAction(title: localizedString(folder.isVisible ? "shared_string_hide_from_map" : "shared_string_show_on_map"), image: menuImage(folder.isVisible ? "ic_custom_hide_outlined" : "ic_custom_show_outlined")) { [weak self] _ in + OAFavoriteFoldersBridge.setFavoriteGroupVisible(folder.groupName, visible: !folder.isVisible) + self?.applySnapshot(animatingDifferences: true) + } + let pinAction = UIAction(title: localizedString(folder.isPinned ? "unpin_folder" : "pin_folder"), image: menuImage("ic_custom_map_pin_outlined")) { [weak self] _ in + OAFavoriteFoldersBridge.setFavoriteGroupPinned(folder.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: menuImage("ic_custom_edit")) { [weak self] _ in + self?.showRenameAlert(for: folder) + } + let defaultAppearanceAction = UIAction(title: localizedString("default_appearance"), image: menuImage("ic_custom_appearance_outlined")) { [weak self] _ in + guard let navigationController = self?.navigationController else { return } + OAFavoriteFoldersBridge.openFavoriteGroupAppearance(folder.groupName, navigationController: navigationController) + } + let secondButtonsSection = UIMenu(title: "", options: .displayInline, children: [renameAction, defaultAppearanceAction]) + + let shareAction = UIAction(title: localizedString("shared_string_share"), image: menuImage("ic_custom_export_outlined")) { [weak self] _ in + guard let self else { return } + let sourceView: UIView = self.collectionView.cellForItem(at: indexPath) ?? self.collectionView + OAFavoriteFoldersBridge.shareFavoriteGroup(folder.groupName, sourceView: sourceView, viewController: self) + } + let moveAction = UIAction(title: localizedString("shared_string_move"), image: menuImage("ic_custom_folder_move_outlined")) { [weak self] _ in + guard let navigationController = self?.navigationController else { return } + OAFavoriteFoldersBridge.openFavoriteGroupMove(folder.groupName, navigationController: navigationController) { [weak self] in + self?.applySnapshot(animatingDifferences: true) + } + } + let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: folder.groupName.isEmpty ? [shareAction] : [shareAction, moveAction]) + + let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: menuImage("ic_custom_map_pin_outlined")) { _ in + OAFavoriteFoldersBridge.addFavoriteGroup(toMapMarkers: folder.groupName) + } + let trackAction = UIAction(title: localizedString("add_to_a_track"), image: menuImage("ic_custom_trip")) { [weak self] _ in + guard let navigationController = self?.navigationController else { return } + OAFavoriteFoldersBridge.openFavoriteGroupAdd(toTrack: folder.groupName, navigationController: navigationController) + } + let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: menuImage("ic_custom_navigation_outlined")) { _ in + OAFavoriteFoldersBridge.addFavoriteGroup(toNavigation: folder.groupName) + } + let addToMenu = UIMenu(title: localizedString("shared_string_add"), image: menuImage("ic_custom_add"), children: [mapMarkersAction, trackAction, navigationAction]) + let fourthButtonsSection = UIMenu(title: "", options: .displayInline, children: [addToMenu]) + + let deleteAction = UIAction(title: localizedString("shared_string_delete"), image: menuImage("ic_custom_trash_outlined"), attributes: .destructive) { [weak self] _ in + self?.showDeleteAlert(for: folder) + } + let lastButtonsSection = UIMenu(title: "", options: .displayInline, children: [deleteAction]) + + return UIMenu(title: "", children: [firstButtonsSection, secondButtonsSection, thirdButtonsSection, fourthButtonsSection, lastButtonsSection]) + } + + private 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 text = alert?.textFields?.first?.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return } + let newGroupName = self?.groupName(folder.groupName, replacingLastComponentWith: text) ?? text + OAFavoriteFoldersBridge.renameFavoriteGroup(folder.groupName, newName: 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) + } + + private func groupName(_ groupName: String, replacingLastComponentWith lastComponent: String) -> String { + guard let separatorIndex = groupName.lastIndex(of: "/") else { return lastComponent } + let parentGroupName = groupName[.. UIImage? { + UIImage(named: name)?.resizedMenuImage() + } + + @objc private func selectButtonPressed() { + setEdit(true) + } + + @objc private func cancelButtonPressed() { + setEdit(false) + } + + @objc private func selectAllButtonPressed() { + for section in 0.. UIContextMenuConfiguration? { + guard !collectionView.isEditing else { return nil } + guard let item = dataSource.itemIdentifier(for: indexPath), case .folder(let folder) = item else { return nil } + let menuProvider: UIContextMenuActionProvider = { [weak self] _ in + self?.makeFolderContextMenu(for: folder, indexPath: indexPath) + } + + return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: menuProvider) + } +} + +extension FavoriteListViewController: MyPlacesSearchable, UISearchResultsUpdating, UISearchBarDelegate { + func updateSearchResults(for searchController: UISearchController) { + searchResults(for: searchController) + } + + func searchResults(for searchController: UISearchController) { + isSearchActive = searchController.isActive + searchText = searchController.searchBar.searchTextField.text ?? "" + updateSegmentedControlVisibility() + applySnapshot(animatingDifferences: false) + } + + func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { + isSearchActive = false + searchText = "" + updateSegmentedControlVisibility() + applySnapshot(animatingDifferences: false) + } +} + +extension FavoriteListViewController: UIDocumentPickerDelegate { + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url = urls.first else { return } + OARootViewController.instance().import(asFavorites: url) + } +} + +extension Notification.Name { + static let favoriteImportViewControllerDidDismiss = Notification.Name("OAFavoriteImportViewControllerDidDismissNotification") +} diff --git a/Sources/Controllers/MyPlaces/MyPlacesContainerViewController.swift b/Sources/Controllers/MyPlaces/MyPlacesContainerViewController.swift index 3ace191030..4239f87f88 100644 --- a/Sources/Controllers/MyPlaces/MyPlacesContainerViewController.swift +++ b/Sources/Controllers/MyPlaces/MyPlacesContainerViewController.swift @@ -49,7 +49,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 @@ -112,11 +112,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 } diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h new file mode 100644 index 0000000000..d93dafdd89 --- /dev/null +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h @@ -0,0 +1,60 @@ +// +// OAFavoriteFoldersBridge.h +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class UIColor, UIImage, UINavigationController, UIView, UIViewController; + +@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) BOOL isVisible; +@property (nonatomic, readonly) BOOL isPinned; +@property (nonatomic, readonly, nullable) UIColor *color; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +@interface OAFavoritePointBridgeItem : NSObject + +@property (nonatomic, readonly) NSString *identifier; +@property (nonatomic, readonly) NSString *groupName; +@property (nonatomic, readonly) NSString *title; +@property (nonatomic, readonly, nullable) NSString *subtitle; +@property (nonatomic, readonly, nullable) UIImage *icon; +@property (nonatomic, readonly) BOOL isVisible; + +- (instancetype)init NS_UNAVAILABLE; + +@end + +@interface OAFavoriteFoldersBridge : NSObject + ++ (NSArray *)favoriteFolders; ++ (NSArray *)favoritePointsForGroupName:(NSString *)groupName; ++ (void)openFavoritePointWithIdentifier:(NSString *)identifier; ++ (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible; ++ (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned; ++ (void)renameFavoriteGroup:(NSString *)groupName newName:(NSString *)newName; ++ (void)openFavoriteGroupAppearance:(NSString *)groupName navigationController:(UINavigationController *)navigationController; ++ (void)shareFavoriteGroup:(NSString *)groupName sourceView:(nullable UIView *)sourceView viewController:(UIViewController *)viewController; ++ (void)openFavoriteGroupMove:(NSString *)groupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion; ++ (void)deleteFavoriteGroup:(NSString *)groupName; ++ (void)addFavoriteGroupToMapMarkers:(NSString *)groupName; ++ (void)openFavoriteGroupAddToTrack:(NSString *)groupName navigationController:(UINavigationController *)navigationController; ++ (void)addFavoriteGroupToNavigation:(NSString *)groupName; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm new file mode 100644 index 0000000000..b07bde4b39 --- /dev/null +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm @@ -0,0 +1,581 @@ +// +// OAFavoriteFoldersBridge.mm +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +#import "OAFavoriteFoldersBridge.h" +#import "OAAppSettings.h" +#import "OAEditGroupViewController.h" +#import "OAFavoriteItem.h" +#import "OAFavoriteGroupEditorViewController.h" +#import "OAFavoritesHelper.h" +#import "OAGPXDatabase.h" +#import "OAIndexConstants.h" +#import "OALocationServices.h" +#import "OAMapPanelViewController.h" +#import "OAOpenAddTrackViewController.h" +#import "OAOsmAndFormatter.h" +#import "OAPointDescription.h" +#import "OARootViewController.h" +#import "OASavingTrackHelper.h" +#import "OASelectedGPXHelper.h" +#import "OATargetPointsHelper.h" +#import "OAUtilities.h" +#import "OsmAndApp.h" +#import "OsmAndSharedWrapper.h" +#import +#import + +#include + +@interface OAFavoriteFolderBridgeItem () + +- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index; + +@end + +@implementation OAFavoriteFolderBridgeItem + +- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index +{ + self = [super init]; + if (self) + { + NSString *groupName = group.name ?: @""; + _identifier = [NSString stringWithFormat:@"%@-%lu", groupName, (unsigned long)index]; + _groupName = groupName; + _title = [OAFavoriteGroup getDisplayName:groupName] ?: groupName; + _pointsCount = group.points.count; + _isVisible = group.isVisible; + _isPinned = group.isPinned; + _color = group.color; + } + + return self; +} + +@end + +@interface OAFavoritePointBridgeItem () + +- (instancetype)initWithFavorite:(OAFavoriteItem *)favorite; ++ (NSString *)subtitleForFavorite:(OAFavoriteItem *)favorite; ++ (NSString *)formattedDistanceForFavorite:(OAFavoriteItem *)favorite; ++ (NSString *)formattedDate:(NSDate *)date; + +@end + +@interface OAFavoriteFoldersBridge () + ++ (NSArray *)sortedFavoritePoints:(NSArray *)points; ++ (NSArray *)sortedFavoritePointsForGroup:(OAFavoriteGroup *)group; ++ (NSArray *)favoriteGroupsInsideOrEqualToGroupName:(NSString *)groupName; ++ (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier; ++ (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName; ++ (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; ++ (BOOL)renameFavoriteGroupTreeFromGroupName:(NSString *)sourceGroupName toGroupName:(NSString *)targetGroupName; ++ (BOOL)isGroupName:(NSString *)groupName insideOrEqualToGroupName:(NSString *)parentGroupName; ++ (NSString *)groupNameByMovingGroupName:(NSString *)groupName toParentGroupName:(NSString *)parentGroupName; ++ (NSString *)suffixForGroupName:(NSString *)groupName parentGroupName:(NSString *)parentGroupName; ++ (NSString *)lastComponentForGroupName:(NSString *)groupName; ++ (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(NSString *)gpxFileName; ++ (CLLocation *)locationForFavorite:(OAFavoriteItem *)favorite; + +@end + +@interface OAFavoriteGroupMoveDelegate : NSObject + +@property (nonatomic, copy) NSString *groupName; +@property (nonatomic, weak) OAEditGroupViewController *controller; +@property (nonatomic, copy, nullable) void (^completion)(void); + +@end + +@implementation OAFavoriteGroupMoveDelegate + +- (void)groupChanged +{ + if (!self.controller.saveChanges) + return; + + if ([OAFavoriteFoldersBridge moveFavoriteGroup:self.groupName toGroupName:self.controller.groupName] && self.completion) + self.completion(); +} + +@end + +@interface OAFavoriteGroupAddToTrackDelegate : NSObject + +@property (nonatomic, copy) NSString *groupName; + +@end + +@implementation OAFavoriteGroupAddToTrackDelegate + +- (void)onFileSelected:(NSString *)gpxFileName +{ + [OAFavoriteFoldersBridge addFavoriteGroupToTrack:self.groupName gpxFileName:gpxFileName]; +} + +@end + +@implementation OAFavoritePointBridgeItem + +- (instancetype)initWithFavorite:(OAFavoriteItem *)favorite +{ + self = [super init]; + if (self) + { + _identifier = [favorite getKey] ?: @""; + _groupName = [favorite getCategory] ?: @""; + _title = [favorite getDisplayName] ?: @""; + _subtitle = [self.class subtitleForFavorite:favorite]; + _icon = [favorite getCompositeIcon]; + _isVisible = [favorite isVisible]; + } + + return self; +} + ++ (NSString *)subtitleForFavorite:(OAFavoriteItem *)favorite +{ + NSMutableArray *parts = [NSMutableArray array]; + NSString *distance = [self formattedDistanceForFavorite:favorite]; + if (distance.length > 0) + [parts addObject:distance]; + + NSString *address = [favorite getAddress]; + if (address.length > 0) + [parts addObject:address]; + + NSDate *timestamp = [favorite getTimestamp]; + if (timestamp) + [parts addObject:[self formattedDate:timestamp]]; + + return parts.count > 0 ? [parts componentsJoinedByString:@" • "] : nil; +} + ++ (NSString *)formattedDistanceForFavorite:(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 [OAOsmAndFormatter getFormattedDistance:distance]; +} + ++ (NSString *)formattedDate:(NSDate *)date +{ + static NSDateFormatter *dateFormatter = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateStyle = NSDateFormatterShortStyle; + dateFormatter.timeStyle = NSDateFormatterNoStyle; + }); + + return [dateFormatter stringFromDate:date]; +} + +@end + +@implementation OAFavoriteFoldersBridge + ++ (NSArray *)favoriteFolders +{ + NSArray *groups = [OAFavoritesHelper getFavoriteGroups] ?: @[]; + NSMutableArray *folders = [NSMutableArray arrayWithCapacity:groups.count]; + [groups enumerateObjectsUsingBlock:^(OAFavoriteGroup * _Nonnull group, NSUInteger index, BOOL * _Nonnull stop) { + [folders addObject:[[OAFavoriteFolderBridgeItem alloc] initWithGroup:group index:index]]; + }]; + + return folders.copy; +} + ++ (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; +} + ++ (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.mapPanel openTargetViewWithFavorite:favorite pushed:YES]; +} + ++ (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return; + + [OAFavoritesHelper updateGroup:group visible:visible saveImmediately:YES]; +} + ++ (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return; + + [OAFavoritesHelper updateGroup:group pinned:pinned saveImmediately: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]; +} + ++ (void)openFavoriteGroupAppearance:(NSString *)groupName navigationController:(UINavigationController *)navigationController +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group || !navigationController) + return; + + OAFavoriteGroupEditorViewController *viewController = [[OAFavoriteGroupEditorViewController alloc] initWithGroup:[group toPointsGroup]]; + [navigationController pushViewController:viewController animated:YES]; +} + ++ (void)shareFavoriteGroup:(NSString *)groupName sourceView:(nullable UIView *)sourceView viewController:(UIViewController *)viewController +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group || !viewController) + return; + + OAFavoriteGroup *groupToShare = [[OAFavoriteGroup alloc] initWithPoints:group.points.copy name:group.name isVisible:group.isVisible color:group.color]; + groupToShare.isPinned = group.isPinned; + groupToShare.iconName = group.iconName; + groupToShare.backgroundType = group.backgroundType; + OsmAndAppInstance app = [OsmAndApp instance]; + NSString *groupFileName = [app getGroupFileName:group.name]; + NSString *filename = [NSString stringWithFormat:@"%@%@%@%@", app.favoritesFilePrefix, groupFileName.length > 0 ? app.favoritesGroupNameSeparator : @"", groupFileName ?: @"", GPX_FILE_EXT]; + NSString *fullFilename = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; + [OAFavoritesHelper saveFile:@[groupToShare] file:fullFilename]; + NSURL *favoritesUrl = [NSURL fileURLWithPath:fullFilename]; + UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[favoritesUrl] applicationActivities:nil]; + activityViewController.completionWithItemsHandler = ^(UIActivityType _Nullable activityType, BOOL completed, NSArray * _Nullable returnedItems, NSError * _Nullable activityError) { + [NSFileManager.defaultManager removeItemAtURL:favoritesUrl error:nil]; + }; + + UIPopoverPresentationController *popover = activityViewController.popoverPresentationController; + if (popover) + { + UIView *popoverSourceView = sourceView ?: viewController.view; + popover.sourceView = popoverSourceView; + popover.sourceRect = popoverSourceView.bounds; + popover.permittedArrowDirections = UIPopoverArrowDirectionAny; + } + + [viewController presentViewController:activityViewController animated:YES completion:nil]; +} + ++ (void)openFavoriteGroupMove:(NSString *)groupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group || !navigationController) + return; + + NSMutableArray *groupNames = [NSMutableArray array]; + for (OAFavoriteGroup *favoriteGroup in [OAFavoritesHelper getFavoriteGroups]) + { + NSString *favoriteGroupName = favoriteGroup.name ?: @""; + if (![self isGroupName:favoriteGroupName insideOrEqualToGroupName:group.name ?: @""]) + [groupNames addObject:favoriteGroupName]; + } + + if (![groupNames containsObject:@""]) + [groupNames addObject:@""]; + + OAEditGroupViewController *groupController = [[OAEditGroupViewController alloc] initWithGroupName:nil groups:groupNames]; + OAFavoriteGroupMoveDelegate *delegate = [[OAFavoriteGroupMoveDelegate alloc] init]; + delegate.groupName = group.name ?: @""; + delegate.controller = groupController; + delegate.completion = completion; + groupController.delegate = delegate; + objc_setAssociatedObject(groupController, @selector(groupChanged), delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + UINavigationController *modalNavigationController = [[UINavigationController alloc] initWithRootViewController:groupController]; + [navigationController presentViewController:modalNavigationController animated:YES completion:nil]; +} + ++ (void)deleteFavoriteGroup:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return; + + NSArray *groupsToDelete = [self favoriteGroupsInsideOrEqualToGroupName:group.name ?: @""]; + if (groupsToDelete.count > 0) + [OAFavoritesHelper deleteFavoriteGroups:groupsToDelete andFavoritesItems:nil]; +} + ++ (void)addFavoriteGroupToMapMarkers:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return; + + OAMapPanelViewController *mapPanel = [OARootViewController instance].mapPanel; + for (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) + { + CLLocation *location = [self locationForFavorite:favorite]; + if (!location) + continue; + + [mapPanel addMapMarker:location.coordinate.latitude lon:location.coordinate.longitude description:[favorite getDisplayName]]; + } +} + ++ (void)openFavoriteGroupAddToTrack:(NSString *)groupName navigationController:(UINavigationController *)navigationController +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group || group.points.count == 0 || !navigationController) + return; + + OAOpenAddTrackViewController *viewController = [[OAOpenAddTrackViewController alloc] initWithScreenType:EOAAddToATrack]; + OAFavoriteGroupAddToTrackDelegate *delegate = [[OAFavoriteGroupAddToTrackDelegate alloc] init]; + delegate.groupName = group.name ?: @""; + viewController.delegate = delegate; + objc_setAssociatedObject(viewController, @selector(onFileSelected:), delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + UINavigationController *modalNavigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; + [navigationController presentViewController:modalNavigationController animated:YES completion:nil]; +} + ++ (void)addFavoriteGroupToNavigation:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return; + + NSArray *points = [self sortedFavoritePointsForGroup:group]; + if (points.count == 0) + return; + + OATargetPointsHelper *targetPointsHelper = OATargetPointsHelper.sharedInstance; + [targetPointsHelper clearAllPoints:NO]; + for (NSUInteger index = 0; index < points.count; index++) + { + OAFavoriteItem *favorite = points[index]; + CLLocation *location = [self locationForFavorite:favorite]; + if (!location) + continue; + + OAPointDescription *description = [[OAPointDescription alloc] initWithType:POINT_TYPE_FAVORITE name:[favorite getDisplayName]]; + BOOL isDestination = index == points.count - 1; + [targetPointsHelper navigateToPoint:location updateRoute:isDestination intermediate:isDestination ? -1 : (int)index historyName:description]; + } + + OARootViewController *rootViewController = [OARootViewController instance]; + [rootViewController.navigationController popToRootViewControllerAnimated:YES]; + [rootViewController.mapPanel showRouteInfo]; +} + ++ (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]]; + }]; +} + ++ (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; +} + ++ (NSArray *)sortedFavoritePointsForGroup:(OAFavoriteGroup *)group +{ + return [self sortedFavoritePoints:group.points ?: @[]]; +} + ++ (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; +} + ++ (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName +{ + return [OAFavoritesHelper getGroupByName:groupName ?: @""]; +} + ++ (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]; +} + ++ (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]; + + 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 ?: @""; +} + ++ (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(NSString *)gpxFileName +{ + NSArray *points = [self sortedFavoritePointsForGroup:[self favoriteGroupWithName: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:[OAUtilities getGpxShortPath:dataItem.file.absolutePath]]; +} + ++ (CLLocation *)locationForFavorite:(OAFavoriteItem *)favorite +{ + if (!favorite.favorite) + return nil; + + return [[CLLocation alloc] initWithLatitude:[favorite getLatitude] longitude:[favorite getLongitude]]; +} + +@end 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..fb5f6375cd 100644 --- a/Sources/Helpers/OAFavoritesHelper.mm +++ b/Sources/Helpers/OAFavoritesHelper.mm @@ -582,6 +582,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 +922,7 @@ + (void)updateGroupAppearance:(OAFavoriteGroup *)favoriteGroup favoriteGroup.iconName = pointsGroup.iconName; favoriteGroup.backgroundType = pointsGroup.backgroundType; favoriteGroup.isVisible = ![pointsGroup isHidden]; + favoriteGroup.isPinned = pointsGroup.isPinned.boolValue; } } @@ -1244,6 +1254,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]); @@ -1340,7 +1351,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 = _isPinned ? [OASBoolean numberWithBool:YES] : nil; + 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) @@ -1365,6 +1377,7 @@ + (OAFavoriteGroup *)fromPointsGroup:(OASGpxUtilitiesPointsGroup *)pointsGroup favoriteGroup.color = UIColorFromRGB(pointsGroup.color); favoriteGroup.iconName = pointsGroup.iconName; favoriteGroup.backgroundType = pointsGroup.backgroundType; + favoriteGroup.isPinned = pointsGroup.isPinned.boolValue; for (OASWptPt *point in pointsGroup.points) { [favoriteGroup.points addObject:[OAFavoriteItem fromWpt:point @@ -1380,6 +1393,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/OsmAnd Maps-Bridging-Header.h b/Sources/OsmAnd Maps-Bridging-Header.h index 2281c70491..0af6a734e9 100644 --- a/Sources/OsmAnd Maps-Bridging-Header.h +++ b/Sources/OsmAnd Maps-Bridging-Header.h @@ -109,6 +109,7 @@ #import "OAOsmBugsDBHelper.h" #import "OAOpenStreetMapPoint.h" #import "OAOsmNotePoint.h" +#import "OAFavoriteFoldersBridge.h" // Widgets #import "OAMapWidgetRegistry.h" From a024943cbd2cadc1c2cd0fca33511536be880f67 Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Thu, 4 Jun 2026 19:41:36 +0300 Subject: [PATCH 02/41] Context menu Add new folder --- .../en.lproj/Localizable.strings | 1 + .../MyPlaces/FavoriteListViewController.swift | 20 +++++-- .../MyPlaces/OAFavoriteFoldersBridge.h | 1 + .../MyPlaces/OAFavoriteFoldersBridge.mm | 40 ++++++++++++++ Sources/Helpers/OAFavoritesHelper.mm | 52 +++++++++++++++++-- Sources/Models/OAFavoriteItem.mm | 7 ++- 6 files changed, 112 insertions(+), 9 deletions(-) diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index 0b8151ed07..75b0473903 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -1682,6 +1682,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"; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index e8fb4f902a..c8b310712c 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -146,6 +146,10 @@ final class FavoriteListViewController: UIViewController { previousTitle } } + private var parentGroupName: String? { + guard case .folder(let folder, _) = screenMode, !folder.groupName.isEmpty else { return nil } + return folder.groupName + } private lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) @@ -417,16 +421,24 @@ final class FavoriteListViewController: UIViewController { } private func makeActionsMenu() -> UIMenu { - let importAction = UIAction(title: localizedString("shared_string_import"), image: menuImage("ic_custom_import_outlined")) { [weak self] _ in + let addFolderAction = UIAction(title: localizedString("add_new_folder"), image: .icCustomFolderAddOutlined.resizedMenuImage()) { [weak self] _ in + guard let self, let navigationController = self.navigationController else { return } + OAFavoriteFoldersBridge.openNewFavoriteGroupEditor(withParentGroupName: self.parentGroupName, navigationController: navigationController) { [weak self] in + self?.applySnapshot(animatingDifferences: true) + } + } + let importAction = UIAction(title: localizedString("shared_string_import"), image: .icCustomImportOutlined.resizedMenuImage()) { [weak self] _ in guard let self else { return } - let gpxType = UTType(filenameExtension: "gpx") ?? UTType(importedAs: "com.topografix.gpx", conformingTo: .xml) + 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) } - - return UIMenu(title: "", options: .displayInline, children: [importAction]) + + let addFolderSection = UIMenu(title: "", options: .displayInline, children: [addFolderAction]) + let importSection = UIMenu(title: "", options: .displayInline, children: [importAction]) + return UIMenu(title: "", children: [addFolderSection, importSection]) } private func setEdit(_ isEdit: Bool) { diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h index d93dafdd89..fa131ffb8c 100644 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h @@ -44,6 +44,7 @@ NS_ASSUME_NONNULL_BEGIN + (NSArray *)favoriteFolders; + (NSArray *)favoritePointsForGroupName:(NSString *)groupName; + (void)openFavoritePointWithIdentifier:(NSString *)identifier; ++ (void)openNewFavoriteGroupEditorWithParentGroupName:(nullable NSString *)parentGroupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion; + (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible; + (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned; + (void)renameFavoriteGroup:(NSString *)groupName newName:(NSString *)newName; diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm index b07bde4b39..aa254a0c8c 100644 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm @@ -122,6 +122,30 @@ - (void)onFileSelected:(NSString *)gpxFileName @end +@interface OAFavoriteGroupCreationHandler : NSObject + +@property (nonatomic, copy) NSString *parentGroupName; +@property (nonatomic, copy, nullable) void (^completion)(void); + +@end + +@implementation OAFavoriteGroupCreationHandler + +- (void)addNewItemWithName:(NSString *)name iconName:(NSString *)iconName color:(UIColor *)color backgroundIconName:(NSString *)backgroundIconName +{ + NSString *trimmedName = [(name ?: @"") stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; + NSString *groupName = self.parentGroupName.length > 0 && trimmedName.length > 0 ? [NSString stringWithFormat:@"%@/%@", self.parentGroupName, trimmedName] : trimmedName; + if (groupName.length == 0 || [OAFavoritesHelper getGroupByName:groupName]) + return; + + [OAFavoritesHelper addFavoriteGroup:groupName color:color iconName:iconName backgroundIconName:backgroundIconName]; + [OAFavoritesHelper saveCurrentPointsIntoFile]; + if (self.completion) + self.completion(); +} + +@end + @implementation OAFavoritePointBridgeItem - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite @@ -209,6 +233,21 @@ @implementation OAFavoriteFoldersBridge return items.copy; } ++ (void)openNewFavoriteGroupEditorWithParentGroupName:(nullable NSString *)parentGroupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion +{ + if (!navigationController) + return; + + OAFavoriteGroupEditorViewController *viewController = [[OAFavoriteGroupEditorViewController alloc] initWithNew]; + OAFavoriteGroupCreationHandler *handler = [[OAFavoriteGroupCreationHandler alloc] init]; + handler.parentGroupName = parentGroupName ?: @""; + handler.completion = completion; + viewController.delegate = (id) handler; + objc_setAssociatedObject(viewController, @selector(openNewFavoriteGroupEditorWithParentGroupName:navigationController:completion:), handler, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + UINavigationController *modalNavigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; + [navigationController presentViewController:modalNavigationController animated:YES completion:nil]; +} + + (void)openFavoritePointWithIdentifier:(NSString *)identifier { OAFavoriteItem *favorite = [self favoritePointWithIdentifier:identifier]; @@ -223,6 +262,7 @@ + (void)openFavoritePointWithIdentifier:(NSString *)identifier 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]; } diff --git a/Sources/Helpers/OAFavoritesHelper.mm b/Sources/Helpers/OAFavoritesHelper.mm index fb5f6375cd..a8d61cef36 100644 --- a/Sources/Helpers/OAFavoritesHelper.mm +++ b/Sources/Helpers/OAFavoritesHelper.mm @@ -1228,6 +1228,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 @@ -1302,9 +1310,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) @@ -1323,6 +1359,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")]; @@ -1374,17 +1415,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.boolValue; + 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; } 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 From f993569563f41326558fd58b9b00926756b920f4 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Thu, 4 Jun 2026 19:52:57 +0300 Subject: [PATCH 03/41] toolbar ui --- .../MyPlaces/FavoriteListViewController.swift | 128 +++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index c8b310712c..86a08fd850 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -12,6 +12,15 @@ import UniformTypeIdentifiers private enum ScreenMode { case root case folder(FavoriteFolderRow, previousTitle: String) + + var isRoot: Bool { + switch self { + case .root: + return true + case .folder: + return false + } + } } private enum FavoriteFolderSection: Hashable { @@ -235,6 +244,8 @@ final class FavoriteListViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) configureNavigation() + navigationController?.setToolbarHidden(true, animated: false) + configureToolbar() applySnapshot() } @@ -251,13 +262,11 @@ final class FavoriteListViewController: UIViewController { private func configureNavigation() { navigationController?.setNavigationBarHidden(false, animated: false) - navigationController?.setToolbarHidden(true, animated: false) navigationController?.navigationBar.prefersLargeTitles = false configureNavigationButtons() configureSearchVisibility() updateNavigationBarTitle() updateSegmentedControlVisibility() - myPlacesDelegate?.updateToolbar?(with: nil) } private func configureNavigationButtons() { @@ -303,6 +312,26 @@ final class FavoriteListViewController: UIViewController { navigationItem.searchController = collectionView.isEditing ? nil : subfolderSearchController } + private func configureToolbar() { + let fixedSpacer = 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 deleteButton = UIBarButtonItem(image: .icCustomTrashOutlined, style: .plain, target: self, action: #selector(deleteButtonClicked)) + deleteButton.tintColor = .iconColorDisruptive + + let items = [shareButton, fixedSpacer, moveButton, flexibleSpacer, deleteButton] + + if screenMode.isRoot { + myPlacesDelegate?.updateToolbar?(with: items) + } else { + toolbarItems = items + } + } + private func updateNavigationBarTitle() { if collectionView.isEditing { let selectedItemsCount = collectionView.indexPathsForSelectedItems?.count ?? 0 @@ -450,6 +479,7 @@ final class FavoriteListViewController: UIViewController { collectionView.reloadData() myPlacesDelegate?.updateEditMode(isEdit) configureNavigation() + navigationController?.setToolbarHidden(!isEdit, animated: true) } private func makeFolderContextMenu(for folder: FavoriteFolderRow, indexPath: IndexPath) -> UIMenu { @@ -544,6 +574,37 @@ final class FavoriteListViewController: UIViewController { present(alert, animated: true) } + private func shareItems(_ selectedItems: [IndexPath], sourceView: UIView) { + if selectedItems.isEmpty { + 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 + } + + // TODO +// guard let favoritesUrl = OAFavoriteFoldersBridge.shareFavoriteItems(bridgeItems(for: selectedItems)) else { return } +// showActivity( +// [favoritesUrl], +// sourceView: sourceView, +// barButtonItem: nil, +// completionWithItemsHandler: { +// try? FileManager.default.removeItem(at: favoritesUrl) +// } +// ) + } + private func menuImage(_ name: String) -> UIImage? { UIImage(named: name)?.resizedMenuImage() } @@ -574,6 +635,69 @@ final class FavoriteListViewController: UIViewController { @objc private func favoriteDataDidChange() { applySnapshot(animatingDifferences: true) } + + @objc private func shareButtonClicked(_ sender: Any) { + // TODO +// guard let items = collectionView.indexPathsForSelectedItems else { +// return +// } +// let sourceView = sender as? UIView ?? collectionView +// shareItems(items, sourceView: sourceView) +// setEdit(false) +// applySnapshot() + } + + @objc private 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 + } + + // TODO + //guard let navigationController else { return } +// OAFavoriteFoldersBridge.openFavoriteItemsMove(bridgeItems(for: selectedItems), navigationController: navigationController) { [weak self] in +// self?.setEdit(false) +// self?.applySnapshot(animatingDifferences: true) +// } + } + + @objc private func deleteButtonClicked(_ sender: Any) { + if collectionView.indexPathsForSelectedItems?.isEmpty == true { + 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 alert = UIAlertController( + title: nil, + message: localizedString("fav_remove_q"), + preferredStyle: .alert + ) + + let yesButton = UIAlertAction( + title: localizedString("shared_string_yes"), + style: .default + ) { _ in + // TODO + //self?.removeSelectedFavoriteItems() + } + + let cancelButton = UIAlertAction( + title: localizedString("shared_string_no"), + style: .cancel, + handler: nil + ) + + alert.addAction(yesButton) + alert.addAction(cancelButton) + + present(alert, animated: true, completion: nil) + } } extension FavoriteListViewController: UICollectionViewDelegate { From 81bbb9b784b8d621564882ae9dccb2740927806f Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Thu, 4 Jun 2026 20:51:40 +0300 Subject: [PATCH 04/41] Add actionsButton to Toolbar --- .../MyPlaces/FavoriteListViewController.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 86a08fd850..2217ab23c2 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -314,17 +314,14 @@ final class FavoriteListViewController: UIViewController { private func configureToolbar() { 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: UIImage(systemName: "ellipsis.circle"), style: .plain, target: self, action: #selector(actionsButtonClicked)) let deleteButton = UIBarButtonItem(image: .icCustomTrashOutlined, style: .plain, target: self, action: #selector(deleteButtonClicked)) deleteButton.tintColor = .iconColorDisruptive - - let items = [shareButton, fixedSpacer, moveButton, flexibleSpacer, deleteButton] - + let items = [shareButton, fixedSpacer, moveButton, actionsFixedSpacer, actionsButton, flexibleSpacer, deleteButton] if screenMode.isRoot { myPlacesDelegate?.updateToolbar?(with: items) } else { @@ -664,6 +661,10 @@ final class FavoriteListViewController: UIViewController { // } } + @objc private func actionsButtonClicked(_ sender: Any) { + // TODO + } + @objc private func deleteButtonClicked(_ sender: Any) { if collectionView.indexPathsForSelectedItems?.isEmpty == true { let alert = UIAlertController(title: nil, message: localizedString("fav_select_remove"), preferredStyle: .alert) From d4208a6cd56d9ba4ecc479aa9d8de43a25bf71c8 Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Fri, 5 Jun 2026 09:58:48 +0300 Subject: [PATCH 05/41] Add FreeBackupBanner --- .../MyPlaces/FavoriteListViewController.swift | 104 ++++++++++++++---- 1 file changed, 83 insertions(+), 21 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 2217ab23c2..313aaa6838 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -12,15 +12,6 @@ import UniformTypeIdentifiers private enum ScreenMode { case root case folder(FavoriteFolderRow, previousTitle: String) - - var isRoot: Bool { - switch self { - case .root: - return true - case .folder: - return false - } - } } private enum FavoriteFolderSection: Hashable { @@ -41,11 +32,13 @@ private enum FavoriteFolderSection: Hashable { } private enum FavoriteListSection: Hashable { + case backupBanner case folderSection(FavoriteFolderSection) case content } private enum FavoriteListItem: Hashable { + case backupBanner case header(FavoriteFolderSection) case folder(FavoriteFolderRow) case favorite(FavoritePointRow) @@ -125,16 +118,23 @@ final class FavoriteListViewController: UIViewController { weak var myPlacesDelegate: MyPlacesDelegate? - private static let imageSize: CGFloat = 30 + private static let imageSize: CGFloat = 30.0 + private static let favoriteIconSize: CGFloat = 36.0 private static let navigationTitleFontSize: CGFloat = 17.0 private static let navigationTitleMaximumSize: CGFloat = 22.0 private static let navigationSubtitleFontSize: CGFloat = 12.0 private static let navigationSubtitleMaximumSize: CGFloat = 18.0 + private static let rowContentInsets = NSDirectionalEdgeInsets(top: 12.0, leading: 0.0, bottom: 12.0, trailing: 0.0) + private static let wasClosedFreeBackupFavoritesBannerKey = "wasClosedFreeBackupFavoritesBanner" private let screenMode: ScreenMode + private var layoutSections: [FavoriteListSection] = [] private var searchText = "" private var isSearchActive = false + private var isAvailablePaymentBanner: Bool { + isRootFolder && !UserDefaults.standard.bool(forKey: Self.wasClosedFreeBackupFavoritesBannerKey) && !OAIAPHelper.isOsmAndProAvailable() && !OABackupHelper.sharedInstance().isRegistered() + } private var isRootFolder: Bool { guard case .root = screenMode else { return false } return true @@ -181,8 +181,25 @@ final class FavoriteListViewController: UIViewController { cell.accessories = [.outlineDisclosure(options: disclosureOptions)] cell.tintColor = .iconColorActive } + private lazy var backupBannerCellRegistration = UICollectionView.CellRegistration { [weak self] cell, _, _ in + cell.contentView.subviews.forEach { $0.removeFromSuperview() } + guard let self, let banner = Bundle.main.loadNibNamed("FreeBackupBanner", owner: self)?.first as? FreeBackupBanner else { return } + banner.configure(bannerType: .favorite) + banner.didOsmAndCloudButtonAction = { [weak self] in + self?.navigationController?.pushViewController(OACloudIntroductionViewController(), animated: true) + } + banner.didCloseButtonAction = { [weak self] in + self?.closeFreeBackupBanner() + } + banner.translatesAutoresizingMaskIntoConstraints = false + cell.contentView.addSubview(banner) + let fittingWidth = cell.contentView.bounds.width > 0.0 ? cell.contentView.bounds.width : cell.bounds.width + NSLayoutConstraint.activate([banner.topAnchor.constraint(equalTo: cell.contentView.topAnchor), banner.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), banner.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor)]) + NSLayoutConstraint.activate([banner.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), banner.heightAnchor.constraint(equalToConstant: self.backupBannerHeight(banner, fittingWidth: fittingWidth))]) + } private lazy var folderCellRegistration = CellRegistration { [weak self] cell, _, folder in var content = cell.defaultContentConfiguration() + content.directionalLayoutMargins = Self.rowContentInsets content.image = UIImage.templateImageNamed(folder.iconName)?.resizedTemplateImage(with: FavoriteListViewController.imageSize) content.imageProperties.tintColor = folder.iconColor content.text = folder.title @@ -195,7 +212,8 @@ final class FavoriteListViewController: UIViewController { } private lazy var favoriteCellRegistration = CellRegistration { cell, _, favorite in var content = cell.defaultContentConfiguration() - content.image = favorite.icon + content.directionalLayoutMargins = Self.rowContentInsets + content.image = OAUtilities.resize(favorite.icon, newSize: CGSize(width: Self.favoriteIconSize, height: Self.favoriteIconSize)) content.text = favorite.title content.textProperties.color = favorite.titleColor content.textProperties.font = favorite.titleFont @@ -235,10 +253,12 @@ final class FavoriteListViewController: UIViewController { 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) } deinit { NotificationCenter.default.removeObserver(self, name: .favoriteImportViewControllerDidDismiss, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name(NSNotification.Name.OAIAPProductPurchased.rawValue), object: nil) } override func viewWillAppear(_ animated: Bool) { @@ -255,9 +275,12 @@ final class FavoriteListViewController: UIViewController { } private func createLayout() -> UICollectionViewLayout { - var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) - configuration.headerMode = isRootFolder ? .firstItemInSection : .none - return UICollectionViewCompositionalLayout.list(using: configuration) + UICollectionViewCompositionalLayout { [weak self] sectionIndex, environment in + var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped) + let section = self?.layoutSections.indices.contains(sectionIndex) == true ? self?.layoutSections[sectionIndex] : nil + configuration.headerMode = self?.isRootFolder == true && section != .backupBanner ? .firstItemInSection : .none + return NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: environment) + } } private func configureNavigation() { @@ -286,7 +309,7 @@ final class FavoriteListViewController: UIViewController { } 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: UIImage(systemName: "ellipsis.circle"), menu: makeActionsMenu()) + let actionsButton = UIBarButtonItem(image: .icNavbarOverflowMenuOutlined, menu: makeActionsMenu()) actionsButton.accessibilityLabel = localizedString("shared_string_actions") targetNavigationItem?.leftBarButtonItem = nil targetNavigationItem?.rightBarButtonItems = [actionsButton, selectButton] @@ -318,11 +341,11 @@ final class FavoriteListViewController: UIViewController { 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: UIImage(systemName: "ellipsis.circle"), style: .plain, target: self, action: #selector(actionsButtonClicked)) + let actionsButton = UIBarButtonItem(image: .icNavbarOverflowMenuOutlined, style: .plain, target: self, action: #selector(actionsButtonClicked)) let deleteButton = UIBarButtonItem(image: .icCustomTrashOutlined, style: .plain, target: self, action: #selector(deleteButtonClicked)) deleteButton.tintColor = .iconColorDisruptive let items = [shareButton, fixedSpacer, moveButton, actionsFixedSpacer, actionsButton, flexibleSpacer, deleteButton] - if screenMode.isRoot { + if isRootFolder { myPlacesDelegate?.updateToolbar?(with: items) } else { toolbarItems = items @@ -353,11 +376,14 @@ final class FavoriteListViewController: UIViewController { } private func makeDataSource() -> DataSource { + let backupBannerCellRegistration = backupBannerCellRegistration let folderCellRegistration = folderCellRegistration let favoriteCellRegistration = favoriteCellRegistration let headerCellRegistration = headerCellRegistration return DataSource(collectionView: collectionView) { collectionView, indexPath, item in switch item { + 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): @@ -380,8 +406,20 @@ final class FavoriteListViewController: UIViewController { private func applyRootSnapshot(animatingDifferences: Bool) { let foldersBySection = favoriteFoldersBySection() let folderSections = rootSections(foldersBySection: foldersBySection) + let isPaymentBannerVisible = isAvailablePaymentBanner var snapshot = Snapshot() - snapshot.appendSections(folderSections.map { .folderSection($0) }) + var sections = folderSections.map { FavoriteListSection.folderSection($0) } + if isPaymentBannerVisible { + sections.insert(.backupBanner, at: 0) + } + + layoutSections = sections + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections(sections) + if isPaymentBannerVisible { + snapshot.appendItems([.backupBanner], toSection: .backupBanner) + } + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) folderSections.forEach { section in let headerItem = FavoriteListItem.header(section) @@ -398,6 +436,8 @@ final class FavoriteListViewController: UIViewController { let folders = directFavoriteFolders(parentGroupName: folder.groupName).filter { matchesSearch($0.title) } let favorites = OAFavoriteFoldersBridge.favoritePoints(forGroupName: folder.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.subtitle) } var snapshot = Snapshot() + layoutSections = [.content] + collectionView.collectionViewLayout.invalidateLayout() snapshot.appendSections([.content]) snapshot.appendItems(folders.map(FavoriteListItem.folder), toSection: .content) snapshot.appendItems(favorites.map(FavoriteListItem.favorite), toSection: .content) @@ -426,6 +466,20 @@ final class FavoriteListViewController: UIViewController { return sections } + private 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) + } + + private func closeFreeBackupBanner() { + UserDefaults.standard.set(true, forKey: Self.wasClosedFreeBackupFavoritesBannerKey) + applySnapshot(animatingDifferences: true) + } + private func directFavoriteFolders(parentGroupName: String?) -> [FavoriteFolderRow] { OAFavoriteFoldersBridge.favoriteFolders() .map { FavoriteFolderRow(item: $0) } @@ -619,10 +673,12 @@ final class FavoriteListViewController: UIViewController { for item in 0.. Date: Fri, 5 Jun 2026 12:36:42 +0300 Subject: [PATCH 06/41] Add stats --- .../en.lproj/Localizable.strings | 1 + .../MyPlaces/FavoriteListViewController.swift | 134 +++++++++++++++--- .../MyPlaces/OAFavoriteFoldersBridge.h | 1 + .../MyPlaces/OAFavoriteFoldersBridge.mm | 7 + 4 files changed, 126 insertions(+), 17 deletions(-) diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index 75b0473903..aedaf514e7 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -4122,6 +4122,7 @@ "my_places_no_tracks_descr" = "This folder doesn’t have any track yet."; "delete_folder" = "Delete folder"; "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/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 313aaa6838..126ee0e62d 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -35,6 +35,7 @@ private enum FavoriteListSection: Hashable { case backupBanner case folderSection(FavoriteFolderSection) case content + case statsFooter } private enum FavoriteListItem: Hashable { @@ -42,6 +43,7 @@ private enum FavoriteListItem: Hashable { case header(FavoriteFolderSection) case folder(FavoriteFolderRow) case favorite(FavoritePointRow) + case statsFooter(FavoriteFolderStats) } private struct FavoriteFolderRow: Hashable { @@ -111,6 +113,24 @@ private struct FavoritePointRow: Hashable { } } +private 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 FavoriteListViewController: UIViewController { private typealias DataSource = UICollectionViewDiffableDataSource private typealias Snapshot = NSDiffableDataSourceSnapshot @@ -125,6 +145,7 @@ final class FavoriteListViewController: UIViewController { private static let navigationSubtitleFontSize: CGFloat = 12.0 private static let navigationSubtitleMaximumSize: CGFloat = 18.0 private static let rowContentInsets = NSDirectionalEdgeInsets(top: 12.0, leading: 0.0, bottom: 12.0, trailing: 0.0) + private static let statsFooterInsets = NSDirectionalEdgeInsets(top: 12.0, leading: 20.0, bottom: 12.0, trailing: 20.0) private static let wasClosedFreeBackupFavoritesBannerKey = "wasClosedFreeBackupFavoritesBanner" private let screenMode: ScreenMode @@ -208,9 +229,10 @@ final class FavoriteListViewController: UIViewController { content.secondaryText = "\(localizedString("points_count")) \(folder.pointsCount)" content.secondaryTextProperties.color = .textColorSecondary cell.contentConfiguration = content + cell.backgroundConfiguration = self?.listCellBackgroundConfiguration() cell.accessories = self?.collectionView.isEditing == true ? [.multiselect()] : [.multiselect(), .disclosureIndicator()] } - private lazy var favoriteCellRegistration = CellRegistration { cell, _, favorite in + private lazy var favoriteCellRegistration = CellRegistration { [weak self] cell, _, favorite in var content = cell.defaultContentConfiguration() content.directionalLayoutMargins = Self.rowContentInsets content.image = OAUtilities.resize(favorite.icon, newSize: CGSize(width: Self.favoriteIconSize, height: Self.favoriteIconSize)) @@ -220,8 +242,24 @@ final class FavoriteListViewController: UIViewController { content.secondaryText = favorite.subtitle content.secondaryTextProperties.color = .textColorSecondary cell.contentConfiguration = content + cell.backgroundConfiguration = self?.listCellBackgroundConfiguration() cell.accessories = [.multiselect()] } + private lazy var statsFooterCellRegistration = UICollectionView.CellRegistration { cell, _, stats in + cell.backgroundColor = .clear + cell.contentView.backgroundColor = .clear + cell.contentView.subviews.forEach { $0.removeFromSuperview() } + let label = UILabel() + label.font = .preferredFont(forTextStyle: .footnote) + label.adjustsFontForContentSizeCategory = true + label.textColor = .textColorSecondary + label.textAlignment = .center + label.numberOfLines = 0 + label.text = stats.text + label.translatesAutoresizingMaskIntoConstraints = false + cell.contentView.addSubview(label) + NSLayoutConstraint.activate([label.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: Self.statsFooterInsets.top), label.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: Self.statsFooterInsets.leading), label.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -Self.statsFooterInsets.trailing), label.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -Self.statsFooterInsets.bottom)]) + } private lazy var subfolderSearchController: UISearchController = { let searchController = UISearchController(searchResultsController: nil) searchController.searchResultsUpdater = self @@ -276,13 +314,31 @@ final class FavoriteListViewController: UIViewController { 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) == true ? self?.layoutSections[sectionIndex] : nil - configuration.headerMode = self?.isRootFolder == true && section != .backupBanner ? .firstItemInSection : .none + let section = self.layoutSections.indices.contains(sectionIndex) ? self.layoutSections[sectionIndex] : nil + if section == .statsFooter { + return self.statsFooterLayoutSection() + } + + configuration.headerMode = self.isRootFolder && section != .backupBanner ? .firstItemInSection : .none return NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: environment) } } + 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 listCellBackgroundConfiguration() -> UIBackgroundConfiguration { + var configuration = UIBackgroundConfiguration.listGroupedCell() + configuration.backgroundColor = .groupBg + return configuration + } + private func configureNavigation() { navigationController?.setNavigationBarHidden(false, animated: false) navigationController?.navigationBar.prefersLargeTitles = false @@ -380,6 +436,7 @@ final class FavoriteListViewController: UIViewController { let folderCellRegistration = folderCellRegistration let favoriteCellRegistration = favoriteCellRegistration let headerCellRegistration = headerCellRegistration + let statsFooterCellRegistration = statsFooterCellRegistration return DataSource(collectionView: collectionView) { collectionView, indexPath, item in switch item { case .backupBanner: @@ -390,6 +447,8 @@ final class FavoriteListViewController: UIViewController { 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) } } } @@ -404,22 +463,32 @@ final class FavoriteListViewController: UIViewController { } private func applyRootSnapshot(animatingDifferences: Bool) { - let foldersBySection = favoriteFoldersBySection() + let allFolders = favoriteFolders() + let foldersBySection = favoriteFoldersBySection(folders: allFolders) let folderSections = rootSections(foldersBySection: foldersBySection) let isPaymentBannerVisible = isAvailablePaymentBanner + let stats = folderStats(allFolders: allFolders, currentGroupName: nil) var snapshot = Snapshot() var sections = folderSections.map { FavoriteListSection.folderSection($0) } if isPaymentBannerVisible { sections.insert(.backupBanner, at: 0) } - + + if stats != nil { + sections.append(.statsFooter) + } + layoutSections = sections collectionView.collectionViewLayout.invalidateLayout() snapshot.appendSections(sections) 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) @@ -433,19 +502,25 @@ final class FavoriteListViewController: UIViewController { } private func applyFolderSnapshot(folder: FavoriteFolderRow, animatingDifferences: Bool) { - let folders = directFavoriteFolders(parentGroupName: folder.groupName).filter { matchesSearch($0.title) } + let allFolders = favoriteFolders() + let folders = directFavoriteFolders(allFolders, parentGroupName: folder.groupName).filter { matchesSearch($0.title) } let favorites = OAFavoriteFoldersBridge.favoritePoints(forGroupName: folder.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.subtitle) } + let stats = folderStats(allFolders: allFolders, currentGroupName: folder.groupName) var snapshot = Snapshot() - layoutSections = [.content] + layoutSections = stats == nil ? [.content] : [.content, .statsFooter] collectionView.collectionViewLayout.invalidateLayout() - snapshot.appendSections([.content]) + snapshot.appendSections(layoutSections) 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 favoriteFoldersBySection() -> [FavoriteFolderSection: [FavoriteFolderRow]] { - let folders = directFavoriteFolders(parentGroupName: nil).filter { matchesSearch($0.title) } + 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 }] } @@ -474,17 +549,37 @@ final class FavoriteListViewController: UIViewController { let descriptionHeight = OAUtilities.calculateTextBounds(banner.descriptionLabel.text ?? "", width: textWidth, font: banner.descriptionLabel.font).height return ceil(CGFloat(banner.defaultFrameHeight) + titleHeight + descriptionHeight) } - + + private func folderStats(allFolders: [FavoriteFolderRow], currentGroupName: String?) -> FavoriteFolderStats? { + guard !isSearchActive else { return nil } + guard let currentGroupName else { + let pointsCount = allFolders.reduce(0) { $0 + $1.pointsCount } + guard !allFolders.isEmpty || pointsCount > 0 else { return nil } + let fileSize = allFolders.reduce(Int64(0)) { $0 + OAFavoriteFoldersBridge.favoriteGroupSize(forGroupName: $1.groupName) } + return FavoriteFolderStats(foldersCount: allFolders.count, pointsCount: pointsCount, fileSize: fileSize) + } + + let nestedFolders = allFolders.filter { isNestedFolder($0.groupName, in: currentGroupName) } + let groupNames = [currentGroupName] + nestedFolders.map(\.groupName) + let currentPointsCount = allFolders.first { $0.groupName == currentGroupName }?.pointsCount ?? 0 + let pointsCount = currentPointsCount + nestedFolders.reduce(0) { $0 + $1.pointsCount } + guard !nestedFolders.isEmpty || pointsCount > 0 else { return nil } + let fileSize = groupNames.reduce(Int64(0)) { $0 + OAFavoriteFoldersBridge.favoriteGroupSize(forGroupName: $1) } + return FavoriteFolderStats(foldersCount: nestedFolders.count, pointsCount: pointsCount, fileSize: fileSize) + } + private func closeFreeBackupBanner() { UserDefaults.standard.set(true, forKey: Self.wasClosedFreeBackupFavoritesBannerKey) applySnapshot(animatingDifferences: true) } - private func directFavoriteFolders(parentGroupName: String?) -> [FavoriteFolderRow] { + private func directFavoriteFolders(_ folders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoriteFolderRow] { + folders.filter { isDirectFolder($0.groupName, parentGroupName: parentGroupName) }.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } + } + + private func favoriteFolders() -> [FavoriteFolderRow] { OAFavoriteFoldersBridge.favoriteFolders() .map { FavoriteFolderRow(item: $0) } - .filter { isDirectFolder($0.groupName, parentGroupName: parentGroupName) } - .sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } } private func isDirectFolder(_ groupName: String, parentGroupName: String?) -> Bool { @@ -495,6 +590,11 @@ final class FavoriteListViewController: UIViewController { return !childPath.isEmpty && !childPath.contains("/") } + private func isNestedFolder(_ groupName: String, in parentGroupName: String) -> Bool { + guard !parentGroupName.isEmpty else { return false } + return groupName.hasPrefix(parentGroupName + "/") + } + private func matchesSearch(_ text: String?) -> Bool { guard !searchText.isEmpty else { return true } return text?.localizedCaseInsensitiveContains(searchText) ?? false @@ -674,7 +774,7 @@ final class FavoriteListViewController: UIViewController { let indexPath = IndexPath(item: item, section: section) guard let itemIdentifier = dataSource.itemIdentifier(for: indexPath) else { continue } switch itemIdentifier { - case .backupBanner, .header: + case .backupBanner, .header, .statsFooter: continue case .folder, .favorite: collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) @@ -781,7 +881,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { return } OAFavoriteFoldersBridge.openFavoritePoint(withIdentifier: favorite.identifier) - case .backupBanner, .header: + case .backupBanner, .header, .statsFooter: break } diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h index fa131ffb8c..90429b4274 100644 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h @@ -43,6 +43,7 @@ NS_ASSUME_NONNULL_BEGIN + (NSArray *)favoriteFolders; + (NSArray *)favoritePointsForGroupName:(NSString *)groupName; ++ (long long)favoriteGroupSizeForGroupName:(NSString *)groupName; + (void)openFavoritePointWithIdentifier:(NSString *)identifier; + (void)openNewFavoriteGroupEditorWithParentGroupName:(nullable NSString *)parentGroupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion; + (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible; diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm index aa254a0c8c..71d73a1bdc 100644 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm @@ -233,6 +233,13 @@ @implementation OAFavoriteFoldersBridge return items.copy; } ++ (long long)favoriteGroupSizeForGroupName:(NSString *)groupName +{ + NSString *filePath = [OsmAndApp.instance favoritesStorageFilename:groupName ?: @""]; + NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:filePath error:nil]; + return [attributes[NSFileSize] longLongValue]; +} + + (void)openNewFavoriteGroupEditorWithParentGroupName:(nullable NSString *)parentGroupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion { if (!navigationController) From 8aedcf89698c56d48d762980629c2f23cb163066 Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Fri, 5 Jun 2026 18:33:37 +0300 Subject: [PATCH 07/41] Add sort --- OsmAnd.xcodeproj/project.pbxproj | 4 + .../en.lproj/Localizable.strings | 2 + .../MyPlaces/FavoriteListViewController.swift | 190 +++++++++++------- .../MyPlaces/FavoriteSortModeHelper.swift | 157 +++++++++++++++ .../MyPlaces/OAFavoriteFoldersBridge.h | 7 +- .../MyPlaces/OAFavoriteFoldersBridge.mm | 106 +++++----- Sources/Helpers/OAAppSettings.h | 4 + Sources/Helpers/OAAppSettings.m | 19 ++ 8 files changed, 365 insertions(+), 124 deletions(-) create mode 100644 Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index d638c87c62..5a2c40955f 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -1526,6 +1526,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 */; }; @@ -5416,6 +5417,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 = ""; }; @@ -12659,6 +12661,7 @@ DA5A7E0126C563A300F274C7 /* OAFavoriteListViewController.h */, DA5A7E0C26C563A300F274C7 /* OAFavoriteListViewController.mm */, C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */, + C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */, C5EB56EA2FD188A900D01657 /* OAFavoriteFoldersBridge.h */, C5EB56EB2FD188A900D01657 /* OAFavoriteFoldersBridge.mm */, 327D68A42A8F9BA80076D1E0 /* SavedArticlesTabViewController.swift */, @@ -18085,6 +18088,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 */, diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index aedaf514e7..6407c381b5 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"; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 126ee0e62d..f4c2484894 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -7,6 +7,7 @@ // import UIKit +import CoreLocation import UniformTypeIdentifiers private enum ScreenMode { @@ -32,6 +33,7 @@ private enum FavoriteFolderSection: Hashable { } private enum FavoriteListSection: Hashable { + case sortHeader case backupBanner case folderSection(FavoriteFolderSection) case content @@ -39,6 +41,7 @@ private enum FavoriteListSection: Hashable { } private enum FavoriteListItem: Hashable { + case sortHeader(FavoriteSortMode) case backupBanner case header(FavoriteFolderSection) case folder(FavoriteFolderRow) @@ -46,7 +49,7 @@ private enum FavoriteListItem: Hashable { case statsFooter(FavoriteFolderStats) } -private struct FavoriteFolderRow: Hashable { +private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { let identifier: String let groupName: String let title: String @@ -54,6 +57,8 @@ private struct FavoriteFolderRow: Hashable { let isVisible: Bool let isPinned: Bool let color: UIColor? + let lastModified: Date? + let fileSize: Int64 var iconName: String { isVisible ? "ic_custom_folder" : "ic_custom_folder_hidden_outlined" @@ -80,6 +85,8 @@ private struct FavoriteFolderRow: Hashable { isVisible = item.isVisible isPinned = item.isPinned color = item.color + lastModified = item.lastModifiedDate + fileSize = item.fileSize } private static func title(for groupName: String, fallback: String) -> String { @@ -88,10 +95,12 @@ private struct FavoriteFolderRow: Hashable { } } -private struct FavoritePointRow: Hashable { +private struct FavoritePointRow: Hashable, FavoriteSortablePoint { let identifier: String let title: String - let subtitle: String? + let address: String? + let distance: CLLocationDistance? + let timestamp: Date? let icon: UIImage? let isVisible: Bool @@ -107,7 +116,9 @@ private struct FavoritePointRow: Hashable { init(item: OAFavoritePointBridgeItem) { identifier = item.identifier title = item.title - subtitle = item.subtitle + address = item.address + distance = item.distance?.doubleValue + timestamp = item.timestampDate icon = item.icon isVisible = item.isVisible } @@ -140,6 +151,8 @@ final class FavoriteListViewController: UIViewController { private static let imageSize: CGFloat = 30.0 private static let favoriteIconSize: CGFloat = 36.0 + private static let sortHeaderHeight: CGFloat = 44.0 + private static let sortHeaderLeadingInset: CGFloat = 16.0 private static let navigationTitleFontSize: CGFloat = 17.0 private static let navigationTitleMaximumSize: CGFloat = 22.0 private static let navigationSubtitleFontSize: CGFloat = 12.0 @@ -149,12 +162,13 @@ final class FavoriteListViewController: UIViewController { private static let wasClosedFreeBackupFavoritesBannerKey = "wasClosedFreeBackupFavoritesBanner" private let screenMode: ScreenMode - private var layoutSections: [FavoriteListSection] = [] + private let settings = OAAppSettings.sharedManager() + private var layoutSections: [FavoriteListSection] = [] private var searchText = "" private var isSearchActive = false private var isAvailablePaymentBanner: Bool { - isRootFolder && !UserDefaults.standard.bool(forKey: Self.wasClosedFreeBackupFavoritesBannerKey) && !OAIAPHelper.isOsmAndProAvailable() && !OABackupHelper.sharedInstance().isRegistered() + isRootFolder && !isSearchActive && !UserDefaults.standard.bool(forKey: Self.wasClosedFreeBackupFavoritesBannerKey) && !OAIAPHelper.isOsmAndProAvailable() && !OABackupHelper.sharedInstance().isRegistered() } private var isRootFolder: Bool { guard case .root = screenMode else { return false } @@ -180,6 +194,12 @@ final class FavoriteListViewController: UIViewController { guard case .folder(let folder, _) = screenMode, !folder.groupName.isEmpty else { return nil } return folder.groupName } + private var currentSortMode: FavoriteSortMode { + isSearchActive ? searchFavoriteSortMode() : favoriteSortMode() + } + private var currentSortEntryId: String { + parentGroupName ?? "" + } private lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) @@ -202,6 +222,10 @@ final class FavoriteListViewController: UIViewController { cell.accessories = [.outlineDisclosure(options: disclosureOptions)] cell.tintColor = .iconColorActive } + private lazy var sortHeaderCellRegistration = UICollectionView.CellRegistration { [weak self] cell, _, sortMode in + cell.sortButton.setImage(sortMode.image, for: .normal) + cell.sortButton.menu = self?.makeSortMenu() + } private lazy var backupBannerCellRegistration = UICollectionView.CellRegistration { [weak self] cell, _, _ in cell.contentView.subviews.forEach { $0.removeFromSuperview() } guard let self, let banner = Bundle.main.loadNibNamed("FreeBackupBanner", owner: self)?.first as? FreeBackupBanner else { return } @@ -239,7 +263,7 @@ final class FavoriteListViewController: UIViewController { content.text = favorite.title content.textProperties.color = favorite.titleColor content.textProperties.font = favorite.titleFont - content.secondaryText = favorite.subtitle + content.secondaryText = favorite.address content.secondaryTextProperties.color = .textColorSecondary cell.contentConfiguration = content cell.backgroundConfiguration = self?.listCellBackgroundConfiguration() @@ -285,6 +309,11 @@ final class FavoriteListViewController: UIViewController { super.init(coder: coder) } + deinit { + NotificationCenter.default.removeObserver(self, name: .favoriteImportViewControllerDidDismiss, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name(NSNotification.Name.OAIAPProductPurchased.rawValue), object: nil) + } + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .viewBg @@ -294,11 +323,6 @@ final class FavoriteListViewController: UIViewController { NotificationCenter.default.addObserver(self, selector: #selector(productPurchased), name: Notification.Name(NSNotification.Name.OAIAPProductPurchased.rawValue), object: nil) } - deinit { - NotificationCenter.default.removeObserver(self, name: .favoriteImportViewControllerDidDismiss, object: nil) - NotificationCenter.default.removeObserver(self, name: Notification.Name(NSNotification.Name.OAIAPProductPurchased.rawValue), object: nil) - } - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) configureNavigation() @@ -317,15 +341,31 @@ final class FavoriteListViewController: UIViewController { 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() } - configuration.headerMode = self.isRootFolder && section != .backupBanner ? .firstItemInSection : .none + 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) @@ -431,7 +471,48 @@ final class FavoriteListViewController: UIViewController { myPlacesDelegate?.updateSegmentedControlVisibility(isRootFolder && !collectionView.isEditing && !isSearchActive) } + private func favoriteSortMode() -> FavoriteSortMode { + let sortModes = settings.getFavoriteSortModes() + guard let sortModeTitle = sortModes[currentSortEntryId] else { return FavoriteSortModeHelper.defaultSortMode() } + return FavoriteSortMode.byTitle(sortModeTitle) + } + + private func searchFavoriteSortMode() -> FavoriteSortMode { + let sortModeTitle = settings.searchFavoriteSortMode.get() + return FavoriteSortMode.byTitle(sortModeTitle) + } + + private func setFavoriteSortMode(_ sortMode: FavoriteSortMode) { + if isSearchActive { + settings.searchFavoriteSortMode.set(sortMode.title) + } else { + var sortModes = settings.getFavoriteSortModes() + sortModes[currentSortEntryId] = sortMode.title + settings.saveFavoriteSortModes(sortModes) + } + + applySnapshot(animatingDifferences: false) + } + + private func makeSortMenu() -> UIMenu { + let modes: [FavoriteSortMode] = !isRootFolder || isSearchActive ? [.nameAZ, .nameZA, .nearest, .farthest, .newestDateFirst, .oldestDateFirst] : [.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) + } + + private func makeSortAction(for sortMode: FavoriteSortMode) -> UIAction { + UIAction(title: sortMode.title, image: sortMode.image?.resizedMenuImage(), state: currentSortMode == sortMode ? .on : .off) { [weak self] _ in + self?.setFavoriteSortMode(sortMode) + } + } + private func makeDataSource() -> DataSource { + let sortHeaderCellRegistration = sortHeaderCellRegistration let backupBannerCellRegistration = backupBannerCellRegistration let folderCellRegistration = folderCellRegistration let favoriteCellRegistration = favoriteCellRegistration @@ -439,6 +520,8 @@ final class FavoriteListViewController: UIViewController { let statsFooterCellRegistration = statsFooterCellRegistration return DataSource(collectionView: collectionView) { collectionView, indexPath, item in switch item { + case .sortHeader(let sortMode): + return collectionView.dequeueConfiguredReusableCell(using: sortHeaderCellRegistration, for: indexPath, item: sortMode) case .backupBanner: return collectionView.dequeueConfiguredReusableCell(using: backupBannerCellRegistration, for: indexPath, item: item) case .header(let section): @@ -464,16 +547,17 @@ final class FavoriteListViewController: UIViewController { private func applyRootSnapshot(animatingDifferences: Bool) { let allFolders = favoriteFolders() - let foldersBySection = favoriteFoldersBySection(folders: allFolders) + 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 snapshot = Snapshot() - var sections = folderSections.map { FavoriteListSection.folderSection($0) } + var sections: [FavoriteListSection] = [.sortHeader] if isPaymentBannerVisible { - sections.insert(.backupBanner, at: 0) + sections.append(.backupBanner) } + sections.append(contentsOf: folderSections.map { FavoriteListSection.folderSection($0) }) if stats != nil { sections.append(.statsFooter) } @@ -481,6 +565,7 @@ final class FavoriteListViewController: UIViewController { layoutSections = sections collectionView.collectionViewLayout.invalidateLayout() snapshot.appendSections(sections) + snapshot.appendItems([.sortHeader(currentSortMode)], toSection: .sortHeader) if isPaymentBannerVisible { snapshot.appendItems([.backupBanner], toSection: .backupBanner) } @@ -503,13 +588,14 @@ final class FavoriteListViewController: UIViewController { private func applyFolderSnapshot(folder: FavoriteFolderRow, animatingDifferences: Bool) { let allFolders = favoriteFolders() - let folders = directFavoriteFolders(allFolders, parentGroupName: folder.groupName).filter { matchesSearch($0.title) } - let favorites = OAFavoriteFoldersBridge.favoritePoints(forGroupName: folder.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.subtitle) } + let folders = FavoriteSortModeHelper.sortFoldersWithMode(directFavoriteFolders(allFolders, parentGroupName: folder.groupName).filter { matchesSearch($0.title) }, mode: currentSortMode) + let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(OAFavoriteFoldersBridge.favoritePoints(forGroupName: folder.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.address) }, mode: currentSortMode) let stats = folderStats(allFolders: allFolders, currentGroupName: folder.groupName) var snapshot = Snapshot() - layoutSections = stats == nil ? [.content] : [.content, .statsFooter] + layoutSections = stats == nil ? [.sortHeader, .content] : [.sortHeader, .content, .statsFooter] collectionView.collectionViewLayout.invalidateLayout() snapshot.appendSections(layoutSections) + snapshot.appendItems([.sortHeader(currentSortMode)], toSection: .sortHeader) snapshot.appendItems(folders.map(FavoriteListItem.folder), toSection: .content) snapshot.appendItems(favorites.map(FavoriteListItem.favorite), toSection: .content) if let stats { @@ -555,16 +641,16 @@ final class FavoriteListViewController: UIViewController { guard let currentGroupName else { let pointsCount = allFolders.reduce(0) { $0 + $1.pointsCount } guard !allFolders.isEmpty || pointsCount > 0 else { return nil } - let fileSize = allFolders.reduce(Int64(0)) { $0 + OAFavoriteFoldersBridge.favoriteGroupSize(forGroupName: $1.groupName) } + let fileSize = allFolders.reduce(Int64(0)) { $0 + $1.fileSize } return FavoriteFolderStats(foldersCount: allFolders.count, pointsCount: pointsCount, fileSize: fileSize) } let nestedFolders = allFolders.filter { isNestedFolder($0.groupName, in: currentGroupName) } - let groupNames = [currentGroupName] + nestedFolders.map(\.groupName) - let currentPointsCount = allFolders.first { $0.groupName == currentGroupName }?.pointsCount ?? 0 + let currentFolder = allFolders.first { $0.groupName == currentGroupName } + let currentPointsCount = currentFolder?.pointsCount ?? 0 let pointsCount = currentPointsCount + nestedFolders.reduce(0) { $0 + $1.pointsCount } guard !nestedFolders.isEmpty || pointsCount > 0 else { return nil } - let fileSize = groupNames.reduce(Int64(0)) { $0 + OAFavoriteFoldersBridge.favoriteGroupSize(forGroupName: $1) } + let fileSize = (currentFolder?.fileSize ?? 0) + nestedFolders.reduce(Int64(0)) { $0 + $1.fileSize } return FavoriteFolderStats(foldersCount: nestedFolders.count, pointsCount: pointsCount, fileSize: fileSize) } @@ -574,7 +660,7 @@ final class FavoriteListViewController: UIViewController { } private func directFavoriteFolders(_ folders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoriteFolderRow] { - folders.filter { isDirectFolder($0.groupName, parentGroupName: parentGroupName) }.sorted { $0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending } + folders.filter { isDirectFolder($0.groupName, parentGroupName: parentGroupName) } } private func favoriteFolders() -> [FavoriteFolderRow] { @@ -725,37 +811,6 @@ final class FavoriteListViewController: UIViewController { present(alert, animated: true) } - private func shareItems(_ selectedItems: [IndexPath], sourceView: UIView) { - if selectedItems.isEmpty { - 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 - } - - // TODO -// guard let favoritesUrl = OAFavoriteFoldersBridge.shareFavoriteItems(bridgeItems(for: selectedItems)) else { return } -// showActivity( -// [favoritesUrl], -// sourceView: sourceView, -// barButtonItem: nil, -// completionWithItemsHandler: { -// try? FileManager.default.removeItem(at: favoritesUrl) -// } -// ) - } - private func menuImage(_ name: String) -> UIImage? { UIImage(named: name)?.resizedMenuImage() } @@ -774,7 +829,7 @@ final class FavoriteListViewController: UIViewController { let indexPath = IndexPath(item: item, section: section) guard let itemIdentifier = dataSource.itemIdentifier(for: indexPath) else { continue } switch itemIdentifier { - case .backupBanner, .header, .statsFooter: + case .sortHeader, .backupBanner, .header, .statsFooter: continue case .folder, .favorite: collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) @@ -796,14 +851,7 @@ final class FavoriteListViewController: UIViewController { } @objc private func shareButtonClicked(_ sender: Any) { - // TODO -// guard let items = collectionView.indexPathsForSelectedItems else { -// return -// } -// let sourceView = sender as? UIView ?? collectionView -// shareItems(items, sourceView: sourceView) -// setEdit(false) -// applySnapshot() + // Selection export will be connected with selected favorites model. } @objc private func moveButtonClicked(_ sender: Any) { @@ -815,16 +863,11 @@ final class FavoriteListViewController: UIViewController { return } - // TODO - //guard let navigationController else { return } -// OAFavoriteFoldersBridge.openFavoriteItemsMove(bridgeItems(for: selectedItems), navigationController: navigationController) { [weak self] in -// self?.setEdit(false) -// self?.applySnapshot(animatingDifferences: true) -// } + // Selection move will be connected with selected favorites model. } @objc private func actionsButtonClicked(_ sender: Any) { - // TODO + // Selection actions menu will be connected with selected favorites model. } @objc private func deleteButtonClicked(_ sender: Any) { @@ -846,8 +889,7 @@ final class FavoriteListViewController: UIViewController { title: localizedString("shared_string_yes"), style: .default ) { _ in - // TODO - //self?.removeSelectedFavoriteItems() + // Selection delete will be connected with selected favorites model. } let cancelButton = UIAlertAction( @@ -881,7 +923,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { return } OAFavoriteFoldersBridge.openFavoritePoint(withIdentifier: favorite.identifier) - case .backupBanner, .header, .statsFooter: + case .sortHeader, .backupBanner, .header, .statsFooter: break } diff --git a/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift b/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift new file mode 100644 index 0000000000..089fb39707 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift @@ -0,0 +1,157 @@ +// +// 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 timestamp: 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 UIImage.templateImageNamed("ic_custom_sort_near") ?? .icCustomNearby + case .farthest: return UIImage.templateImageNamed("ic_custom_sort_far") ?? .icCustomNearby + case .newestDateFirst: return .icCustomSortDateNewest + case .oldestDateFirst: return .icCustomSortDateOldest + } + } + + 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] { + stableSorted(folders) { compareFolders($0, $1, mode: mode) } + } + + static func sortFavoritePointsWithMode(_ points: [Point], mode: FavoriteSortMode) -> [Point] { + stableSorted(points) { compareFavoritePoints($0, $1, mode: mode) } + } + + 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 lhs.title.localizedCaseInsensitiveCompare(rhs.title) + case .nameZA: + return rhs.title.localizedCaseInsensitiveCompare(lhs.title) + case .nearest, .farthest: + return .orderedSame + } + } + + private static func compareFavoritePoints(_ lhs: Point, _ rhs: Point, mode: FavoriteSortMode) -> ComparisonResult { + switch mode { + case .nameAZ: + return lhs.title.localizedCaseInsensitiveCompare(rhs.title) + case .nameZA: + return rhs.title.localizedCaseInsensitiveCompare(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 .newestDateFirst: + return compareDates(lhs.timestamp, rhs.timestamp, newestFirst: true, lhsTitle: lhs.title, rhsTitle: rhs.title) + case .oldestDateFirst: + return compareDates(lhs.timestamp, rhs.timestamp, newestFirst: false, lhsTitle: lhs.title, rhsTitle: rhs.title) + case .lastModified: + return .orderedSame + } + } + + private static func compareDates(_ lhs: Date?, _ rhs: Date?, newestFirst: Bool, lhsTitle: String, rhsTitle: String) -> ComparisonResult { + switch (lhs, rhs) { + case let (lhs?, rhs?) where lhs != rhs: + return newestFirst ? rhs.compare(lhs) : lhs.compare(rhs) + case (_?, nil): + return .orderedAscending + case (nil, _?): + return .orderedDescending + default: + return lhsTitle.localizedCaseInsensitiveCompare(rhsTitle) + } + } + + private static func compareDistances(_ lhs: CLLocationDistance?, _ rhs: CLLocationDistance?, nearestFirst: Bool, lhsTitle: String, rhsTitle: String) -> ComparisonResult { + switch (lhs, rhs) { + case let (lhs?, rhs?) where lhs != rhs: + if nearestFirst { + return lhs < rhs ? .orderedAscending : .orderedDescending + } + return lhs > rhs ? .orderedAscending : .orderedDescending + case (_?, nil): + return .orderedAscending + case (nil, _?): + return .orderedDescending + default: + return lhsTitle.localizedCaseInsensitiveCompare(rhsTitle) + } + } + + private static func stableSorted(_ elements: [Element], by comparator: (Element, Element) -> ComparisonResult) -> [Element] { + elements.enumerated().sorted { lhs, rhs in + let result = comparator(lhs.element, rhs.element) + return result == .orderedSame ? lhs.offset < rhs.offset : result == .orderedAscending + }.map(\.element) + } +} diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h index 90429b4274..289f63a139 100644 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h @@ -21,6 +21,8 @@ NS_ASSUME_NONNULL_BEGIN @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)init NS_UNAVAILABLE; @@ -31,7 +33,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) NSString *identifier; @property (nonatomic, readonly) NSString *groupName; @property (nonatomic, readonly) NSString *title; -@property (nonatomic, readonly, nullable) NSString *subtitle; +@property (nonatomic, readonly, nullable) NSString *address; +@property (nonatomic, readonly, nullable) NSNumber *distance; +@property (nonatomic, readonly, nullable) NSDate *timestampDate; @property (nonatomic, readonly, nullable) UIImage *icon; @property (nonatomic, readonly) BOOL isVisible; @@ -43,7 +47,6 @@ NS_ASSUME_NONNULL_BEGIN + (NSArray *)favoriteFolders; + (NSArray *)favoritePointsForGroupName:(NSString *)groupName; -+ (long long)favoriteGroupSizeForGroupName:(NSString *)groupName; + (void)openFavoritePointWithIdentifier:(NSString *)identifier; + (void)openNewFavoriteGroupEditorWithParentGroupName:(nullable NSString *)parentGroupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion; + (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible; diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm index 71d73a1bdc..3f044034cc 100644 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm @@ -17,7 +17,6 @@ #import "OALocationServices.h" #import "OAMapPanelViewController.h" #import "OAOpenAddTrackViewController.h" -#import "OAOsmAndFormatter.h" #import "OAPointDescription.h" #import "OARootViewController.h" #import "OASavingTrackHelper.h" @@ -33,13 +32,13 @@ @interface OAFavoriteFolderBridgeItem () -- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index; +- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize; @end @implementation OAFavoriteFolderBridgeItem -- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index +- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize { self = [super init]; if (self) @@ -52,6 +51,8 @@ - (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index _isVisible = group.isVisible; _isPinned = group.isPinned; _color = group.color; + _lastModifiedDate = lastModifiedDate; + _fileSize = fileSize; } return self; @@ -62,9 +63,7 @@ - (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index @interface OAFavoritePointBridgeItem () - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite; -+ (NSString *)subtitleForFavorite:(OAFavoriteItem *)favorite; -+ (NSString *)formattedDistanceForFavorite:(OAFavoriteItem *)favorite; -+ (NSString *)formattedDate:(NSDate *)date; ++ (nullable NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite; @end @@ -73,6 +72,8 @@ @interface OAFavoriteFoldersBridge () + (NSArray *)sortedFavoritePoints:(NSArray *)points; + (NSArray *)sortedFavoritePointsForGroup:(OAFavoriteGroup *)group; + (NSArray *)favoriteGroupsInsideOrEqualToGroupName:(NSString *)groupName; ++ (nullable NSDate *)lastModifiedDateForGroupName:(NSString *)groupName groups:(NSArray *)groups fileAttributesByGroupName:(NSDictionary *> *)fileAttributesByGroupName; ++ (NSDictionary *> *)favoriteStorageAttributesForGroups:(NSArray *)groups; + (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier; + (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName; + (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; @@ -156,7 +157,9 @@ - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite _identifier = [favorite getKey] ?: @""; _groupName = [favorite getCategory] ?: @""; _title = [favorite getDisplayName] ?: @""; - _subtitle = [self.class subtitleForFavorite:favorite]; + _address = [favorite getAddress]; + _distance = [self.class distanceForFavorite:favorite]; + _timestampDate = [favorite getTimestamp]; _icon = [favorite getCompositeIcon]; _isVisible = [favorite isVisible]; } @@ -164,25 +167,7 @@ - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite return self; } -+ (NSString *)subtitleForFavorite:(OAFavoriteItem *)favorite -{ - NSMutableArray *parts = [NSMutableArray array]; - NSString *distance = [self formattedDistanceForFavorite:favorite]; - if (distance.length > 0) - [parts addObject:distance]; - - NSString *address = [favorite getAddress]; - if (address.length > 0) - [parts addObject:address]; - - NSDate *timestamp = [favorite getTimestamp]; - if (timestamp) - [parts addObject:[self formattedDate:timestamp]]; - - return parts.count > 0 ? [parts componentsJoinedByString:@" • "] : nil; -} - -+ (NSString *)formattedDistanceForFavorite:(OAFavoriteItem *)favorite ++ (nullable NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite { CLLocation *location = [OsmAndApp instance].locationServices.lastKnownLocation; if (!location || !favorite.favorite) @@ -192,20 +177,7 @@ + (NSString *)formattedDistanceForFavorite:(OAFavoriteItem *)favorite 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 [OAOsmAndFormatter getFormattedDistance:distance]; -} - -+ (NSString *)formattedDate:(NSDate *)date -{ - static NSDateFormatter *dateFormatter = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - dateFormatter = [[NSDateFormatter alloc] init]; - dateFormatter.dateStyle = NSDateFormatterShortStyle; - dateFormatter.timeStyle = NSDateFormatterNoStyle; - }); - - return [dateFormatter stringFromDate:date]; + return @(distance); } @end @@ -215,9 +187,14 @@ @implementation OAFavoriteFoldersBridge + (NSArray *)favoriteFolders { 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) { - [folders addObject:[[OAFavoriteFolderBridgeItem alloc] initWithGroup:group index:index]]; + NSString *groupName = group.name ?: @""; + NSDictionary *fileAttributes = fileAttributesByGroupName[groupName]; + NSDate *lastModifiedDate = [self lastModifiedDateForGroupName:groupName groups:groups fileAttributesByGroupName:fileAttributesByGroupName]; + long long fileSize = [fileAttributes[NSFileSize] longLongValue]; + [folders addObject:[[OAFavoriteFolderBridgeItem alloc] initWithGroup:group index:index lastModifiedDate:lastModifiedDate fileSize:fileSize]]; }]; return folders.copy; @@ -233,13 +210,6 @@ @implementation OAFavoriteFoldersBridge return items.copy; } -+ (long long)favoriteGroupSizeForGroupName:(NSString *)groupName -{ - NSString *filePath = [OsmAndApp.instance favoritesStorageFilename:groupName ?: @""]; - NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:filePath error:nil]; - return [attributes[NSFileSize] longLongValue]; -} - + (void)openNewFavoriteGroupEditorWithParentGroupName:(nullable NSString *)parentGroupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion { if (!navigationController) @@ -482,6 +452,46 @@ + (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier return [self sortedFavoritePoints:group.points ?: @[]]; } ++ (nullable 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; +} + ++ (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]; diff --git a/Sources/Helpers/OAAppSettings.h b/Sources/Helpers/OAAppSettings.h index 4c21a69772..6188141b5b 100644 --- a/Sources/Helpers/OAAppSettings.h +++ b/Sources/Helpers/OAAppSettings.h @@ -1230,6 +1230,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; @@ -1327,6 +1329,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 2da3cc5181..9c521952c0 100644 --- a/Sources/Helpers/OAAppSettings.m +++ b/Sources/Helpers/OAAppSettings.m @@ -251,6 +251,8 @@ 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"; @@ -6085,6 +6087,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]; @@ -7572,6 +7580,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]; @@ -7594,6 +7607,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]; From 8b5e64fad00de1e6c0e8ed9bfda7ea24576f15e6 Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Fri, 5 Jun 2026 18:43:40 +0300 Subject: [PATCH 08/41] Add image to UIAction --- Sources/Controllers/MyPlaces/FavoriteListViewController.swift | 2 +- Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index f4c2484894..660dae3e82 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -506,7 +506,7 @@ final class FavoriteListViewController: UIViewController { } private func makeSortAction(for sortMode: FavoriteSortMode) -> UIAction { - UIAction(title: sortMode.title, image: sortMode.image?.resizedMenuImage(), state: currentSortMode == sortMode ? .on : .off) { [weak self] _ in + UIAction(title: sortMode.title, image: sortMode.image, state: currentSortMode == sortMode ? .on : .off) { [weak self] _ in self?.setFavoriteSortMode(sortMode) } } diff --git a/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift b/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift index 089fb39707..f6b3cf4467 100644 --- a/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift +++ b/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift @@ -48,8 +48,8 @@ protocol FavoriteSortablePoint { case .lastModified: return .icCustomLastModified case .nameAZ: return .icCustomSortNameAscending case .nameZA: return .icCustomSortNameDescending - case .nearest: return UIImage.templateImageNamed("ic_custom_sort_near") ?? .icCustomNearby - case .farthest: return UIImage.templateImageNamed("ic_custom_sort_far") ?? .icCustomNearby + case .nearest: return .icCustomSortNear + case .farthest: return .icCustomSortFar case .newestDateFirst: return .icCustomSortDateNewest case .oldestDateFirst: return .icCustomSortDateOldest } From 45abeabbfecaa2fc45abeeb096c33b691b1672ec Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Mon, 8 Jun 2026 11:50:00 +0300 Subject: [PATCH 09/41] refactored, new wrapper added --- OsmAnd.xcodeproj/project.pbxproj | 6 + .../en.lproj/Localizable.strings | 1 + .../MyPlaces/FavoriteListViewController.swift | 397 ++++++-- .../MyPlaces/OAFavoriteFoldersBridge.h | 35 +- .../MyPlaces/OAFavoriteFoldersBridge.mm | 72 +- .../MyPlaces/OAFavoritesSwiftHelper.h | 83 ++ .../MyPlaces/OAFavoritesSwiftHelper.mm | 863 ++++++++++++++++++ .../Editors/OAEditGroupViewController.h | 2 +- Sources/OsmAnd Maps-Bridging-Header.h | 4 + 9 files changed, 1272 insertions(+), 191 deletions(-) create mode 100644 Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h create mode 100644 Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index 5a2c40955f..42a5327c4d 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -219,6 +219,7 @@ 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 /* OAFavoritesSwiftHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */; }; 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 */; }; @@ -3727,6 +3728,8 @@ 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 /* OAFavoritesSwiftHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OAFavoritesSwiftHelper.h; sourceTree = ""; }; + 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OAFavoritesSwiftHelper.mm; 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 = ""; }; @@ -12664,6 +12667,8 @@ C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */, C5EB56EA2FD188A900D01657 /* OAFavoriteFoldersBridge.h */, C5EB56EB2FD188A900D01657 /* OAFavoriteFoldersBridge.mm */, + 272AFB972FD2DC42006C2E21 /* OAFavoritesSwiftHelper.h */, + 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */, 327D68A42A8F9BA80076D1E0 /* SavedArticlesTabViewController.swift */, 325CFBEA2B5052E000090DF2 /* TracksViewController.swift */, C50E32812CA3FBDF00EEC41F /* TracksFiltersViewController.swift */, @@ -17641,6 +17646,7 @@ DA5A814D26C563A700F274C7 /* OASearchCategoriesListController.mm in Sources */, DA5A81F726C563A700F274C7 /* OAChoosePlanViewController.mm in Sources */, FAA6505A2ADD42C50020DCEA /* DeviceFactory.swift in Sources */, + 272AFB9B2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm in Sources */, DA5A84F126C563A900F274C7 /* OAMapSource.m in Sources */, FA282AF62C2C456700CC7AC1 /* WeatherNavigationBarView.swift in Sources */, DA5A825526C563A700F274C7 /* OAArrivalAnnouncementViewController.m in Sources */, diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index 6407c381b5..493e7eb4f5 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -4117,6 +4117,7 @@ "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."; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 660dae3e82..02e9326808 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -6,7 +6,6 @@ // Copyright © 2026 OsmAnd. All rights reserved. // -import UIKit import CoreLocation import UniformTypeIdentifiers @@ -50,22 +49,30 @@ private enum FavoriteListItem: Hashable { } private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { - let identifier: String - let groupName: String - let title: String - let pointsCount: Int - let isVisible: Bool - let isPinned: Bool - let color: UIColor? - let lastModified: Date? - let fileSize: Int64 + let bridgeItem: OAFavoriteFolderBridgeItem + var title: String { + bridgeItem.title + } + + var isVisible: Bool { + bridgeItem.isVisible + } + + var isPinned: Bool { + bridgeItem.isPinned + } + + var lastModified: Date? { + bridgeItem.lastModifiedDate + } + var iconName: String { isVisible ? "ic_custom_folder" : "ic_custom_folder_hidden_outlined" } var iconColor: UIColor { - isVisible ? (color ?? .iconColorSelected) : .iconColorSecondary + isVisible ? (bridgeItem.color ?? .iconColorSelected) : .iconColorSecondary } var titleColor: UIColor { @@ -78,15 +85,7 @@ private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { } init(item: OAFavoriteFolderBridgeItem) { - identifier = item.identifier - groupName = item.groupName - title = Self.title(for: item.groupName, fallback: item.title) - pointsCount = Int(item.pointsCount) - isVisible = item.isVisible - isPinned = item.isPinned - color = item.color - lastModified = item.lastModifiedDate - fileSize = item.fileSize + bridgeItem = item } private static func title(for groupName: String, fallback: String) -> String { @@ -96,31 +95,31 @@ private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { } private struct FavoritePointRow: Hashable, FavoriteSortablePoint { - let identifier: String - let title: String - let address: String? - let distance: CLLocationDistance? - let timestamp: Date? - let icon: UIImage? - let isVisible: Bool + let bridgeItem: OAFavoritePointBridgeItem + + var title: String { + bridgeItem.title + } + + var distance: CLLocationDistance? { + bridgeItem.distance?.doubleValue + } + + var timestamp: Date? { + bridgeItem.timestampDate + } var titleColor: UIColor { - isVisible ? .textColorPrimary : .textColorSecondary + bridgeItem.isVisible ? .textColorPrimary : .textColorSecondary } var titleFont: UIFont { - guard !isVisible, let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitItalic) else { return .preferredFont(forTextStyle: .body) } + 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) { - identifier = item.identifier - title = item.title - address = item.address - distance = item.distance?.doubleValue - timestamp = item.timestampDate - icon = item.icon - isVisible = item.isVisible + bridgeItem = item } } @@ -142,6 +141,11 @@ private struct FavoriteFolderStats: Hashable { } } +private enum FavoriteGroupEditContext { + case movingGroup(String) + case movingItems([Any]) +} + final class FavoriteListViewController: UIViewController { private typealias DataSource = UICollectionViewDiffableDataSource private typealias Snapshot = NSDiffableDataSourceSnapshot @@ -163,8 +167,12 @@ final class FavoriteListViewController: UIViewController { private let screenMode: ScreenMode private let settings = OAAppSettings.sharedManager() - private var layoutSections: [FavoriteListSection] = [] + private let appearanceCollection: OAGPXAppearanceCollection = .sharedInstance() + private var groupController: OAEditGroupViewController? + private var groupEditContext: FavoriteGroupEditContext? + private var addToTrackGroupName: String? + private var searchText = "" private var isSearchActive = false private var isAvailablePaymentBanner: Bool { @@ -191,8 +199,8 @@ final class FavoriteListViewController: UIViewController { } } private var parentGroupName: String? { - guard case .folder(let folder, _) = screenMode, !folder.groupName.isEmpty else { return nil } - return folder.groupName + guard case .folder(let folder, _) = screenMode, !folder.bridgeItem.groupName.isEmpty else { return nil } + return folder.bridgeItem.groupName } private var currentSortMode: FavoriteSortMode { isSearchActive ? searchFavoriteSortMode() : favoriteSortMode() @@ -250,7 +258,7 @@ final class FavoriteListViewController: UIViewController { content.text = folder.title content.textProperties.color = folder.titleColor content.textProperties.font = folder.titleFont - content.secondaryText = "\(localizedString("points_count")) \(folder.pointsCount)" + content.secondaryText = "\(localizedString("points_count")) \(folder.bridgeItem.pointsCount)" content.secondaryTextProperties.color = .textColorSecondary cell.contentConfiguration = content cell.backgroundConfiguration = self?.listCellBackgroundConfiguration() @@ -259,11 +267,11 @@ final class FavoriteListViewController: UIViewController { private lazy var favoriteCellRegistration = CellRegistration { [weak self] cell, _, favorite in var content = cell.defaultContentConfiguration() content.directionalLayoutMargins = Self.rowContentInsets - content.image = OAUtilities.resize(favorite.icon, newSize: CGSize(width: Self.favoriteIconSize, height: Self.favoriteIconSize)) + content.image = OAUtilities.resize(favorite.bridgeItem.icon, newSize: CGSize(width: Self.favoriteIconSize, height: Self.favoriteIconSize)) content.text = favorite.title content.textProperties.color = favorite.titleColor content.textProperties.font = favorite.titleFont - content.secondaryText = favorite.address + content.secondaryText = favorite.bridgeItem.address content.secondaryTextProperties.color = .textColorSecondary cell.contentConfiguration = content cell.backgroundConfiguration = self?.listCellBackgroundConfiguration() @@ -432,15 +440,18 @@ final class FavoriteListViewController: UIViewController { } private func configureToolbar() { + 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: .icNavbarOverflowMenuOutlined, style: .plain, target: self, action: #selector(actionsButtonClicked)) + 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 { @@ -588,9 +599,9 @@ final class FavoriteListViewController: UIViewController { private func applyFolderSnapshot(folder: FavoriteFolderRow, animatingDifferences: Bool) { let allFolders = favoriteFolders() - let folders = FavoriteSortModeHelper.sortFoldersWithMode(directFavoriteFolders(allFolders, parentGroupName: folder.groupName).filter { matchesSearch($0.title) }, mode: currentSortMode) - let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(OAFavoriteFoldersBridge.favoritePoints(forGroupName: folder.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.address) }, mode: currentSortMode) - let stats = folderStats(allFolders: allFolders, currentGroupName: folder.groupName) + let folders = FavoriteSortModeHelper.sortFoldersWithMode(directFavoriteFolders(allFolders, parentGroupName: folder.bridgeItem.groupName).filter { matchesSearch($0.title) }, mode: currentSortMode) + let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(OAFavoriteFoldersBridge.favoritePoints(forGroupName: folder.bridgeItem.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.bridgeItem.address) }, mode: currentSortMode) + let stats = folderStats(allFolders: allFolders, currentGroupName: folder.bridgeItem.groupName) var snapshot = Snapshot() layoutSections = stats == nil ? [.sortHeader, .content] : [.sortHeader, .content, .statsFooter] collectionView.collectionViewLayout.invalidateLayout() @@ -639,19 +650,19 @@ final class FavoriteListViewController: UIViewController { private func folderStats(allFolders: [FavoriteFolderRow], currentGroupName: String?) -> FavoriteFolderStats? { guard !isSearchActive else { return nil } guard let currentGroupName else { - let pointsCount = allFolders.reduce(0) { $0 + $1.pointsCount } + 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.fileSize } - return FavoriteFolderStats(foldersCount: allFolders.count, pointsCount: pointsCount, fileSize: fileSize) + let fileSize = allFolders.reduce(Int64(0)) { $0 + OAFavoritesSwiftHelper.favoriteGroupSize(forGroupName: $1.bridgeItem.groupName) } + return FavoriteFolderStats(foldersCount: allFolders.count, pointsCount: Int(pointsCount), fileSize: fileSize) } - let nestedFolders = allFolders.filter { isNestedFolder($0.groupName, in: currentGroupName) } - let currentFolder = allFolders.first { $0.groupName == currentGroupName } - let currentPointsCount = currentFolder?.pointsCount ?? 0 - let pointsCount = currentPointsCount + nestedFolders.reduce(0) { $0 + $1.pointsCount } + let nestedFolders = allFolders.filter { isNestedFolder($0.bridgeItem.groupName, in: currentGroupName) } + let groupNames = [currentGroupName] + nestedFolders.map(\.bridgeItem.groupName) + let currentPointsCount = allFolders.first { $0.bridgeItem.groupName == currentGroupName }?.bridgeItem.pointsCount ?? 0 + let pointsCount = currentPointsCount + nestedFolders.reduce(0) { $0 + $1.bridgeItem.pointsCount } guard !nestedFolders.isEmpty || pointsCount > 0 else { return nil } - let fileSize = (currentFolder?.fileSize ?? 0) + nestedFolders.reduce(Int64(0)) { $0 + $1.fileSize } - return FavoriteFolderStats(foldersCount: nestedFolders.count, pointsCount: pointsCount, fileSize: fileSize) + let fileSize = groupNames.reduce(Int64(0)) { $0 + OAFavoritesSwiftHelper.favoriteGroupSize(forGroupName: $1) } + return FavoriteFolderStats(foldersCount: nestedFolders.count, pointsCount: Int(pointsCount), fileSize: fileSize) } private func closeFreeBackupBanner() { @@ -660,11 +671,11 @@ final class FavoriteListViewController: UIViewController { } private func directFavoriteFolders(_ folders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoriteFolderRow] { - folders.filter { isDirectFolder($0.groupName, parentGroupName: parentGroupName) } + folders.filter { isDirectFolder($0.bridgeItem.groupName, parentGroupName: parentGroupName) } } private func favoriteFolders() -> [FavoriteFolderRow] { - OAFavoriteFoldersBridge.favoriteFolders() + OAFavoritesSwiftHelper.favoriteFolders() .map { FavoriteFolderRow(item: $0) } } @@ -686,12 +697,53 @@ final class FavoriteListViewController: UIViewController { return text?.localizedCaseInsensitiveContains(searchText) ?? false } + private func openNewFavoriteGroupEditor() { + guard let navigationController, let viewController = OAFavoriteGroupEditorViewController(new: ()) else { return } + viewController.delegate = self + let modalNavigationController = UINavigationController(rootViewController: viewController) + navigationController.present(modalNavigationController, animated: true) + } + + private func openFavoriteGroupAppearance(_ groupName: String) { + guard let viewController = OAFavoriteGroupEditorViewController(group: OAFavoritesSwiftHelper.pointsGroup(forGroupName: groupName)) else { return } + viewController.delegate = self + navigationController?.pushViewController(viewController, animated: true) + } + + private func openFavoriteGroupMove(_ groupName: String) { + let groupNames = OAFavoritesSwiftHelper.favoriteGroupsToMove(forGroupName: groupName) + guard let navigationController, let groupController = OAEditGroupViewController(groupName: nil, groups: groupNames) else { return } + self.groupController = groupController + groupEditContext = .movingGroup(groupName) + groupController.delegate = self + let modalNavigationController = UINavigationController(rootViewController: groupController) + navigationController.present(modalNavigationController, animated: true) + } + + private func openFavoriteItemsMove(_ favoriteItems: [Any]) { + guard !favoriteItems.isEmpty, + let navigationController, + let groupController = OAEditGroupViewController(groupName: nil, groups: OAFavoritesSwiftHelper.favoriteGroupNames(forMovingFavoriteItems: favoriteItems)) else { + return + } + self.groupController = groupController + groupEditContext = .movingItems(favoriteItems) + groupController.delegate = self + let modalNavigationController = UINavigationController(rootViewController: groupController) + navigationController.present(modalNavigationController, animated: true) + } + + private func openFavoriteGroupAddToTrack(_ groupName: String) { + guard OAFavoritesSwiftHelper.canUseGroup(withName: groupName), let navigationController, let viewController = OAOpenAddTrackViewController(screenType: .addToATrack) else { return } + addToTrackGroupName = groupName + viewController.delegate = self + let modalNavigationController = UINavigationController(rootViewController: viewController) + navigationController.present(modalNavigationController, animated: true) + } + private func makeActionsMenu() -> UIMenu { let addFolderAction = UIAction(title: localizedString("add_new_folder"), image: .icCustomFolderAddOutlined.resizedMenuImage()) { [weak self] _ in - guard let self, let navigationController = self.navigationController else { return } - OAFavoriteFoldersBridge.openNewFavoriteGroupEditor(withParentGroupName: self.parentGroupName, navigationController: navigationController) { [weak self] in - self?.applySnapshot(animatingDifferences: true) - } + self?.openNewFavoriteGroupEditor() } let importAction = UIAction(title: localizedString("shared_string_import"), image: .icCustomImportOutlined.resizedMenuImage()) { [weak self] _ in guard let self else { return } @@ -721,11 +773,11 @@ final class FavoriteListViewController: UIViewController { private func makeFolderContextMenu(for folder: FavoriteFolderRow, indexPath: IndexPath) -> UIMenu { let showHideAction = UIAction(title: localizedString(folder.isVisible ? "shared_string_hide_from_map" : "shared_string_show_on_map"), image: menuImage(folder.isVisible ? "ic_custom_hide_outlined" : "ic_custom_show_outlined")) { [weak self] _ in - OAFavoriteFoldersBridge.setFavoriteGroupVisible(folder.groupName, visible: !folder.isVisible) + OAFavoritesSwiftHelper.setFavoriteGroupVisible(folder.bridgeItem.groupName, visible: !folder.isVisible) self?.applySnapshot(animatingDifferences: true) } let pinAction = UIAction(title: localizedString(folder.isPinned ? "unpin_folder" : "pin_folder"), image: menuImage("ic_custom_map_pin_outlined")) { [weak self] _ in - OAFavoriteFoldersBridge.setFavoriteGroupPinned(folder.groupName, pinned: !folder.isPinned) + OAFavoritesSwiftHelper.setFavoriteGroupPinned(folder.bridgeItem.groupName, pinned: !folder.isPinned) self?.applySnapshot(animatingDifferences: true) } let firstButtonsSection = UIMenu(title: "", options: .displayInline, children: [showHideAction, pinAction]) @@ -734,33 +786,36 @@ final class FavoriteListViewController: UIViewController { self?.showRenameAlert(for: folder) } let defaultAppearanceAction = UIAction(title: localizedString("default_appearance"), image: menuImage("ic_custom_appearance_outlined")) { [weak self] _ in - guard let navigationController = self?.navigationController else { return } - OAFavoriteFoldersBridge.openFavoriteGroupAppearance(folder.groupName, navigationController: navigationController) + self?.openFavoriteGroupAppearance(folder.bridgeItem.groupName) } let secondButtonsSection = UIMenu(title: "", options: .displayInline, children: [renameAction, defaultAppearanceAction]) let shareAction = UIAction(title: localizedString("shared_string_share"), image: menuImage("ic_custom_export_outlined")) { [weak self] _ in guard let self else { return } let sourceView: UIView = self.collectionView.cellForItem(at: indexPath) ?? self.collectionView - OAFavoriteFoldersBridge.shareFavoriteGroup(folder.groupName, sourceView: sourceView, viewController: self) + guard let favoritesUrl = OAFavoritesSwiftHelper.shareFavoriteGroupName(folder.bridgeItem.groupName) else { return } + showActivity( + [favoritesUrl], + sourceView: sourceView, + barButtonItem: nil, + completionWithItemsHandler: { + try? FileManager.default.removeItem(at: favoritesUrl) + } + ) } let moveAction = UIAction(title: localizedString("shared_string_move"), image: menuImage("ic_custom_folder_move_outlined")) { [weak self] _ in - guard let navigationController = self?.navigationController else { return } - OAFavoriteFoldersBridge.openFavoriteGroupMove(folder.groupName, navigationController: navigationController) { [weak self] in - self?.applySnapshot(animatingDifferences: true) - } + self?.openFavoriteGroupMove(folder.bridgeItem.groupName) } - let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: folder.groupName.isEmpty ? [shareAction] : [shareAction, moveAction]) + let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: folder.bridgeItem.groupName.isEmpty ? [shareAction] : [shareAction, moveAction]) let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: menuImage("ic_custom_map_pin_outlined")) { _ in - OAFavoriteFoldersBridge.addFavoriteGroup(toMapMarkers: folder.groupName) + OAFavoritesSwiftHelper.addFavoriteGroup(toMapMarkers: folder.bridgeItem.groupName) } let trackAction = UIAction(title: localizedString("add_to_a_track"), image: menuImage("ic_custom_trip")) { [weak self] _ in - guard let navigationController = self?.navigationController else { return } - OAFavoriteFoldersBridge.openFavoriteGroupAdd(toTrack: folder.groupName, navigationController: navigationController) + self?.openFavoriteGroupAddToTrack(folder.bridgeItem.groupName) } let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: menuImage("ic_custom_navigation_outlined")) { _ in - OAFavoriteFoldersBridge.addFavoriteGroup(toNavigation: folder.groupName) + OAFavoritesSwiftHelper.addFavoriteGroup(toNavigation: folder.bridgeItem.groupName) } let addToMenu = UIMenu(title: localizedString("shared_string_add"), image: menuImage("ic_custom_add"), children: [mapMarkersAction, trackAction, navigationAction]) let fourthButtonsSection = UIMenu(title: "", options: .displayInline, children: [addToMenu]) @@ -777,8 +832,8 @@ final class FavoriteListViewController: UIViewController { 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 text = alert?.textFields?.first?.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return } - let newGroupName = self?.groupName(folder.groupName, replacingLastComponentWith: text) ?? text - OAFavoriteFoldersBridge.renameFavoriteGroup(folder.groupName, newName: newGroupName) + let newGroupName = self?.groupName(folder.bridgeItem.groupName, replacingLastComponentWith: text) ?? text + OAFavoritesSwiftHelper.renameFavoriteGroup(folder.bridgeItem.groupName, newName: newGroupName) self?.applySnapshot(animatingDifferences: true) } @@ -803,7 +858,7 @@ final class FavoriteListViewController: UIViewController { private func showDeleteAlert(for folder: FavoriteFolderRow) { let alert = UIAlertController(title: nil, message: localizedString("fav_remove_q"), preferredStyle: .alert) alert.addAction(UIAlertAction(title: localizedString("shared_string_yes"), style: .destructive) { [weak self] _ in - OAFavoriteFoldersBridge.deleteFavoriteGroup(folder.groupName) + OAFavoritesSwiftHelper.deleteFavoriteGroup(folder.bridgeItem.groupName) self?.applySnapshot(animatingDifferences: true) }) @@ -811,10 +866,101 @@ final class FavoriteListViewController: UIViewController { present(alert, animated: true) } + 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 = OAFavoritesSwiftHelper.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 ?? [] + OAFavoritesSwiftHelper.deleteFavoriteItems(bridgeItems(for: selectedIndexPaths)) + setEdit(false) + applySnapshot(animatingDifferences: true) + } + + private 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 + case .backupBanner, .header, .statsFooter, .sortHeader: + return nil + } + } + } + private func menuImage(_ name: String) -> UIImage? { UIImage(named: name)?.resizedMenuImage() } + private func makeAdditionalContextMenu() -> UIMenu { + var menuElements: [UIMenuElement] = [] + let hasPoints = collectionView.indexPathsForSelectedItems?.contains { + guard case .favorite = dataSource.itemIdentifier(for: $0) else { return false } + return true + } ?? false + + let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: menuImage("ic_custom_marker")) { _ in + // TODO + } + let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: menuImage("ic_custom_trip")) { _ in + // TODO + } + let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: menuImage("ic_custom_navigation_outlined")) { _ in + // TODO + } + let addToMenu = UIMenu(title: localizedString("add_to"), image: menuImage("ic_custom_add"), children: [trackAction, navigationAction, mapMarkersAction]) + let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: [addToMenu]) + menuElements.append(thirdButtonsSection) + + let changeAppearanceAction = UIAction(title: localizedString("change_appearance"), image: menuImage("ic_custom_appearance_outlined")) { _ in + // TODO + } + let secondButtonsSection = UIMenu(title: "", options: .displayInline, children: [changeAppearanceAction]) + menuElements.append(secondButtonsSection) + + if !hasPoints { + let showHideAction = UIAction(title: localizedString("shared_string_hide_from_map"), image: menuImage("ic_custom_hide_outlined")) { _ in + // TODO + } + let pinAction = UIAction(title: localizedString("pin_folder"), image: menuImage("ic_custom_map_pin_outlined")) { _ in + // TODO + } + let firstButtonsSection = UIMenu(title: "", options: .displayInline, children: [pinAction, showHideAction]) + menuElements.append(firstButtonsSection) + } + + return UIMenu(title: "", children: menuElements) + } + @objc private func selectButtonPressed() { setEdit(true) } @@ -851,7 +997,10 @@ final class FavoriteListViewController: UIViewController { } @objc private func shareButtonClicked(_ sender: Any) { - // Selection export will be connected with selected favorites model. + let sourceView = sender as? UIView ?? collectionView + shareItems(for: sourceView) + setEdit(false) + applySnapshot() } @objc private func moveButtonClicked(_ sender: Any) { @@ -863,11 +1012,7 @@ final class FavoriteListViewController: UIViewController { return } - // Selection move will be connected with selected favorites model. - } - - @objc private func actionsButtonClicked(_ sender: Any) { - // Selection actions menu will be connected with selected favorites model. + openFavoriteItemsMove(bridgeItems(for: selectedItems)) } @objc private func deleteButtonClicked(_ sender: Any) { @@ -888,8 +1033,8 @@ final class FavoriteListViewController: UIViewController { let yesButton = UIAlertAction( title: localizedString("shared_string_yes"), style: .default - ) { _ in - // Selection delete will be connected with selected favorites model. + ) { [weak self] _ in + self?.removeSelectedFavoriteItems() } let cancelButton = UIAlertAction( @@ -912,6 +1057,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { case .folder(let folder): if collectionView.isEditing { updateNavigationBarTitle() + configureToolbar() return } let viewController = FavoriteListViewController(frame: view.bounds, screenMode: .folder(folder, previousTitle: normalTitle)) @@ -920,9 +1066,10 @@ extension FavoriteListViewController: UICollectionViewDelegate { case .favorite(let favorite): if collectionView.isEditing { updateNavigationBarTitle() + configureToolbar() return } - OAFavoriteFoldersBridge.openFavoritePoint(withIdentifier: favorite.identifier) + OAFavoriteFoldersBridge.openFavoritePoint(withIdentifier: favorite.bridgeItem.identifier) case .sortHeader, .backupBanner, .header, .statsFooter: break } @@ -933,6 +1080,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { guard collectionView.isEditing else { return } updateNavigationBarTitle() + configureToolbar() } func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { @@ -966,6 +1114,81 @@ extension FavoriteListViewController: MyPlacesSearchable, UISearchResultsUpdatin } } +extension FavoriteListViewController: OAEditGroupViewControllerDelegate { + func groupChanged() { + guard let groupController else { return } + defer { + self.groupController = nil + groupEditContext = nil + } + + guard groupController.saveChanges else { return } + + let targetGroupName = groupController.groupName ?? "" + switch groupEditContext { + case .movingGroup(let groupName): + OAFavoritesSwiftHelper.moveFavoriteGroup(groupName, toGroupName: targetGroupName) + case .movingItems(let favoriteItems): + OAFavoritesSwiftHelper.moveFavoriteItems(favoriteItems, toGroupName: targetGroupName) + case .none: + return + } + setEdit(false) + applySnapshot(animatingDifferences: true) + } +} + +extension FavoriteListViewController: OAOpenAddTrackDelegate { + func onFileSelected(_ gpxFilePath: String) { + guard let addToTrackGroupName else { return } + OAFavoritesSwiftHelper.addFavoriteGroup(toTrack: addToTrackGroupName, gpxFileName: gpxFilePath) + self.addToTrackGroupName = nil + } +} + +extension FavoriteListViewController: OAEditorDelegate { + func addNewItem(withName name: String?, iconName: String, color: UIColor, backgroundIconName: String) { + guard OAFavoritesSwiftHelper.addFavoriteGroup(name ?? "", + parentGroupName: parentGroupName, + iconName: iconName, + color: color, + backgroundIconName: backgroundIconName) else { return } + applySnapshot(animatingDifferences: true) + } + + func onEditorUpdated() { + 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: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let url = urls.first else { return } diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h index 289f63a139..dbcde7edd3 100644 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h @@ -10,38 +10,7 @@ NS_ASSUME_NONNULL_BEGIN -@class UIColor, UIImage, UINavigationController, UIView, UIViewController; - -@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) 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)init NS_UNAVAILABLE; - -@end - -@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, nullable) NSNumber *distance; -@property (nonatomic, readonly, nullable) NSDate *timestampDate; -@property (nonatomic, readonly, nullable) UIImage *icon; -@property (nonatomic, readonly) BOOL isVisible; - -- (instancetype)init NS_UNAVAILABLE; - -@end +@class UIColor, UIImage, UINavigationController, UIView, UIViewController, OAFavoritePointBridgeItem, OAFavoriteFolderBridgeItem; @interface OAFavoriteFoldersBridge : NSObject @@ -59,6 +28,8 @@ NS_ASSUME_NONNULL_BEGIN + (void)addFavoriteGroupToMapMarkers:(NSString *)groupName; + (void)openFavoriteGroupAddToTrack:(NSString *)groupName navigationController:(UINavigationController *)navigationController; + (void)addFavoriteGroupToNavigation:(NSString *)groupName; ++ (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; ++ (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(NSString *)gpxFileName; @end diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm index 3f044034cc..3187d89599 100644 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm @@ -27,45 +27,10 @@ #import "OsmAndSharedWrapper.h" #import #import +#import "OAFavoritesSwiftHelper.h" #include -@interface OAFavoriteFolderBridgeItem () - -- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize; - -@end - -@implementation OAFavoriteFolderBridgeItem - -- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize -{ - self = [super init]; - if (self) - { - NSString *groupName = group.name ?: @""; - _identifier = [NSString stringWithFormat:@"%@-%lu", groupName, (unsigned long)index]; - _groupName = groupName; - _title = [OAFavoriteGroup getDisplayName:groupName] ?: groupName; - _pointsCount = group.points.count; - _isVisible = group.isVisible; - _isPinned = group.isPinned; - _color = group.color; - _lastModifiedDate = lastModifiedDate; - _fileSize = fileSize; - } - - return self; -} - -@end - -@interface OAFavoritePointBridgeItem () - -- (instancetype)initWithFavorite:(OAFavoriteItem *)favorite; -+ (nullable NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite; - -@end @interface OAFavoriteFoldersBridge () @@ -147,41 +112,6 @@ - (void)addNewItemWithName:(NSString *)name iconName:(NSString *)iconName color: @end -@implementation OAFavoritePointBridgeItem - -- (instancetype)initWithFavorite:(OAFavoriteItem *)favorite -{ - self = [super init]; - if (self) - { - _identifier = [favorite getKey] ?: @""; - _groupName = [favorite getCategory] ?: @""; - _title = [favorite getDisplayName] ?: @""; - _address = [favorite getAddress]; - _distance = [self.class distanceForFavorite:favorite]; - _timestampDate = [favorite getTimestamp]; - _icon = [favorite getCompositeIcon]; - _isVisible = [favorite isVisible]; - } - - return self; -} - -+ (nullable 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); -} - -@end - @implementation OAFavoriteFoldersBridge + (NSArray *)favoriteFolders diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h new file mode 100644 index 0000000000..dfadeb2345 --- /dev/null +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h @@ -0,0 +1,83 @@ +// +// OAFavoritesSwiftHelper.h +// OsmAnd +// +// Created by Vladyslav Lysenko on 05.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@class OAEditGroupViewController, OAFavoriteGroupEditorViewController, OAOpenAddTrackViewController, UIColor, UIImage, OAFavoriteGroup, OAFavoriteItem, OASGpxUtilitiesPointsGroup; + +@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) 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; + +@end + +@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, nullable) NSNumber *distance; +@property (nonatomic, readonly, nullable) NSDate *timestampDate; +@property (nonatomic, readonly, nullable) UIImage *icon; +@property (nonatomic, readonly) BOOL isVisible; + +- (instancetype)initWithFavorite:(OAFavoriteItem *)favorite; ++ (nullable NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite; + +@end + +@interface OAFavoritesSwiftHelper : NSObject + ++ (NSArray *)favoriteFolders; ++ (NSArray *)favoritePointsForGroupName:(NSString *)groupName; ++ (long long)favoriteGroupSizeForGroupName:(NSString *)groupName; + ++ (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible; ++ (void)setFavoriteGroupPinned:(NSString *)groupName 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; ++ (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; ++ (void)moveFavoriteItems:(NSArray *)favoriteItems toGroupName:(NSString *)targetGroupName; ++ (NSArray *)favoriteGroupNamesForMovingFavoriteItems:(NSArray *)favoriteItems; + ++ (OASGpxUtilitiesPointsGroup *)pointsGroupForGroupName:(NSString *)groupName; ++ (NSArray *)favoriteGroupsToMoveForGroupName:(NSString *)groupName; ++ (BOOL)canUseGroupWithName:(NSString *)groupName; + ++ (nullable NSURL *)shareFavoriteGroupName:(NSString *)groupName; ++ (nullable NSURL *)shareFavoriteItems:(NSArray *)favoriteItems; + ++ (BOOL)deleteFavoriteGroup:(NSString *)groupName; ++ (BOOL)deleteFavoriteItems:(NSArray *)favoriteItems; + ++ (void)openFavoritePointWithIdentifier:(NSString *)identifier; ++ (void)addFavoriteGroupToMapMarkers:(NSString *)groupName; ++ (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(nullable NSString *)gpxFileName; ++ (void)addFavoriteGroupToNavigation:(NSString *)groupName; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm new file mode 100644 index 0000000000..db67594062 --- /dev/null +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm @@ -0,0 +1,863 @@ +// +// OAFavoritesSwiftHelper.mm +// OsmAnd Maps +// +// Created by Vladyslav Lysenko on 05.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +#import "OAFavoritesSwiftHelper.h" +#import "OAAppSettings.h" +#import "OAEditGroupViewController.h" +#import "OAFavoriteItem.h" +#import "OAFavoriteGroupEditorViewController.h" +#import "OAFavoritesHelper.h" +#import "OAGPXDatabase.h" +#import "OAIndexConstants.h" +#import "OALocationServices.h" +#import "OAMapPanelViewController.h" +#import "OAOpenAddTrackViewController.h" +#import "OAOsmAndFormatter.h" +#import "OAPointDescription.h" +#import "OARootViewController.h" +#import "OASavingTrackHelper.h" +#import "OASelectedGPXHelper.h" +#import "OATargetPointsHelper.h" +#import "OAUtilities.h" +#import "OsmAndApp.h" +#import "OsmAndSharedWrapper.h" +#import + +#include + +@implementation OAFavoriteFolderBridgeItem + +- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize +{ + self = [super init]; + if (self) + { + NSString *groupName = group.name ?: @""; + _identifier = [NSString stringWithFormat:@"%@-%lu", groupName, (unsigned long)index]; + _groupName = groupName; + _title = [OAFavoriteGroup getDisplayName:groupName] ?: groupName; + _pointsCount = group.points.count; + _isVisible = group.isVisible; + _isPinned = group.isPinned; + _color = group.color; + _lastModifiedDate = lastModifiedDate; + _fileSize = fileSize; + } + + return self; +} + +@end + +@interface OAFavoritePointBridgeItem () + ++ (NSString *)subtitleForFavorite:(OAFavoriteItem *)favorite; ++ (NSString *)formattedDistanceForFavorite:(OAFavoriteItem *)favorite; ++ (NSString *)formattedDate:(NSDate *)date; + +@end + +@implementation OAFavoritePointBridgeItem + +- (instancetype)initWithFavorite:(OAFavoriteItem *)favorite +{ + self = [super init]; + if (self) + { + _identifier = [favorite getKey] ?: @""; + _groupName = [favorite getCategory] ?: @""; + _title = [favorite getDisplayName] ?: @""; + _address = [favorite getAddress]; + _distance = [self.class distanceForFavorite:favorite]; + _timestampDate = [favorite getTimestamp]; + _icon = [favorite getCompositeIcon]; + _isVisible = [favorite isVisible]; + } + + return self; +} + ++ (nullable 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); +} + ++ (NSString *)subtitleForFavorite:(OAFavoriteItem *)favorite +{ + NSMutableArray *parts = [NSMutableArray array]; + NSString *distance = [self formattedDistanceForFavorite:favorite]; + if (distance.length > 0) + [parts addObject:distance]; + + NSString *address = [favorite getAddress]; + if (address.length > 0) + [parts addObject:address]; + + NSDate *timestamp = [favorite getTimestamp]; + if (timestamp) + [parts addObject:[self formattedDate:timestamp]]; + + return parts.count > 0 ? [parts componentsJoinedByString:@" • "] : nil; +} + ++ (NSString *)formattedDistanceForFavorite:(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 [OAOsmAndFormatter getFormattedDistance:distance]; +} + ++ (NSString *)formattedDate:(NSDate *)date +{ + static NSDateFormatter *dateFormatter = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + dateFormatter = [[NSDateFormatter alloc] init]; + dateFormatter.dateStyle = NSDateFormatterShortStyle; + dateFormatter.timeStyle = NSDateFormatterNoStyle; + }); + + return [dateFormatter stringFromDate:date]; +} + +@end + +@interface OAFavoritesSwiftHelper () + ++ (NSArray *)sortedFavoritePoints:(NSArray *)points; ++ (NSArray *)sortedFavoritePointsForGroup:(OAFavoriteGroup *)group; ++ (NSArray *)favoriteGroupsInsideOrEqualToGroupName:(NSString *)groupName; ++ (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier; ++ (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName; ++ (BOOL)renameFavoriteGroupTreeFromGroupName:(NSString *)sourceGroupName toGroupName:(NSString *)targetGroupName; ++ (BOOL)isGroupName:(NSString *)groupName insideOrEqualToGroupName:(NSString *)parentGroupName; ++ (NSString *)groupNameByMovingGroupName:(NSString *)groupName toParentGroupName:(NSString *)parentGroupName; ++ (NSString *)suffixForGroupName:(NSString *)groupName parentGroupName:(NSString *)parentGroupName; ++ (NSString *)lastComponentForGroupName:(NSString *)groupName; ++ (OAFavoriteGroup *)favoriteGroupForSharingGroup:(OAFavoriteGroup *)group points:(NSArray *)points; ++ (nullable NSURL *)fileURLForSharingFavoriteGroups:(NSArray *)favoriteGroups; ++ (CLLocation *)locationForFavorite:(OAFavoriteItem *)favorite; + +@end + +@implementation OAFavoritesSwiftHelper + ++ (NSArray *)favoriteFolders +{ + 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]; + [folders addObject:[[OAFavoriteFolderBridgeItem alloc] initWithGroup:group index:index lastModifiedDate:lastModifiedDate fileSize:fileSize]]; + }]; + + return [folders copy]; +} + ++ (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; +} + ++ (long long)favoriteGroupSizeForGroupName:(NSString *)groupName +{ + NSString *filePath = [OsmAndApp.instance favoritesStorageFilename:groupName ?: @""]; + NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:filePath error:nil]; + return [attributes[NSFileSize] longLongValue]; +} + ++ (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return; + + [OAFavoritesHelper updateGroup:group visible:visible saveImmediately:YES]; +} + ++ (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return; + + [OAFavoritesHelper updateGroup:group pinned:pinned saveImmediately:YES]; +} + ++ (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]; + 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]; + } +} + ++ (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]; +} + ++ (OASGpxUtilitiesPointsGroup *)pointsGroupForGroupName:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return nil; + return group ? [group toPointsGroup] : nil; +} + ++ (NSArray *)favoriteGroupsToMoveForGroupName:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return nil; + + NSMutableArray *groupNames = [NSMutableArray array]; + for (OAFavoriteGroup *favoriteGroup in [OAFavoritesHelper getFavoriteGroups]) + { + NSString *favoriteGroupName = favoriteGroup.name ?: @""; + if (![self isGroupName:favoriteGroupName insideOrEqualToGroupName:group.name ?: @""]) + [groupNames addObject:favoriteGroupName]; + } + + if (![groupNames containsObject:@""]) + [groupNames addObject:@""]; + + return [groupNames copy]; +} + ++ (BOOL)canUseGroupWithName:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + return group && group.points.count > 0; +} + ++ (nullable NSURL *)shareFavoriteGroupName:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return nil; + + OAFavoriteGroup *groupToShare = [self favoriteGroupForSharingGroup:group points:group.points.copy]; + return [self fileURLForSharingFavoriteGroups:@[groupToShare]]; +} + ++ (nullable 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; + + return [OAFavoritesHelper deleteFavoriteGroups:groupsToDelete andFavoritesItems:nil]; +} + ++ (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; + + return [OAFavoritesHelper deleteFavoriteGroups:groupsToDelete.count > 0 ? groupsToDelete : nil + andFavoritesItems:itemsToDelete.count > 0 ? itemsToDelete : nil]; +} + ++ (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]; +} + ++ (void)addFavoriteGroupToMapMarkers:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return; + + OAMapPanelViewController *mapPanel = [OARootViewController instance].mapPanel; + for (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) + { + 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 sortedFavoritePointsForGroup:[self favoriteGroupWithName: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:[OAUtilities getGpxShortPath:dataItem.file.absolutePath]]; +} + ++ (void)addFavoriteGroupToNavigation:(NSString *)groupName +{ + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + if (!group) + return; + + NSArray *points = [self sortedFavoritePointsForGroup:group]; + if (points.count == 0) + return; + + OATargetPointsHelper *targetPointsHelper = OATargetPointsHelper.sharedInstance; + [targetPointsHelper clearAllPoints:NO]; + for (NSUInteger index = 0; index < points.count; index++) + { + OAFavoriteItem *favorite = points[index]; + CLLocation *location = [self locationForFavorite:favorite]; + if (!location) + continue; + + OAPointDescription *description = [[OAPointDescription alloc] initWithType:POINT_TYPE_FAVORITE name:[favorite getDisplayName]]; + BOOL isDestination = index == points.count - 1; + [targetPointsHelper navigateToPoint:location updateRoute:isDestination intermediate:isDestination ? -1 : (int)index historyName:description]; + } + + OARootViewController *rootViewController = [OARootViewController instance]; + [rootViewController.navigationController popToRootViewControllerAnimated:YES]; + [rootViewController.mapPanel showRouteInfo]; +} + ++ (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 ?: @[]]; +} + ++ (nullable 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; +} + ++ (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]; + + 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; +} + ++ (nullable 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]]; +} + +@end 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/OsmAnd Maps-Bridging-Header.h b/Sources/OsmAnd Maps-Bridging-Header.h index 0af6a734e9..8fa2989136 100644 --- a/Sources/OsmAnd Maps-Bridging-Header.h +++ b/Sources/OsmAnd Maps-Bridging-Header.h @@ -63,6 +63,10 @@ #import "OACommonTypes.h" #import "OABaseCollectionHandler.h" #import "OAResourcesUISwiftHelper.h" +#import "OAEditGroupViewController.h" +#import "OAFavoriteGroupEditorViewController.h" +#import "OAFavoritesSwiftHelper.h" +#import "OAOpenAddTrackViewController.h" #import "OATravelGuidesHelper.h" #import "OAGPXDocumentAdapter.h" #import "OATravelLocalDataDbHelper.h" From e0db552476a1cb96a9c42994facfdbd800d86f38 Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Mon, 8 Jun 2026 12:13:02 +0300 Subject: [PATCH 10/41] fix in FavoriteSortModeHelper and conflicts --- .../MyPlaces/FavoriteListViewController.swift | 4 +- .../MyPlaces/FavoriteSortModeHelper.swift | 51 ++++++++----------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 02e9326808..501b2a3e6c 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -105,7 +105,7 @@ private struct FavoritePointRow: Hashable, FavoriteSortablePoint { bridgeItem.distance?.doubleValue } - var timestamp: Date? { + var lastModified: Date? { bridgeItem.timestampDate } @@ -506,7 +506,7 @@ final class FavoriteListViewController: UIViewController { } private func makeSortMenu() -> UIMenu { - let modes: [FavoriteSortMode] = !isRootFolder || isSearchActive ? [.nameAZ, .nameZA, .nearest, .farthest, .newestDateFirst, .oldestDateFirst] : [.lastModified, .nameAZ, .nameZA, .newestDateFirst, .oldestDateFirst] + let modes: [FavoriteSortMode] = isRootFolder && !isSearchActive ? [.lastModified, .nameAZ, .nameZA, .newestDateFirst, .oldestDateFirst] : FavoriteSortMode.allCases 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) } diff --git a/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift b/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift index f6b3cf4467..3d682b84d2 100644 --- a/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift +++ b/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift @@ -19,7 +19,7 @@ protocol FavoriteSortableFolder { protocol FavoriteSortablePoint { var title: String { get } var distance: CLLocationDistance? { get } - var timestamp: Date? { get } + var lastModified: Date? { get } } @objc enum FavoriteSortMode: Int, CaseIterable { @@ -62,11 +62,11 @@ protocol FavoriteSortablePoint { @objc final class FavoriteSortModeHelper: NSObject { static func sortFoldersWithMode(_ folders: [Folder], mode: FavoriteSortMode) -> [Folder] { - stableSorted(folders) { compareFolders($0, $1, mode: mode) } + folders.sorted { compareFolders($0, $1, mode: mode) == .orderedAscending } } static func sortFavoritePointsWithMode(_ points: [Point], mode: FavoriteSortMode) -> [Point] { - stableSorted(points) { compareFavoritePoints($0, $1, mode: mode) } + points.sorted { compareFavoritePoints($0, $1, mode: mode) == .orderedAscending } } static func defaultSortMode() -> FavoriteSortMode { @@ -92,9 +92,9 @@ protocol FavoriteSortablePoint { case .oldestDateFirst: return compareDates(lhs.lastModified, rhs.lastModified, newestFirst: false, lhsTitle: lhs.title, rhsTitle: rhs.title) case .nameAZ: - return lhs.title.localizedCaseInsensitiveCompare(rhs.title) + return compareTitles(lhs.title, rhs.title) case .nameZA: - return rhs.title.localizedCaseInsensitiveCompare(lhs.title) + return compareTitles(rhs.title, lhs.title) case .nearest, .farthest: return .orderedSame } @@ -103,55 +103,48 @@ protocol FavoriteSortablePoint { private static func compareFavoritePoints(_ lhs: Point, _ rhs: Point, mode: FavoriteSortMode) -> ComparisonResult { switch mode { case .nameAZ: - return lhs.title.localizedCaseInsensitiveCompare(rhs.title) + return compareTitles(lhs.title, rhs.title) case .nameZA: - return rhs.title.localizedCaseInsensitiveCompare(lhs.title) + 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 .newestDateFirst: - return compareDates(lhs.timestamp, rhs.timestamp, newestFirst: true, 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.timestamp, rhs.timestamp, newestFirst: false, lhsTitle: lhs.title, rhsTitle: rhs.title) - case .lastModified: - return .orderedSame + 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 { - switch (lhs, rhs) { - case let (lhs?, rhs?) where lhs != rhs: + if let lhs, let rhs, lhs != rhs { return newestFirst ? rhs.compare(lhs) : lhs.compare(rhs) - case (_?, nil): + } else if lhs != nil { return .orderedAscending - case (nil, _?): + } else if rhs != nil { return .orderedDescending - default: - return lhsTitle.localizedCaseInsensitiveCompare(rhsTitle) } + + return compareTitles(lhsTitle, rhsTitle) } private static func compareDistances(_ lhs: CLLocationDistance?, _ rhs: CLLocationDistance?, nearestFirst: Bool, lhsTitle: String, rhsTitle: String) -> ComparisonResult { - switch (lhs, rhs) { - case let (lhs?, rhs?) where lhs != rhs: + if let lhs, let rhs, lhs != rhs { if nearestFirst { return lhs < rhs ? .orderedAscending : .orderedDescending } return lhs > rhs ? .orderedAscending : .orderedDescending - case (_?, nil): + } else if lhs != nil { return .orderedAscending - case (nil, _?): + } else if rhs != nil { return .orderedDescending - default: - return lhsTitle.localizedCaseInsensitiveCompare(rhsTitle) } + + return compareTitles(lhsTitle, rhsTitle) } - private static func stableSorted(_ elements: [Element], by comparator: (Element, Element) -> ComparisonResult) -> [Element] { - elements.enumerated().sorted { lhs, rhs in - let result = comparator(lhs.element, rhs.element) - return result == .orderedSame ? lhs.offset < rhs.offset : result == .orderedAscending - }.map(\.element) + private static func compareTitles(_ lhs: String, _ rhs: String) -> ComparisonResult { + lhs.localizedCaseInsensitiveCompare(rhs) } } From 63a802f24f0278f375c442f6da5b73a93630eafe Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Mon, 8 Jun 2026 13:50:09 +0300 Subject: [PATCH 11/41] add folder subtitle --- .../MyPlaces/FavoriteListViewController.swift | 35 ++++--- .../MyPlaces/OAFavoriteFoldersBridge.mm | 55 +---------- .../MyPlaces/OAFavoritesSwiftHelper.h | 8 +- .../MyPlaces/OAFavoritesSwiftHelper.mm | 91 +++++++------------ Sources/OsmAnd Maps-Bridging-Header.h | 1 - 5 files changed, 57 insertions(+), 133 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 501b2a3e6c..896eb2a1f6 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -49,6 +49,12 @@ private enum FavoriteListItem: Hashable { } private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { + private static let subtitleDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "d MMM" + return formatter + }() + let bridgeItem: OAFavoriteFolderBridgeItem var title: String { @@ -67,6 +73,12 @@ private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { 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" } @@ -87,11 +99,6 @@ private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { init(item: OAFavoriteFolderBridgeItem) { bridgeItem = item } - - private static func title(for groupName: String, fallback: String) -> String { - guard !groupName.isEmpty else { return fallback } - return groupName.components(separatedBy: "/").last ?? fallback - } } private struct FavoritePointRow: Hashable, FavoriteSortablePoint { @@ -253,12 +260,13 @@ final class FavoriteListViewController: UIViewController { private lazy var folderCellRegistration = CellRegistration { [weak self] cell, _, folder in var content = cell.defaultContentConfiguration() content.directionalLayoutMargins = Self.rowContentInsets - content.image = UIImage.templateImageNamed(folder.iconName)?.resizedTemplateImage(with: FavoriteListViewController.imageSize) + let iconName = self?.isRootFolder == true && folder.isPinned ? "ic_custom_folder_pin" : folder.iconName + content.image = UIImage.templateImageNamed(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.secondaryText = "\(localizedString("points_count")) \(folder.bridgeItem.pointsCount)" + content.secondaryText = folder.subtitle content.secondaryTextProperties.color = .textColorSecondary cell.contentConfiguration = content cell.backgroundConfiguration = self?.listCellBackgroundConfiguration() @@ -600,7 +608,7 @@ final class FavoriteListViewController: UIViewController { private func applyFolderSnapshot(folder: FavoriteFolderRow, animatingDifferences: Bool) { let allFolders = favoriteFolders() let folders = FavoriteSortModeHelper.sortFoldersWithMode(directFavoriteFolders(allFolders, parentGroupName: folder.bridgeItem.groupName).filter { matchesSearch($0.title) }, mode: currentSortMode) - let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(OAFavoriteFoldersBridge.favoritePoints(forGroupName: folder.bridgeItem.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.bridgeItem.address) }, mode: currentSortMode) + let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(OAFavoritesSwiftHelper.favoritePoints(forGroupName: folder.bridgeItem.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.bridgeItem.address) }, mode: currentSortMode) let stats = folderStats(allFolders: allFolders, currentGroupName: folder.bridgeItem.groupName) var snapshot = Snapshot() layoutSections = stats == nil ? [.sortHeader, .content] : [.sortHeader, .content, .statsFooter] @@ -652,16 +660,15 @@ final class FavoriteListViewController: UIViewController { 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 + OAFavoritesSwiftHelper.favoriteGroupSize(forGroupName: $1.bridgeItem.groupName) } + 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 groupNames = [currentGroupName] + nestedFolders.map(\.bridgeItem.groupName) - let currentPointsCount = allFolders.first { $0.bridgeItem.groupName == currentGroupName }?.bridgeItem.pointsCount ?? 0 - let pointsCount = currentPointsCount + nestedFolders.reduce(0) { $0 + $1.bridgeItem.pointsCount } + 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 = groupNames.reduce(Int64(0)) { $0 + OAFavoritesSwiftHelper.favoriteGroupSize(forGroupName: $1) } + let fileSize = (currentFolder?.bridgeItem.fileSize ?? 0) + nestedFolders.reduce(Int64(0)) { $0 + $1.bridgeItem.fileSize } return FavoriteFolderStats(foldersCount: nestedFolders.count, pointsCount: Int(pointsCount), fileSize: fileSize) } @@ -1069,7 +1076,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { configureToolbar() return } - OAFavoriteFoldersBridge.openFavoritePoint(withIdentifier: favorite.bridgeItem.identifier) + OAFavoritesSwiftHelper.openFavoritePoint(withIdentifier: favorite.bridgeItem.identifier) case .sortHeader, .backupBanner, .header, .statsFooter: break } diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm index 3187d89599..eae51e483c 100644 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm +++ b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm @@ -37,8 +37,6 @@ @interface OAFavoriteFoldersBridge () + (NSArray *)sortedFavoritePoints:(NSArray *)points; + (NSArray *)sortedFavoritePointsForGroup:(OAFavoriteGroup *)group; + (NSArray *)favoriteGroupsInsideOrEqualToGroupName:(NSString *)groupName; -+ (nullable NSDate *)lastModifiedDateForGroupName:(NSString *)groupName groups:(NSArray *)groups fileAttributesByGroupName:(NSDictionary *> *)fileAttributesByGroupName; -+ (NSDictionary *> *)favoriteStorageAttributesForGroups:(NSArray *)groups; + (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier; + (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName; + (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; @@ -116,18 +114,7 @@ @implementation OAFavoriteFoldersBridge + (NSArray *)favoriteFolders { - 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]; - [folders addObject:[[OAFavoriteFolderBridgeItem alloc] initWithGroup:group index:index lastModifiedDate:lastModifiedDate fileSize:fileSize]]; - }]; - - return folders.copy; + return [OAFavoritesSwiftHelper favoriteFolders]; } + (NSArray *)favoritePointsForGroupName:(NSString *)groupName @@ -382,46 +369,6 @@ + (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier return [self sortedFavoritePoints:group.points ?: @[]]; } -+ (nullable 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; -} - -+ (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]; diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h index dfadeb2345..1bf7cb7df4 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h @@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN -@class OAEditGroupViewController, OAFavoriteGroupEditorViewController, OAOpenAddTrackViewController, UIColor, UIImage, OAFavoriteGroup, OAFavoriteItem, OASGpxUtilitiesPointsGroup; +@class UIColor, UIImage, OAFavoriteItem, OASGpxUtilitiesPointsGroup; @interface OAFavoriteFolderBridgeItem : NSObject @@ -18,14 +18,13 @@ NS_ASSUME_NONNULL_BEGIN @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; - @end @interface OAFavoritePointBridgeItem : NSObject @@ -40,7 +39,6 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) BOOL isVisible; - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite; -+ (nullable NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite; @end @@ -48,7 +46,6 @@ NS_ASSUME_NONNULL_BEGIN + (NSArray *)favoriteFolders; + (NSArray *)favoritePointsForGroupName:(NSString *)groupName; -+ (long long)favoriteGroupSizeForGroupName:(NSString *)groupName; + (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible; + (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned; @@ -80,4 +77,3 @@ NS_ASSUME_NONNULL_BEGIN @end NS_ASSUME_NONNULL_END - diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm index db67594062..158d917fbd 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm @@ -17,7 +17,6 @@ #import "OALocationServices.h" #import "OAMapPanelViewController.h" #import "OAOpenAddTrackViewController.h" -#import "OAOsmAndFormatter.h" #import "OAPointDescription.h" #import "OARootViewController.h" #import "OASavingTrackHelper.h" @@ -30,9 +29,16 @@ #include +@interface OAFavoriteFolderBridgeItem () + +- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize subtreePointsCount:(NSUInteger)subtreePointsCount; ++ (NSString *)titleForGroupName:(NSString *)groupName; + +@end + @implementation OAFavoriteFolderBridgeItem -- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize +- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize subtreePointsCount:(NSUInteger)subtreePointsCount { self = [super init]; if (self) @@ -40,8 +46,9 @@ - (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index l NSString *groupName = group.name ?: @""; _identifier = [NSString stringWithFormat:@"%@-%lu", groupName, (unsigned long)index]; _groupName = groupName; - _title = [OAFavoriteGroup getDisplayName:groupName] ?: groupName; + _title = [self.class titleForGroupName:groupName]; _pointsCount = group.points.count; + _subtreePointsCount = subtreePointsCount; _isVisible = group.isVisible; _isPinned = group.isPinned; _color = group.color; @@ -52,13 +59,17 @@ - (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index l return self; } ++ (NSString *)titleForGroupName:(NSString *)groupName +{ + NSString *lastComponent = [[groupName componentsSeparatedByString:@"/"] lastObject] ?: groupName; + return [OAFavoriteGroup getDisplayName:lastComponent] ?: lastComponent; +} + @end @interface OAFavoritePointBridgeItem () -+ (NSString *)subtitleForFavorite:(OAFavoriteItem *)favorite; -+ (NSString *)formattedDistanceForFavorite:(OAFavoriteItem *)favorite; -+ (NSString *)formattedDate:(NSDate *)date; ++ (nullable NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite; @end @@ -95,50 +106,6 @@ + (nullable NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite return @(distance); } -+ (NSString *)subtitleForFavorite:(OAFavoriteItem *)favorite -{ - NSMutableArray *parts = [NSMutableArray array]; - NSString *distance = [self formattedDistanceForFavorite:favorite]; - if (distance.length > 0) - [parts addObject:distance]; - - NSString *address = [favorite getAddress]; - if (address.length > 0) - [parts addObject:address]; - - NSDate *timestamp = [favorite getTimestamp]; - if (timestamp) - [parts addObject:[self formattedDate:timestamp]]; - - return parts.count > 0 ? [parts componentsJoinedByString:@" • "] : nil; -} - -+ (NSString *)formattedDistanceForFavorite:(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 [OAOsmAndFormatter getFormattedDistance:distance]; -} - -+ (NSString *)formattedDate:(NSDate *)date -{ - static NSDateFormatter *dateFormatter = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - dateFormatter = [[NSDateFormatter alloc] init]; - dateFormatter.dateStyle = NSDateFormatterShortStyle; - dateFormatter.timeStyle = NSDateFormatterNoStyle; - }); - - return [dateFormatter stringFromDate:date]; -} - @end @interface OAFavoritesSwiftHelper () @@ -146,6 +113,7 @@ @interface OAFavoritesSwiftHelper () + (NSArray *)sortedFavoritePoints:(NSArray *)points; + (NSArray *)sortedFavoritePointsForGroup:(OAFavoriteGroup *)group; + (NSArray *)favoriteGroupsInsideOrEqualToGroupName:(NSString *)groupName; ++ (NSUInteger)subtreePointsCountForGroupName:(NSString *)groupName groups:(NSArray *)groups; + (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier; + (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName; + (BOOL)renameFavoriteGroupTreeFromGroupName:(NSString *)sourceGroupName toGroupName:(NSString *)targetGroupName; @@ -171,7 +139,8 @@ @implementation OAFavoritesSwiftHelper NSDictionary *fileAttributes = fileAttributesByGroupName[groupName]; NSDate *lastModifiedDate = [self lastModifiedDateForGroupName:groupName groups:groups fileAttributesByGroupName:fileAttributesByGroupName]; long long fileSize = [fileAttributes[NSFileSize] longLongValue]; - [folders addObject:[[OAFavoriteFolderBridgeItem alloc] initWithGroup:group index:index lastModifiedDate:lastModifiedDate fileSize:fileSize]]; + NSUInteger subtreePointsCount = [self subtreePointsCountForGroupName:groupName groups:groups]; + [folders addObject:[[OAFavoriteFolderBridgeItem alloc] initWithGroup:group index:index lastModifiedDate:lastModifiedDate fileSize:fileSize subtreePointsCount:subtreePointsCount]]; }]; return [folders copy]; @@ -187,13 +156,6 @@ @implementation OAFavoritesSwiftHelper return items.copy; } -+ (long long)favoriteGroupSizeForGroupName:(NSString *)groupName -{ - NSString *filePath = [OsmAndApp.instance favoritesStorageFilename:groupName ?: @""]; - NSDictionary *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:filePath error:nil]; - return [attributes[NSFileSize] longLongValue]; -} - + (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible { OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; @@ -710,6 +672,19 @@ + (nullable NSDate *)lastModifiedDateForGroupName:(NSString *)groupName groups:( 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]; diff --git a/Sources/OsmAnd Maps-Bridging-Header.h b/Sources/OsmAnd Maps-Bridging-Header.h index 8fa2989136..77ad6bfdc5 100644 --- a/Sources/OsmAnd Maps-Bridging-Header.h +++ b/Sources/OsmAnd Maps-Bridging-Header.h @@ -113,7 +113,6 @@ #import "OAOsmBugsDBHelper.h" #import "OAOpenStreetMapPoint.h" #import "OAOsmNotePoint.h" -#import "OAFavoriteFoldersBridge.h" // Widgets #import "OAMapWidgetRegistry.h" From 64529f29eee814b6343dd35cdd56e6aa815597f0 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Tue, 9 Jun 2026 11:04:40 +0300 Subject: [PATCH 12/41] toolbar --- .../en.lproj/Localizable.strings | 5 + .../MyPlaces/FavoriteListViewController.swift | 182 ++++++++-- .../MyPlaces/OAFavoritesSwiftHelper.h | 6 + .../MyPlaces/OAFavoritesSwiftHelper.mm | 319 ++++++++++++++++++ Sources/OsmAnd Maps-Bridging-Header.h | 1 + 5 files changed, 486 insertions(+), 27 deletions(-) diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index 493e7eb4f5..a917b199f2 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -1066,6 +1066,11 @@ "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."; "favorite_friends_category" = "Friends"; "favorite_places_category" = "Places"; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 896eb2a1f6..5ae2a99b3c 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -177,8 +177,10 @@ final class FavoriteListViewController: UIViewController { private var layoutSections: [FavoriteListSection] = [] private let appearanceCollection: OAGPXAppearanceCollection = .sharedInstance() private var groupController: OAEditGroupViewController? + private var colorController: OAEditColorViewController? private var groupEditContext: FavoriteGroupEditContext? private var addToTrackGroupName: String? + private var addToTrackFavoriteItems: [Any]? private var searchText = "" private var isSearchActive = false @@ -743,6 +745,16 @@ final class FavoriteListViewController: UIViewController { private func openFavoriteGroupAddToTrack(_ groupName: String) { guard OAFavoritesSwiftHelper.canUseGroup(withName: groupName), let navigationController, let viewController = OAOpenAddTrackViewController(screenType: .addToATrack) else { return } addToTrackGroupName = groupName + addToTrackFavoriteItems = nil + viewController.delegate = self + let modalNavigationController = UINavigationController(rootViewController: viewController) + navigationController.present(modalNavigationController, animated: true) + } + + private func openFavoriteItemsAddToTrack(_ favoriteItems: [Any]) { + guard !favoriteItems.isEmpty, let navigationController, let viewController = OAOpenAddTrackViewController(screenType: .addToATrack) else { return } + addToTrackFavoriteItems = favoriteItems + addToTrackGroupName = nil viewController.delegate = self let modalNavigationController = UINavigationController(rootViewController: viewController) navigationController.present(modalNavigationController, animated: true) @@ -910,6 +922,33 @@ final class FavoriteListViewController: UIViewController { 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 folderGroupNames = folders.map(\.groupName) + 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 bridgeItems(for indexPaths: [IndexPath]) -> [Any] { indexPaths.compactMap { indexPath in guard let item = dataSource.itemIdentifier(for: indexPath) else { return nil } @@ -930,44 +969,107 @@ final class FavoriteListViewController: UIViewController { private func makeAdditionalContextMenu() -> UIMenu { var menuElements: [UIMenuElement] = [] - let hasPoints = collectionView.indexPathsForSelectedItems?.contains { + 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 - } ?? false + } - let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: menuImage("ic_custom_marker")) { _ in - // TODO + let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: menuImage("ic_custom_marker")) { [weak self] _ in + OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: selectedBridgeItems) + self?.setEdit(false) + self?.applySnapshot(animatingDifferences: true) } - let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: menuImage("ic_custom_trip")) { _ in - // TODO + let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: menuImage("ic_custom_trip")) { [weak self] _ in + self?.openFavoriteItemsAddToTrack(selectedBridgeItems) + self?.setEdit(false) + self?.applySnapshot(animatingDifferences: true) } - let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: menuImage("ic_custom_navigation_outlined")) { _ in - // TODO + let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: menuImage("ic_custom_navigation_outlined")) { [weak self] _ in + OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: selectedBridgeItems) + self?.applySnapshot(animatingDifferences: true) } let addToMenu = UIMenu(title: localizedString("add_to"), image: menuImage("ic_custom_add"), children: [trackAction, navigationAction, mapMarkersAction]) let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: [addToMenu]) menuElements.append(thirdButtonsSection) - let changeAppearanceAction = UIAction(title: localizedString("change_appearance"), image: menuImage("ic_custom_appearance_outlined")) { _ in - // TODO + let changeAppearanceAction = UIAction(title: localizedString("change_appearance"), image: menuImage("ic_custom_appearance_outlined")) { [weak self] _ in + self?.openFavoriteItemsAppearance() } let secondButtonsSection = UIMenu(title: "", options: .displayInline, children: [changeAppearanceAction]) menuElements.append(secondButtonsSection) if !hasPoints { - let showHideAction = UIAction(title: localizedString("shared_string_hide_from_map"), image: menuImage("ic_custom_hide_outlined")) { _ in - // TODO + let folders: [FavoriteFolderRow] = indexPathItems.compactMap { + guard case .folder(let folder) = dataSource.itemIdentifier(for: $0) else { return nil } + return folder } - let pinAction = UIAction(title: localizedString("pin_folder"), image: menuImage("ic_custom_map_pin_outlined")) { _ in - // TODO + + 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: menuImage("ic_custom_map_pin_outlined")) { [weak self] _ in + OAFavoritesSwiftHelper.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: menuImage("ic_custom_map_pin_outlined")) { [weak self] _ in + OAFavoritesSwiftHelper.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: menuImage("ic_custom_hide_outlined")) { [weak self] _ in + OAFavoritesSwiftHelper.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: menuImage("ic_custom_show_outlined")) { [weak self] _ in + OAFavoritesSwiftHelper.setFavoriteGroupsVisible(hiddenGroupNames, visible: true) + self?.applySnapshot(animatingDifferences: true) + } + folderMenuElements.append(showAction) + } + + let firstButtonsSection = UIMenu(title: "", options: .displayInline, children: folderMenuElements) + menuElements.append(firstButtonsSection) } - let firstButtonsSection = UIMenu(title: "", options: .displayInline, children: [pinAction, showHideAction]) - menuElements.append(firstButtonsSection) } return UIMenu(title: "", children: menuElements) } + private 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) + } + @objc private func selectButtonPressed() { setEdit(true) } @@ -1023,7 +1125,8 @@ final class FavoriteListViewController: UIViewController { } @objc private func deleteButtonClicked(_ sender: Any) { - if collectionView.indexPathsForSelectedItems?.isEmpty == true { + guard let indexPathItems = collectionView.indexPathsForSelectedItems else { return } + if indexPathItems.isEmpty { let alert = UIAlertController(title: nil, message: localizedString("fav_select_remove"), preferredStyle: .alert) let defaultAction = UIAlertAction(title: localizedString("ok"), style: .default) alert.addAction(defaultAction) @@ -1031,26 +1134,30 @@ final class FavoriteListViewController: UIViewController { return } + let selectedBridgeItems = bridgeItems(for: indexPathItems) + let title = deleteConfirmationTitle(for: selectedBridgeItems) + let message = deleteConfirmationMessage(for: selectedBridgeItems) + let alert = UIAlertController( - title: nil, - message: localizedString("fav_remove_q"), + title: title, + message: message, preferredStyle: .alert ) - let yesButton = UIAlertAction( - title: localizedString("shared_string_yes"), - style: .default + let deleteButton = UIAlertAction( + title: localizedString("shared_string_delete"), + style: .destructive ) { [weak self] _ in self?.removeSelectedFavoriteItems() } let cancelButton = UIAlertAction( - title: localizedString("shared_string_no"), + title: localizedString("shared_string_cancel"), style: .cancel, handler: nil ) - alert.addAction(yesButton) + alert.addAction(deleteButton) alert.addAction(cancelButton) present(alert, animated: true, completion: nil) @@ -1121,6 +1228,23 @@ extension FavoriteListViewController: MyPlacesSearchable, UISearchResultsUpdatin } } +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 { + OAFavoritesSwiftHelper.changeFavoriteItems(bridgeItems(for: selectedItems), colorIndex: colorController.colorIndex) + } + + setEdit(false) + applySnapshot(animatingDifferences: true) + } +} + extension FavoriteListViewController: OAEditGroupViewControllerDelegate { func groupChanged() { guard let groupController else { return } @@ -1147,9 +1271,13 @@ extension FavoriteListViewController: OAEditGroupViewControllerDelegate { extension FavoriteListViewController: OAOpenAddTrackDelegate { func onFileSelected(_ gpxFilePath: String) { - guard let addToTrackGroupName else { return } - OAFavoritesSwiftHelper.addFavoriteGroup(toTrack: addToTrackGroupName, gpxFileName: gpxFilePath) - self.addToTrackGroupName = nil + if let addToTrackFavoriteItems { + OAFavoritesSwiftHelper.addFavoriteItems(toTrack: addToTrackFavoriteItems, gpxFileName: gpxFilePath) + self.addToTrackFavoriteItems = nil + } else if let addToTrackGroupName { + OAFavoritesSwiftHelper.addFavoriteGroup(toTrack: addToTrackGroupName, gpxFileName: gpxFilePath) + self.addToTrackGroupName = nil + } } } diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h index 1bf7cb7df4..5c69933a90 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h @@ -49,6 +49,8 @@ NS_ASSUME_NONNULL_BEGIN + (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 @@ -58,6 +60,7 @@ NS_ASSUME_NONNULL_BEGIN + (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; + (void)moveFavoriteItems:(NSArray *)favoriteItems toGroupName:(NSString *)targetGroupName; + (NSArray *)favoriteGroupNamesForMovingFavoriteItems:(NSArray *)favoriteItems; ++ (void)changeFavoriteItems:(NSArray *)favoriteItems colorIndex:(NSInteger)colorIndex; + (OASGpxUtilitiesPointsGroup *)pointsGroupForGroupName:(NSString *)groupName; + (NSArray *)favoriteGroupsToMoveForGroupName:(NSString *)groupName; @@ -71,8 +74,11 @@ NS_ASSUME_NONNULL_BEGIN + (void)openFavoritePointWithIdentifier:(NSString *)identifier; + (void)addFavoriteGroupToMapMarkers:(NSString *)groupName; ++ (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 diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm index 158d917fbd..fbb7de7244 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm @@ -17,6 +17,8 @@ #import "OALocationServices.h" #import "OAMapPanelViewController.h" #import "OAOpenAddTrackViewController.h" +#import "OADefaultFavorite.h" +#import "OAOsmAndFormatter.h" #import "OAPointDescription.h" #import "OARootViewController.h" #import "OASavingTrackHelper.h" @@ -26,6 +28,7 @@ #import "OsmAndApp.h" #import "OsmAndSharedWrapper.h" #import +#import "OAObservable.h" #include @@ -174,6 +177,50 @@ + (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned [OAFavoritesHelper updateGroup:group pinned:pinned saveImmediately:YES]; } ++ (void)setFavoriteGroupsVisible:(NSArray *)groupNames visible:(BOOL)visible +{ + 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 visible:visible saveImmediately:NO]; + changed = YES; + } + + if (changed) + [OAFavoritesHelper saveCurrentPointsIntoFile]; +} + ++ (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]; +} + + (BOOL)addFavoriteGroup:(NSString *)name parentGroupName:(nullable NSString *)parentGroupName iconName:(nullable NSString *)iconName @@ -324,6 +371,61 @@ + (void)moveFavoriteItems:(NSArray *)favoriteItems toGroupName:(NSString *)targe 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];; +} + + (OASGpxUtilitiesPointsGroup *)pointsGroupForGroupName:(NSString *)groupName { OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; @@ -565,6 +667,69 @@ + (void)addFavoriteGroupToMapMarkers:(NSString *)groupName } } ++ (void)addFavoriteItemsToMapMarkers:(NSArray *)favoriteItems +{ + if (favoriteItems.count == 0) + return; + + NSMutableArray *itemsToAdd = [NSMutableArray array]; + NSMutableSet *addedPointKeys = [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 (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) + { + NSString *pointKey = [favorite getKey]; + if (pointKey.length > 0 && [addedPointKeys containsObject:pointKey]) + continue; + + if (pointKey.length > 0) + [addedPointKeys addObject:pointKey]; + [itemsToAdd addObject:favorite]; + } + } + + 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 ([addedPointKeys containsObject:pointKey]) + continue; + + if (pointKey.length > 0) + [addedPointKeys addObject:pointKey]; + [itemsToAdd addObject:favorite]; + } + + 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 sortedFavoritePointsForGroup:[self favoriteGroupWithName:groupName]]; @@ -630,6 +795,160 @@ + (void)addFavoriteGroupToNavigation:(NSString *)groupName [rootViewController.mapPanel showRouteInfo]; } ++ (void)addFavoriteItemsToTrack:(NSArray *)favoriteItems gpxFileName:(nullable NSString *)gpxFileName +{ + if (favoriteItems.count == 0) + return; + + NSMutableArray *itemsToAdd = [NSMutableArray array]; + NSMutableSet *addedPointKeys = [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 (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) + { + NSString *pointKey = [favorite getKey]; + if (pointKey.length > 0 && [addedPointKeys containsObject:pointKey]) + continue; + + if (pointKey.length > 0) + [addedPointKeys addObject:pointKey]; + [itemsToAdd addObject:favorite]; + } + } + + 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 ([addedPointKeys containsObject:pointKey]) + continue; + + if (pointKey.length > 0) + [addedPointKeys addObject:pointKey]; + [itemsToAdd addObject:favorite]; + } + + 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; + + NSMutableArray *itemsToAdd = [NSMutableArray array]; + NSMutableSet *addedPointKeys = [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 (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) + { + NSString *pointKey = [favorite getKey]; + if (pointKey.length > 0 && [addedPointKeys containsObject:pointKey]) + continue; + + if (pointKey.length > 0) + [addedPointKeys addObject:pointKey]; + [itemsToAdd addObject:favorite]; + } + } + + 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 ([addedPointKeys containsObject:pointKey]) + continue; + + if (pointKey.length > 0) + [addedPointKeys addObject:pointKey]; + [itemsToAdd addObject:favorite]; + } + + if (itemsToAdd.count == 0) + return; + + OATargetPointsHelper *targetPointsHelper = [OATargetPointsHelper sharedInstance]; + [targetPointsHelper clearAllPoints:NO]; + for (NSUInteger index = 0; index < itemsToAdd.count; index++) + { + OAFavoriteItem *favorite = itemsToAdd[index]; + CLLocation *location = [self locationForFavorite:favorite]; + if (!location) + continue; + + OAPointDescription *description = [[OAPointDescription alloc] initWithType:POINT_TYPE_FAVORITE name:[favorite getDisplayName]]; + BOOL isDestination = index == itemsToAdd.count - 1; + [targetPointsHelper navigateToPoint:location updateRoute:isDestination intermediate:isDestination ? -1 : (int)index historyName:description]; + } + + OARootViewController *rootViewController = [OARootViewController instance]; + [rootViewController.navigationController popToRootViewControllerAnimated:YES]; + [rootViewController.mapPanel showRouteInfo]; +} + + (NSArray *)sortedFavoritePoints:(NSArray *)points { return [points sortedArrayUsingComparator:^NSComparisonResult(OAFavoriteItem *obj1, OAFavoriteItem *obj2) { diff --git a/Sources/OsmAnd Maps-Bridging-Header.h b/Sources/OsmAnd Maps-Bridging-Header.h index 77ad6bfdc5..031d741bdb 100644 --- a/Sources/OsmAnd Maps-Bridging-Header.h +++ b/Sources/OsmAnd Maps-Bridging-Header.h @@ -67,6 +67,7 @@ #import "OAFavoriteGroupEditorViewController.h" #import "OAFavoritesSwiftHelper.h" #import "OAOpenAddTrackViewController.h" +#import "OAEditColorViewController.h" #import "OATravelGuidesHelper.h" #import "OAGPXDocumentAdapter.h" #import "OATravelLocalDataDbHelper.h" From 3bc75bfca6cca674cfdcd7e8f8f42803d490595a Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Tue, 9 Jun 2026 13:16:43 +0300 Subject: [PATCH 13/41] Add Folder menu --- .../MyPlaces/FavoriteListViewController.swift | 110 +++++++++++------- .../MyPlaces/OAFavoritesSwiftHelper.mm | 50 ++++---- .../OAFavoriteImportViewController.mm | 24 ++++ Sources/Helpers/OAFavoritesHelper.mm | 5 +- 4 files changed, 122 insertions(+), 67 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 5ae2a99b3c..521ac1a16c 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -262,7 +262,7 @@ final class FavoriteListViewController: UIViewController { private lazy var folderCellRegistration = CellRegistration { [weak self] cell, _, folder in var content = cell.defaultContentConfiguration() content.directionalLayoutMargins = Self.rowContentInsets - let iconName = self?.isRootFolder == true && folder.isPinned ? "ic_custom_folder_pin" : folder.iconName + let iconName = folder.isPinned ? "ic_custom_folder_pin" : folder.iconName content.image = UIImage.templateImageNamed(iconName)?.resizedTemplateImage(with: FavoriteListViewController.imageSize) content.imageProperties.tintColor = folder.iconColor content.text = folder.title @@ -492,9 +492,9 @@ final class FavoriteListViewController: UIViewController { myPlacesDelegate?.updateSegmentedControlVisibility(isRootFolder && !collectionView.isEditing && !isSearchActive) } - private func favoriteSortMode() -> FavoriteSortMode { + private func favoriteSortMode(entryId: String? = nil) -> FavoriteSortMode { let sortModes = settings.getFavoriteSortModes() - guard let sortModeTitle = sortModes[currentSortEntryId] else { return FavoriteSortModeHelper.defaultSortMode() } + guard let sortModeTitle = sortModes[entryId ?? currentSortEntryId] else { return FavoriteSortModeHelper.defaultSortMode() } return FavoriteSortMode.byTitle(sortModeTitle) } @@ -515,6 +515,19 @@ final class FavoriteListViewController: UIViewController { applySnapshot(animatingDifferences: false) } + private func clearFavoriteSortModes(forGroupNames groupNames: [String]) { + var sortModes = settings.getFavoriteSortModes() + let keysToRemove = sortModes.keys.filter { key in + groupNames.contains { groupName in + key == groupName || (!groupName.isEmpty && key.hasPrefix(groupName + "/")) + } + } + + guard !keysToRemove.isEmpty else { return } + keysToRemove.forEach { sortModes.removeValue(forKey: $0) } + settings.saveFavoriteSortModes(sortModes) + } + private func makeSortMenu() -> UIMenu { let modes: [FavoriteSortMode] = isRootFolder && !isSearchActive ? [.lastModified, .nameAZ, .nameZA, .newestDateFirst, .oldestDateFirst] : FavoriteSortMode.allCases let groups: [[FavoriteSortMode]] = [[.lastModified], [.nameAZ, .nameZA], [.newestDateFirst, .oldestDateFirst], [.nearest, .farthest]] @@ -760,11 +773,17 @@ final class FavoriteListViewController: UIViewController { navigationController.present(modalNavigationController, animated: true) } + private func favoritePointRows(forGroupName groupName: String) -> [FavoritePointRow] { + let sortMode = isSearchActive ? searchFavoriteSortMode() : favoriteSortMode(entryId: groupName) + let favorites = OAFavoritesSwiftHelper.favoritePoints(forGroupName: groupName).map { FavoritePointRow(item: $0) } + return FavoriteSortModeHelper.sortFavoritePointsWithMode(favorites, mode: sortMode) + } + private func makeActionsMenu() -> UIMenu { - let addFolderAction = UIAction(title: localizedString("add_new_folder"), image: .icCustomFolderAddOutlined.resizedMenuImage()) { [weak self] _ in + 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.resizedMenuImage()) { [weak self] _ in + let importAction = UIAction(title: localizedString("shared_string_import"), image: .icCustomImportOutlined) { [weak self] _ in guard let self else { return } let gpxType = UTType(importedAs: "com.topografix.gpx", conformingTo: .xml) let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [gpxType], asCopy: true) @@ -791,56 +810,59 @@ final class FavoriteListViewController: UIViewController { } private func makeFolderContextMenu(for folder: FavoriteFolderRow, indexPath: IndexPath) -> UIMenu { - let showHideAction = UIAction(title: localizedString(folder.isVisible ? "shared_string_hide_from_map" : "shared_string_show_on_map"), image: menuImage(folder.isVisible ? "ic_custom_hide_outlined" : "ic_custom_show_outlined")) { [weak self] _ in + 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 } OAFavoritesSwiftHelper.setFavoriteGroupVisible(folder.bridgeItem.groupName, visible: !folder.isVisible) - self?.applySnapshot(animatingDifferences: true) + self.applySnapshot(animatingDifferences: true) } - let pinAction = UIAction(title: localizedString(folder.isPinned ? "unpin_folder" : "pin_folder"), image: menuImage("ic_custom_map_pin_outlined")) { [weak self] _ in + let pinAction = UIAction(title: localizedString(folder.isPinned ? "unpin_folder" : "pin_folder"), image: folder.isPinned ? .icCustomDrawingPinDisable : .icCustomDrawingPin) { [weak self] _ in + guard let self else { return } OAFavoritesSwiftHelper.setFavoriteGroupPinned(folder.bridgeItem.groupName, pinned: !folder.isPinned) - self?.applySnapshot(animatingDifferences: true) + self.applySnapshot(animatingDifferences: true) } let firstButtonsSection = UIMenu(title: "", options: .displayInline, children: [showHideAction, pinAction]) - let renameAction = UIAction(title: localizedString("shared_string_rename"), image: menuImage("ic_custom_edit")) { [weak self] _ in - self?.showRenameAlert(for: folder) + 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: menuImage("ic_custom_appearance_outlined")) { [weak self] _ in - self?.openFavoriteGroupAppearance(folder.bridgeItem.groupName) + 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: menuImage("ic_custom_export_outlined")) { [weak self] _ in + 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 = OAFavoritesSwiftHelper.shareFavoriteGroupName(folder.bridgeItem.groupName) else { return } - showActivity( - [favoritesUrl], - sourceView: sourceView, - barButtonItem: nil, - completionWithItemsHandler: { - try? FileManager.default.removeItem(at: favoritesUrl) - } - ) + showActivity([favoritesUrl], sourceView: sourceView, barButtonItem: nil, completionWithItemsHandler: { + try? FileManager.default.removeItem(at: favoritesUrl) + }) } - let moveAction = UIAction(title: localizedString("shared_string_move"), image: menuImage("ic_custom_folder_move_outlined")) { [weak self] _ in - self?.openFavoriteGroupMove(folder.bridgeItem.groupName) + let moveAction = UIAction(title: localizedString("shared_string_move"), image: .icCustomFolderMoveOutlined) { [weak self] _ in + guard let self else { return } + self.openFavoriteGroupMove(folder.bridgeItem.groupName) } let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: folder.bridgeItem.groupName.isEmpty ? [shareAction] : [shareAction, moveAction]) - let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: menuImage("ic_custom_map_pin_outlined")) { _ in + let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { _ in OAFavoritesSwiftHelper.addFavoriteGroup(toMapMarkers: folder.bridgeItem.groupName) } - let trackAction = UIAction(title: localizedString("add_to_a_track"), image: menuImage("ic_custom_trip")) { [weak self] _ in - self?.openFavoriteGroupAddToTrack(folder.bridgeItem.groupName) + let trackAction = UIAction(title: localizedString("add_to_a_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: menuImage("ic_custom_navigation_outlined")) { _ in - OAFavoritesSwiftHelper.addFavoriteGroup(toNavigation: folder.bridgeItem.groupName) + let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in + guard let self else { return } + OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: self.favoritePointRows(forGroupName: folder.bridgeItem.groupName).map { $0.bridgeItem }) } - let addToMenu = UIMenu(title: localizedString("shared_string_add"), image: menuImage("ic_custom_add"), children: [mapMarkersAction, trackAction, navigationAction]) + let addToMenu = UIMenu(title: localizedString("shared_string_add"), image: .icCustomAdd, children: [mapMarkersAction, trackAction, navigationAction]) let fourthButtonsSection = UIMenu(title: "", options: .displayInline, children: [addToMenu]) - let deleteAction = UIAction(title: localizedString("shared_string_delete"), image: menuImage("ic_custom_trash_outlined"), attributes: .destructive) { [weak self] _ in - self?.showDeleteAlert(for: folder) + 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]) @@ -875,13 +897,15 @@ final class FavoriteListViewController: UIViewController { } private func showDeleteAlert(for folder: FavoriteFolderRow) { - let alert = UIAlertController(title: nil, message: localizedString("fav_remove_q"), preferredStyle: .alert) - alert.addAction(UIAlertAction(title: localizedString("shared_string_yes"), style: .destructive) { [weak self] _ in - OAFavoritesSwiftHelper.deleteFavoriteGroup(folder.bridgeItem.groupName) + let message = String(format: localizedString("permanent_delete_warning"), "\"\(folder.title)\"") + 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 OAFavoritesSwiftHelper.deleteFavoriteGroup(folder.bridgeItem.groupName) else { return } + self?.clearFavoriteSortModes(forGroupNames: [folder.bridgeItem.groupName]) self?.applySnapshot(animatingDifferences: true) }) - alert.addAction(UIAlertAction(title: localizedString("shared_string_no"), style: .cancel)) + alert.addAction(UIAlertAction(title: localizedString("shared_string_cancel"), style: .cancel)) present(alert, animated: true) } @@ -917,7 +941,12 @@ final class FavoriteListViewController: UIViewController { private func removeSelectedFavoriteItems() { let selectedIndexPaths = collectionView.indexPathsForSelectedItems ?? [] - OAFavoritesSwiftHelper.deleteFavoriteItems(bridgeItems(for: selectedIndexPaths)) + let items = bridgeItems(for: selectedIndexPaths) + let groupNames = items.compactMap { ($0 as? OAFavoriteFolderBridgeItem)?.groupName } + if OAFavoritesSwiftHelper.deleteFavoriteItems(items) { + clearFavoriteSortModes(forGroupNames: groupNames) + } + setEdit(false) applySnapshot(animatingDifferences: true) } @@ -942,7 +971,6 @@ final class FavoriteListViewController: UIViewController { return localizedString("favorites_delete_confirmation_message") } - let folderGroupNames = folders.map(\.groupName) let folderPointsCount = folders.reduce(0) { $0 + Int($1.subtreePointsCount) } let pointsCount = folderPointsCount + points.count @@ -1125,8 +1153,8 @@ final class FavoriteListViewController: UIViewController { } @objc private func deleteButtonClicked(_ sender: Any) { - guard let indexPathItems = collectionView.indexPathsForSelectedItems else { return } - if indexPathItems.isEmpty { + 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) @@ -1134,7 +1162,7 @@ final class FavoriteListViewController: UIViewController { return } - let selectedBridgeItems = bridgeItems(for: indexPathItems) + let selectedBridgeItems = bridgeItems(for: selectedIndexPaths) let title = deleteConfirmationTitle(for: selectedBridgeItems) let message = deleteConfirmationMessage(for: selectedBridgeItems) diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm index fbb7de7244..fc2f50a906 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm @@ -15,12 +15,15 @@ #import "OAGPXDatabase.h" #import "OAIndexConstants.h" #import "OALocationServices.h" +#import "OAMapActions.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" @@ -763,7 +766,8 @@ + (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(nullable NSSt [OASGpxUtilities.shared writeGpxFileFile:dataItem.file gpxFile:gpxFile]; [gpxDatabase updateDataItem:dataItem]; - [OASelectedGPXHelper.instance markTrackForReload:[OAUtilities getGpxShortPath:dataItem.file.absolutePath]]; + [OASelectedGPXHelper.instance markTrackForReload:dataItem.file.absolutePath]; + [OsmAndApp.instance.updateGpxTracksOnMapObservable notifyEvent]; } + (void)addFavoriteGroupToNavigation:(NSString *)groupName @@ -773,26 +777,13 @@ + (void)addFavoriteGroupToNavigation:(NSString *)groupName return; NSArray *points = [self sortedFavoritePointsForGroup:group]; - if (points.count == 0) - return; - - OATargetPointsHelper *targetPointsHelper = OATargetPointsHelper.sharedInstance; - [targetPointsHelper clearAllPoints:NO]; - for (NSUInteger index = 0; index < points.count; index++) + NSMutableArray *items = [NSMutableArray arrayWithCapacity:points.count]; + for (OAFavoriteItem *point in points) { - OAFavoriteItem *favorite = points[index]; - CLLocation *location = [self locationForFavorite:favorite]; - if (!location) - continue; - - OAPointDescription *description = [[OAPointDescription alloc] initWithType:POINT_TYPE_FAVORITE name:[favorite getDisplayName]]; - BOOL isDestination = index == points.count - 1; - [targetPointsHelper navigateToPoint:location updateRoute:isDestination intermediate:isDestination ? -1 : (int)index historyName:description]; + [items addObject:[[OAFavoritePointBridgeItem alloc] initWithFavorite:point]]; } - OARootViewController *rootViewController = [OARootViewController instance]; - [rootViewController.navigationController popToRootViewControllerAnimated:YES]; - [rootViewController.mapPanel showRouteInfo]; + [self addFavoriteItemsToNavigation:items]; } + (void)addFavoriteItemsToTrack:(NSArray *)favoriteItems gpxFileName:(nullable NSString *)gpxFileName @@ -930,23 +921,32 @@ + (void)addFavoriteItemsToNavigation:(NSArray *)favoriteItems if (itemsToAdd.count == 0) return; - OATargetPointsHelper *targetPointsHelper = [OATargetPointsHelper sharedInstance]; - [targetPointsHelper clearAllPoints:NO]; - for (NSUInteger index = 0; index < itemsToAdd.count; index++) + NSMutableArray *targetPoints = [NSMutableArray arrayWithCapacity:itemsToAdd.count]; + for (OAFavoriteItem *favorite in itemsToAdd) { - OAFavoriteItem *favorite = itemsToAdd[index]; CLLocation *location = [self locationForFavorite:favorite]; if (!location) continue; OAPointDescription *description = [[OAPointDescription alloc] initWithType:POINT_TYPE_FAVORITE name:[favorite getDisplayName]]; - BOOL isDestination = index == itemsToAdd.count - 1; - [targetPointsHelper navigateToPoint:location updateRoute:isDestination intermediate:isDestination ? -1 : (int)index historyName:description]; + 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 showRouteInfo]; + [rootViewController.mapPanel.mapActions enterRoutePlanningModeGivenGpx:nil + from:nil + fromName:nil + useIntermediatePointsByDefault:YES + showDialog:YES]; } + (NSArray *)sortedFavoritePoints:(NSArray *)points diff --git a/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm b/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm index df62feab1a..3f275b0fb0 100644 --- a/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm +++ b/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm @@ -160,6 +160,7 @@ - (void)onRightNavbarButtonPressed if (_gpxFile && _gpxFile.pointsGroups.count > 0) { // IOS-214 + [self normalizeSingleNestedFavoriteGroupIfNeeded]; if (![self isFavoritesValid]) return; @@ -183,6 +184,29 @@ - (void)dismissViewController #pragma mark - Additions +- (void)normalizeSingleNestedFavoriteGroupIfNeeded +{ + NSArray *groupKeys = _gpxFile.pointsGroups.allKeys; + if (groupKeys.count != 1) + return; + + NSString *groupKey = groupKeys.firstObject; + OASGpxUtilitiesPointsGroup *pointsGroup = _gpxFile.pointsGroups[groupKey]; + NSString *groupName = pointsGroup.name ?: groupKey; + NSArray *components = [groupName componentsSeparatedByString:@"/"]; + if (components.count < 2) + return; + + NSString *targetGroupName = components.lastObject; + if (targetGroupName.length == 0) + return; + + for (OASWptPt *point in pointsGroup.points) + { + point.category = targetGroupName; + } +} + - (BOOL)isFavoritesValid { if (!_gpxFile) diff --git a/Sources/Helpers/OAFavoritesHelper.mm b/Sources/Helpers/OAFavoritesHelper.mm index a8d61cef36..5a263d71d2 100644 --- a/Sources/Helpers/OAFavoritesHelper.mm +++ b/Sources/Helpers/OAFavoritesHelper.mm @@ -1018,7 +1018,10 @@ + (BOOL)deleteFavoriteGroups:(NSArray *)groupsToDelete } if (!isNewFavorite) + { + [self recalculateCachedFavPoints]; [self saveCurrentPointsIntoFile]; + } return YES; } @@ -1392,7 +1395,7 @@ - (OASGpxUtilitiesPointsGroup *)toPointsGroup { NSString *mxPrefix = @"mx_"; _iconName = [self removePrefix:mxPrefix fromValue:_iconName]; - OASBoolean *pinned = _isPinned ? [OASBoolean numberWithBool:YES] : nil; + 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]; From 79be4fbf1086e6914f9bf3d2e8bcf4927d239fff Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Tue, 9 Jun 2026 13:27:15 +0300 Subject: [PATCH 14/41] Del menuImage --- .../MyPlaces/FavoriteListViewController.swift | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 521ac1a16c..f1c3a635a8 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -991,10 +991,6 @@ final class FavoriteListViewController: UIViewController { } } - private func menuImage(_ name: String) -> UIImage? { - UIImage(named: name)?.resizedMenuImage() - } - private func makeAdditionalContextMenu() -> UIMenu { var menuElements: [UIMenuElement] = [] let indexPathItems = collectionView.indexPathsForSelectedItems ?? [] @@ -1004,25 +1000,25 @@ final class FavoriteListViewController: UIViewController { return true } - let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: menuImage("ic_custom_marker")) { [weak self] _ in + let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { [weak self] _ in OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: selectedBridgeItems) self?.setEdit(false) self?.applySnapshot(animatingDifferences: true) } - let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: menuImage("ic_custom_trip")) { [weak self] _ in + let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: .icCustomTrip) { [weak self] _ in self?.openFavoriteItemsAddToTrack(selectedBridgeItems) self?.setEdit(false) self?.applySnapshot(animatingDifferences: true) } - let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: menuImage("ic_custom_navigation_outlined")) { [weak self] _ in + let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: selectedBridgeItems) self?.applySnapshot(animatingDifferences: true) } - let addToMenu = UIMenu(title: localizedString("add_to"), image: menuImage("ic_custom_add"), children: [trackAction, navigationAction, mapMarkersAction]) + let addToMenu = UIMenu(title: localizedString("add_to"), image: .icCustomAdd, children: [trackAction, navigationAction, mapMarkersAction]) let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: [addToMenu]) menuElements.append(thirdButtonsSection) - let changeAppearanceAction = UIAction(title: localizedString("change_appearance"), image: menuImage("ic_custom_appearance_outlined")) { [weak self] _ in + let changeAppearanceAction = UIAction(title: localizedString("change_appearance"), image: .icCustomAppearanceOutlined) { [weak self] _ in self?.openFavoriteItemsAppearance() } let secondButtonsSection = UIMenu(title: "", options: .displayInline, children: [changeAppearanceAction]) @@ -1039,7 +1035,7 @@ final class FavoriteListViewController: UIViewController { if folders.contains(where: { !$0.isPinned }) { let unpinnedGroupNames = folders.filter({ !$0.isPinned }).map { $0.bridgeItem.groupName } - let pinAction = UIAction(title: localizedString("pin_folder"), image: menuImage("ic_custom_map_pin_outlined")) { [weak self] _ in + let pinAction = UIAction(title: localizedString("pin_folder"), image: .icCustomMapPinOutlined) { [weak self] _ in OAFavoritesSwiftHelper.setFavoriteGroupsPinned(unpinnedGroupNames, pinned: true) self?.applySnapshot(animatingDifferences: true) } @@ -1048,7 +1044,7 @@ final class FavoriteListViewController: UIViewController { if folders.contains(where: { $0.isPinned }) { let pinnedGroupNames = folders.filter({ $0.isPinned }).map { $0.bridgeItem.groupName } - let unpinAction = UIAction(title: localizedString("unpin_folder"), image: menuImage("ic_custom_map_pin_outlined")) { [weak self] _ in + let unpinAction = UIAction(title: localizedString("unpin_folder"), image: .icCustomMapPinOutlined) { [weak self] _ in OAFavoritesSwiftHelper.setFavoriteGroupsPinned(pinnedGroupNames, pinned: false) self?.applySnapshot(animatingDifferences: true) } @@ -1057,7 +1053,7 @@ final class FavoriteListViewController: UIViewController { 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: menuImage("ic_custom_hide_outlined")) { [weak self] _ in + let hideAction = UIAction(title: localizedString("shared_string_hide_from_map"), image: .icCustomHideOutlined) { [weak self] _ in OAFavoritesSwiftHelper.setFavoriteGroupsVisible(visibleGroupNames, visible: false) self?.applySnapshot(animatingDifferences: true) } @@ -1066,7 +1062,7 @@ final class FavoriteListViewController: UIViewController { 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: menuImage("ic_custom_show_outlined")) { [weak self] _ in + let showAction = UIAction(title: localizedString("shared_string_show_on_map"), image: .icCustomShowOutlined) { [weak self] _ in OAFavoritesSwiftHelper.setFavoriteGroupsVisible(hiddenGroupNames, visible: true) self?.applySnapshot(animatingDifferences: true) } From 2e5baf05dbc5d862854586b51dc0635adc70c932 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Tue, 9 Jun 2026 17:45:10 +0300 Subject: [PATCH 15/41] empty states --- OsmAnd.xcodeproj/project.pbxproj | 8 ++ .../en.lproj/Localizable.strings | 4 + .../Cells/EmptyStateCollectionViewCell.swift | 21 ++++ .../Xibs/EmptyStateCollectionViewCell.xib | 104 ++++++++++++++++++ .../MyPlaces/FavoriteListViewController.swift | 61 ++++++++-- 5 files changed, 186 insertions(+), 12 deletions(-) create mode 100644 Sources/Controllers/Cells/EmptyStateCollectionViewCell.swift create mode 100644 Sources/Controllers/Cells/Xibs/EmptyStateCollectionViewCell.xib diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index cfa1ff084b..d3135f8e47 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -220,6 +220,8 @@ 27291B612EE81169005D0B0A /* PreviewImageViewTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 27291B5F2EE81169005D0B0A /* PreviewImageViewTableViewCell.swift */; }; 27291B622EE81169005D0B0A /* PreviewImageViewTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 27291B602EE81169005D0B0A /* PreviewImageViewTableViewCell.xib */; }; 272AFB9B2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.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 */; }; @@ -3733,6 +3735,8 @@ 27291B602EE81169005D0B0A /* PreviewImageViewTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PreviewImageViewTableViewCell.xib; sourceTree = ""; }; 272AFB972FD2DC42006C2E21 /* OAFavoritesSwiftHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OAFavoritesSwiftHelper.h; sourceTree = ""; }; 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OAFavoritesSwiftHelper.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 = ""; }; @@ -12341,6 +12345,7 @@ 27291B5F2EE81169005D0B0A /* PreviewImageViewTableViewCell.swift */, CE7818252FC07944005CCF47 /* WikipediaContextMenuCell.swift */, 271A5C432FCDE1D700C27411 /* SortButtonCollectionViewCell.swift */, + 272AFBAA2FD810CC006C2E21 /* EmptyStateCollectionViewCell.swift */, DA5A7BB626C563A100F274C7 /* Xibs */, 468CD3822C809E3400CC3436 /* ElevationChartCell.swift */, 46BCB15F2CC2AC6D004E0283 /* GradientChartCell.swift */, @@ -12351,6 +12356,7 @@ DA5A7BB626C563A100F274C7 /* Xibs */ = { isa = PBXGroup; children = ( + 272AFBAB2FD810CC006C2E21 /* EmptyStateCollectionViewCell.xib */, 2702BC602EF98AD900A545A2 /* TopBottomValuesSliderTableViewCell.xib */, 27291B602EE81169005D0B0A /* PreviewImageViewTableViewCell.xib */, 27E8D9F02ED4688D00F98462 /* SegmentButtonsSliderTableViewCell.xib */, @@ -16802,6 +16808,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 */, @@ -17647,6 +17654,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 */, diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index 282ca9d2cd..c7f69803a5 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -1071,6 +1071,10 @@ "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."; "favorite_friends_category" = "Friends"; "favorite_places_category" = "Places"; 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/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.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 5ae2a99b3c..98e95b27e9 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -37,6 +37,7 @@ private enum FavoriteListSection: Hashable { case folderSection(FavoriteFolderSection) case content case statsFooter + case emptyState } private enum FavoriteListItem: Hashable { @@ -46,6 +47,7 @@ private enum FavoriteListItem: Hashable { case folder(FavoriteFolderRow) case favorite(FavoritePointRow) case statsFooter(FavoriteFolderStats) + case emptyState } private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { @@ -302,6 +304,15 @@ final class FavoriteListViewController: UIViewController { cell.contentView.addSubview(label) NSLayoutConstraint.activate([label.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: Self.statsFooterInsets.top), label.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: Self.statsFooterInsets.leading), label.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -Self.statsFooterInsets.trailing), label.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -Self.statsFooterInsets.bottom)]) } + private lazy var emptyStateCellRegistration = UICollectionView.CellRegistration(cellNib: UINib(nibName: EmptyStateCollectionViewCell.reuseIdentifier, bundle: nil)) { [weak self] cell, _, _ in + guard let self else { 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) + } private lazy var subfolderSearchController: UISearchController = { let searchController = UISearchController(searchResultsController: nil) searchController.searchResultsUpdater = self @@ -539,6 +550,7 @@ final class FavoriteListViewController: UIViewController { 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 sortMode): @@ -553,6 +565,8 @@ final class FavoriteListViewController: UIViewController { 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: ()) } } } @@ -568,11 +582,20 @@ final class FavoriteListViewController: UIViewController { private func applyRootSnapshot(animatingDifferences: Bool) { let allFolders = favoriteFolders() + var snapshot = Snapshot() + if allFolders.isEmpty { + layoutSections = [] + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections([.emptyState]) + snapshot.appendItems([.emptyState]) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + return + } + 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 snapshot = Snapshot() var sections: [FavoriteListSection] = [.sortHeader] if isPaymentBannerVisible { sections.append(.backupBanner) @@ -611,8 +634,16 @@ final class FavoriteListViewController: UIViewController { let allFolders = favoriteFolders() let folders = FavoriteSortModeHelper.sortFoldersWithMode(directFavoriteFolders(allFolders, parentGroupName: folder.bridgeItem.groupName).filter { matchesSearch($0.title) }, mode: currentSortMode) let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(OAFavoritesSwiftHelper.favoritePoints(forGroupName: folder.bridgeItem.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.bridgeItem.address) }, mode: currentSortMode) - let stats = folderStats(allFolders: allFolders, currentGroupName: folder.bridgeItem.groupName) var snapshot = Snapshot() + if favorites.isEmpty && folders.isEmpty { + layoutSections = [] + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections([.emptyState]) + snapshot.appendItems([.emptyState]) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + return + } + let stats = folderStats(allFolders: allFolders, currentGroupName: folder.bridgeItem.groupName) layoutSections = stats == nil ? [.sortHeader, .content] : [.sortHeader, .content, .statsFooter] collectionView.collectionViewLayout.invalidateLayout() snapshot.appendSections(layoutSections) @@ -765,12 +796,7 @@ final class FavoriteListViewController: UIViewController { self?.openNewFavoriteGroupEditor() } let importAction = UIAction(title: localizedString("shared_string_import"), image: .icCustomImportOutlined.resizedMenuImage()) { [weak self] _ in - guard let self else { return } - 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) + self?.openPickerToImport() } let addFolderSection = UIMenu(title: "", options: .displayInline, children: [addFolderAction]) @@ -942,7 +968,6 @@ final class FavoriteListViewController: UIViewController { return localizedString("favorites_delete_confirmation_message") } - let folderGroupNames = folders.map(\.groupName) let folderPointsCount = folders.reduce(0) { $0 + Int($1.subtreePointsCount) } let pointsCount = folderPointsCount + points.count @@ -957,7 +982,7 @@ final class FavoriteListViewController: UIViewController { return folder.bridgeItem case .favorite(let favorite): return favorite.bridgeItem - case .backupBanner, .header, .statsFooter, .sortHeader: + case .backupBanner, .header, .statsFooter, .sortHeader, .emptyState: return nil } } @@ -1069,6 +1094,14 @@ final class FavoriteListViewController: UIViewController { let modalNavigationController = UINavigationController(rootViewController: colorController) navigationController.present(modalNavigationController, animated: true) } + + 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) + } @objc private func selectButtonPressed() { setEdit(true) @@ -1084,7 +1117,7 @@ final class FavoriteListViewController: UIViewController { let indexPath = IndexPath(item: item, section: section) guard let itemIdentifier = dataSource.itemIdentifier(for: indexPath) else { continue } switch itemIdentifier { - case .sortHeader, .backupBanner, .header, .statsFooter: + case .sortHeader, .backupBanner, .header, .statsFooter, .emptyState: continue case .folder, .favorite: collectionView.selectItem(at: indexPath, animated: false, scrollPosition: []) @@ -1162,6 +1195,10 @@ final class FavoriteListViewController: UIViewController { present(alert, animated: true, completion: nil) } + + @objc private func importButtonClicked(_ sender: Any) { + openPickerToImport() + } } extension FavoriteListViewController: UICollectionViewDelegate { @@ -1184,7 +1221,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { return } OAFavoritesSwiftHelper.openFavoritePoint(withIdentifier: favorite.bridgeItem.identifier) - case .sortHeader, .backupBanner, .header, .statsFooter: + case .sortHeader, .backupBanner, .header, .statsFooter, .emptyState: break } From c8216148b557a1dbab7d37bef9c6bf30236f4f65 Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Tue, 9 Jun 2026 20:06:29 +0300 Subject: [PATCH 16/41] Add Folder menu fix --- .../en.lproj/Localizable.strings | 1 + .../MyPlaces/FavoriteListViewController.swift | 68 +++++++++---------- .../MyPlaces/OAFavoritesSwiftHelper.h | 4 -- .../MyPlaces/OAFavoritesSwiftHelper.mm | 55 ++------------- 4 files changed, 39 insertions(+), 89 deletions(-) diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index 20bfe34672..b64cdc3951 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -4134,6 +4134,7 @@ "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"; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index c13f790fe6..e0a863a0f3 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -150,11 +150,6 @@ private struct FavoriteFolderStats: Hashable { } } -private enum FavoriteGroupEditContext { - case movingGroup(String) - case movingItems([Any]) -} - final class FavoriteListViewController: UIViewController { private typealias DataSource = UICollectionViewDiffableDataSource private typealias Snapshot = NSDiffableDataSourceSnapshot @@ -180,7 +175,7 @@ final class FavoriteListViewController: UIViewController { private let appearanceCollection: OAGPXAppearanceCollection = .sharedInstance() private var groupController: OAEditGroupViewController? private var colorController: OAEditColorViewController? - private var groupEditContext: FavoriteGroupEditContext? + private var favoriteItemsToMove: [Any]? private var addToTrackGroupName: String? private var addToTrackFavoriteItems: [Any]? @@ -354,12 +349,23 @@ final class FavoriteListViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + definesPresentationContext = true configureNavigation() navigationController?.setToolbarHidden(true, animated: false) configureToolbar() applySnapshot() } + override func viewWillDisappear(_ animated: Bool) { + if !isRootFolder { + navigationItem.searchController = nil + navigationController?.setNavigationBarHidden(true, animated: false) + } + + definesPresentationContext = false + super.viewWillDisappear(animated) + } + 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)]) @@ -410,6 +416,14 @@ final class FavoriteListViewController: UIViewController { private 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() @@ -763,16 +777,6 @@ final class FavoriteListViewController: UIViewController { navigationController?.pushViewController(viewController, animated: true) } - private func openFavoriteGroupMove(_ groupName: String) { - let groupNames = OAFavoritesSwiftHelper.favoriteGroupsToMove(forGroupName: groupName) - guard let navigationController, let groupController = OAEditGroupViewController(groupName: nil, groups: groupNames) else { return } - self.groupController = groupController - groupEditContext = .movingGroup(groupName) - groupController.delegate = self - let modalNavigationController = UINavigationController(rootViewController: groupController) - navigationController.present(modalNavigationController, animated: true) - } - private func openFavoriteItemsMove(_ favoriteItems: [Any]) { guard !favoriteItems.isEmpty, let navigationController, @@ -780,7 +784,7 @@ final class FavoriteListViewController: UIViewController { return } self.groupController = groupController - groupEditContext = .movingItems(favoriteItems) + favoriteItemsToMove = favoriteItems groupController.delegate = self let modalNavigationController = UINavigationController(rootViewController: groupController) navigationController.present(modalNavigationController, animated: true) @@ -861,19 +865,20 @@ final class FavoriteListViewController: UIViewController { 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 = OAFavoritesSwiftHelper.shareFavoriteGroupName(folder.bridgeItem.groupName) else { return } + guard let favoritesUrl = OAFavoritesSwiftHelper.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.openFavoriteGroupMove(folder.bridgeItem.groupName) + self.openFavoriteItemsMove([folder.bridgeItem]) } - let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: folder.bridgeItem.groupName.isEmpty ? [shareAction] : [shareAction, moveAction]) + let thirdButtons: [UIMenuElement] = (folder.bridgeItem.pointsCount > 0 ? [shareAction] : []) + (folder.bridgeItem.groupName.isEmpty ? [] : [moveAction]) + let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: thirdButtons) let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { _ in - OAFavoritesSwiftHelper.addFavoriteGroup(toMapMarkers: folder.bridgeItem.groupName) + OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: [folder.bridgeItem]) } let trackAction = UIAction(title: localizedString("add_to_a_track"), image: .icCustomTrip) { [weak self] _ in guard let self else { return } @@ -883,8 +888,9 @@ final class FavoriteListViewController: UIViewController { guard let self else { return } OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: self.favoritePointRows(forGroupName: folder.bridgeItem.groupName).map { $0.bridgeItem }) } - let addToMenu = UIMenu(title: localizedString("shared_string_add"), image: .icCustomAdd, children: [mapMarkersAction, trackAction, navigationAction]) - let fourthButtonsSection = UIMenu(title: "", options: .displayInline, children: [addToMenu]) + let addToActions: [UIMenuElement] = folder.bridgeItem.pointsCount > 0 ? [mapMarkersAction, trackAction, navigationAction] : [] + let fourthButtons: [UIMenuElement] = addToActions.isEmpty ? [] : [UIMenu(title: localizedString("shared_string_add"), 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 } @@ -892,7 +898,7 @@ final class FavoriteListViewController: UIViewController { } let lastButtonsSection = UIMenu(title: "", options: .displayInline, children: [deleteAction]) - return UIMenu(title: "", children: [firstButtonsSection, secondButtonsSection, thirdButtonsSection, fourthButtonsSection, lastButtonsSection]) + return UIMenu(title: "", children: [firstButtonsSection, secondButtonsSection, thirdButtonsSection, fourthButtonsSection, lastButtonsSection].filter { !$0.children.isEmpty }) } private func showRenameAlert(for folder: FavoriteFolderRow) { @@ -923,7 +929,7 @@ final class FavoriteListViewController: UIViewController { } private func showDeleteAlert(for folder: FavoriteFolderRow) { - let message = String(format: localizedString("permanent_delete_warning"), "\"\(folder.title)\"") + 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 OAFavoritesSwiftHelper.deleteFavoriteGroup(folder.bridgeItem.groupName) else { return } @@ -1312,20 +1318,14 @@ extension FavoriteListViewController: OAEditGroupViewControllerDelegate { guard let groupController else { return } defer { self.groupController = nil - groupEditContext = nil + favoriteItemsToMove = nil } guard groupController.saveChanges else { return } let targetGroupName = groupController.groupName ?? "" - switch groupEditContext { - case .movingGroup(let groupName): - OAFavoritesSwiftHelper.moveFavoriteGroup(groupName, toGroupName: targetGroupName) - case .movingItems(let favoriteItems): - OAFavoritesSwiftHelper.moveFavoriteItems(favoriteItems, toGroupName: targetGroupName) - case .none: - return - } + guard let favoriteItemsToMove else { return } + OAFavoritesSwiftHelper.moveFavoriteItems(favoriteItemsToMove, toGroupName: targetGroupName) setEdit(false) applySnapshot(animatingDifferences: true) } diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h index 5c69933a90..e062dd853a 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h @@ -57,23 +57,19 @@ NS_ASSUME_NONNULL_BEGIN color:(nullable UIColor *)color backgroundIconName:(nullable NSString *)backgroundIconName; + (void)renameFavoriteGroup:(NSString *)groupName newName:(NSString *)newName; -+ (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; + (void)moveFavoriteItems:(NSArray *)favoriteItems toGroupName:(NSString *)targetGroupName; + (NSArray *)favoriteGroupNamesForMovingFavoriteItems:(NSArray *)favoriteItems; + (void)changeFavoriteItems:(NSArray *)favoriteItems colorIndex:(NSInteger)colorIndex; + (OASGpxUtilitiesPointsGroup *)pointsGroupForGroupName:(NSString *)groupName; -+ (NSArray *)favoriteGroupsToMoveForGroupName:(NSString *)groupName; + (BOOL)canUseGroupWithName:(NSString *)groupName; -+ (nullable NSURL *)shareFavoriteGroupName:(NSString *)groupName; + (nullable NSURL *)shareFavoriteItems:(NSArray *)favoriteItems; + (BOOL)deleteFavoriteGroup:(NSString *)groupName; + (BOOL)deleteFavoriteItems:(NSArray *)favoriteItems; + (void)openFavoritePointWithIdentifier:(NSString *)identifier; -+ (void)addFavoriteGroupToMapMarkers:(NSString *)groupName; + (void)addFavoriteItemsToMapMarkers:(NSArray *)favoriteItems; + (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(nullable NSString *)gpxFileName; + (void)addFavoriteGroupToNavigation:(NSString *)groupName; diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm index fc2f50a906..211955fa58 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm @@ -123,6 +123,7 @@ + (NSUInteger)subtreePointsCountForGroupName:(NSString *)groupName groups:(NSArr + (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier; + (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName; + (BOOL)renameFavoriteGroupTreeFromGroupName:(NSString *)sourceGroupName toGroupName:(NSString *)targetGroupName; ++ (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; + (BOOL)isGroupName:(NSString *)groupName insideOrEqualToGroupName:(NSString *)parentGroupName; + (NSString *)groupNameByMovingGroupName:(NSString *)groupName toParentGroupName:(NSString *)parentGroupName; + (NSString *)suffixForGroupName:(NSString *)groupName parentGroupName:(NSString *)parentGroupName; @@ -437,42 +438,12 @@ + (OASGpxUtilitiesPointsGroup *)pointsGroupForGroupName:(NSString *)groupName return group ? [group toPointsGroup] : nil; } -+ (NSArray *)favoriteGroupsToMoveForGroupName:(NSString *)groupName -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) - return nil; - - NSMutableArray *groupNames = [NSMutableArray array]; - for (OAFavoriteGroup *favoriteGroup in [OAFavoritesHelper getFavoriteGroups]) - { - NSString *favoriteGroupName = favoriteGroup.name ?: @""; - if (![self isGroupName:favoriteGroupName insideOrEqualToGroupName:group.name ?: @""]) - [groupNames addObject:favoriteGroupName]; - } - - if (![groupNames containsObject:@""]) - [groupNames addObject:@""]; - - return [groupNames copy]; -} - + (BOOL)canUseGroupWithName:(NSString *)groupName { OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; return group && group.points.count > 0; } -+ (nullable NSURL *)shareFavoriteGroupName:(NSString *)groupName -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) - return nil; - - OAFavoriteGroup *groupToShare = [self favoriteGroupForSharingGroup:group points:group.points.copy]; - return [self fileURLForSharingFavoriteGroups:@[groupToShare]]; -} - + (nullable NSURL *)shareFavoriteItems:(NSArray *)favoriteItems { if (favoriteItems.count == 0) @@ -653,23 +624,6 @@ + (void)openFavoritePointWithIdentifier:(NSString *)identifier [rootViewController.mapPanel openTargetViewWithFavorite:favorite pushed:YES]; } -+ (void)addFavoriteGroupToMapMarkers:(NSString *)groupName -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) - return; - - OAMapPanelViewController *mapPanel = [OARootViewController instance].mapPanel; - for (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) - { - CLLocation *location = [self locationForFavorite:favorite]; - if (!location) - continue; - - [mapPanel addMapMarker:location.coordinate.latitude lon:location.coordinate.longitude description:[favorite getDisplayName]]; - } -} - + (void)addFavoriteItemsToMapMarkers:(NSArray *)favoriteItems { if (favoriteItems.count == 0) @@ -735,7 +689,8 @@ + (void)addFavoriteItemsToMapMarkers:(NSArray *)favoriteItems + (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(nullable NSString *)gpxFileName { - NSArray *points = [self sortedFavoritePointsForGroup:[self favoriteGroupWithName:groupName]]; + OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; + NSArray *points = [self sortedFavoritePointsForGroup:group]; if (points.count == 0) return; @@ -761,9 +716,7 @@ + (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(nullable NSSt if (!gpxFile) return; - for (OAFavoriteItem *favorite in points) - [gpxFile addPointPoint:[favorite toWpt]]; - + [gpxFile addPointsGroupGroup:[group toPointsGroup]]; [OASGpxUtilities.shared writeGpxFileFile:dataItem.file gpxFile:gpxFile]; [gpxDatabase updateDataItem:dataItem]; [OASelectedGPXHelper.instance markTrackForReload:dataItem.file.absolutePath]; From d602207058a06cc35c13f790762e6f83d71835a1 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Wed, 10 Jun 2026 10:41:31 +0300 Subject: [PATCH 17/41] Update FavoriteListViewController.swift --- Sources/Controllers/MyPlaces/FavoriteListViewController.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index e0a863a0f3..4c41b2bb2c 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -1157,6 +1157,7 @@ final class FavoriteListViewController: UIViewController { } updateNavigationBarTitle() + configureToolbar() } @objc private func favoriteDataDidChange() { From e4b1fb1dc4f4f51c060968b26115845aac9e699c Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Wed, 10 Jun 2026 10:50:27 +0300 Subject: [PATCH 18/41] Add selected title --- .../MyPlaces/FavoriteListViewController.swift | 23 +++++++++++++++---- .../MyPlacesContainerViewController.swift | 11 ++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 4c41b2bb2c..9c909ff6dc 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -496,10 +496,15 @@ final class FavoriteListViewController: UIViewController { private func updateNavigationBarTitle() { if collectionView.isEditing { - let selectedItemsCount = collectionView.indexPathsForSelectedItems?.count ?? 0 - let itemText = localizedString(selectedItemsCount > 1 ? "shared_string_items" : "shared_string_item").lowercased() - let title = selectedItemsCount == 0 ? localizedString("select_items") : "\(selectedItemsCount) \(itemText)" - setNavigationTitle(title, subtitle: "", hideSubtitle: true) + 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) } @@ -507,7 +512,9 @@ final class FavoriteListViewController: UIViewController { private func setNavigationTitle(_ title: String, subtitle: String, hideSubtitle: Bool) { if isRootFolder { - myPlacesDelegate?.updateTitle?(title, hideSubtitle: hideSubtitle) + 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)) } @@ -1009,6 +1016,12 @@ final class FavoriteListViewController: UIViewController { return String(format: localizedString("mixed_delete_confirmation_message"), folders.count, pointsCount) } + private 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 + } + private func bridgeItems(for indexPaths: [IndexPath]) -> [Any] { indexPaths.compactMap { indexPath in guard let item = dataSource.itemIdentifier(for: indexPath) else { return nil } diff --git a/Sources/Controllers/MyPlaces/MyPlacesContainerViewController.swift b/Sources/Controllers/MyPlaces/MyPlacesContainerViewController.swift index 4239f87f88..240454413a 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]?) } @@ -200,10 +201,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)) } @@ -304,6 +309,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 From 732bd07db908393652d1562854ce4d192bbffd66 Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Wed, 10 Jun 2026 15:57:25 +0300 Subject: [PATCH 19/41] Add Search --- .../en.lproj/Localizable.strings | 1 + .../MyPlaces/FavoriteListViewController.swift | 247 +++++++++++++++--- 2 files changed, 206 insertions(+), 42 deletions(-) diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index b64cdc3951..5a0e9361e2 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -1016,6 +1016,7 @@ "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"; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 9c909ff6dc..d7b045d242 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -41,7 +41,7 @@ private enum FavoriteListSection: Hashable { } private enum FavoriteListItem: Hashable { - case sortHeader(FavoriteSortMode) + case sortHeader(FavoriteSortHeader) case backupBanner case header(FavoriteFolderSection) case folder(FavoriteFolderRow) @@ -50,6 +50,11 @@ private enum FavoriteListItem: Hashable { case emptyState } +private struct FavoriteSortHeader: Hashable { + let sortMode: FavoriteSortMode + let includesDistanceSortModes: Bool +} + private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { private static let subtitleDateFormatter: DateFormatter = { let formatter = DateFormatter() @@ -181,8 +186,12 @@ final class FavoriteListViewController: UIViewController { private var searchText = "" private var isSearchActive = false + private var isSelectionModeInSearch = false + private var isSearchResultsMode: Bool { + isSearchActive || isSelectionModeInSearch + } private var isAvailablePaymentBanner: Bool { - isRootFolder && !isSearchActive && !UserDefaults.standard.bool(forKey: Self.wasClosedFreeBackupFavoritesBannerKey) && !OAIAPHelper.isOsmAndProAvailable() && !OABackupHelper.sharedInstance().isRegistered() + isRootFolder && !isSearchResultsMode && !UserDefaults.standard.bool(forKey: Self.wasClosedFreeBackupFavoritesBannerKey) && !OAIAPHelper.isOsmAndProAvailable() && !OABackupHelper.sharedInstance().isRegistered() } private var isRootFolder: Bool { guard case .root = screenMode else { return false } @@ -208,8 +217,15 @@ final class FavoriteListViewController: UIViewController { guard case .folder(let folder, _) = screenMode, !folder.bridgeItem.groupName.isEmpty else { return nil } return folder.bridgeItem.groupName } + private var searchParentGroupName: String? { + guard case .folder(let folder, _) = screenMode else { return nil } + return folder.bridgeItem.groupName + } private var currentSortMode: FavoriteSortMode { - isSearchActive ? searchFavoriteSortMode() : favoriteSortMode() + isSearchResultsMode ? searchFavoriteSortMode() : favoriteSortMode() + } + private var currentSortHeader: FavoriteSortHeader { + FavoriteSortHeader(sortMode: currentSortMode, includesDistanceSortModes: isSearchResultsMode || !isRootFolder) } private var currentSortEntryId: String { parentGroupName ?? "" @@ -236,9 +252,9 @@ final class FavoriteListViewController: UIViewController { cell.accessories = [.outlineDisclosure(options: disclosureOptions)] cell.tintColor = .iconColorActive } - private lazy var sortHeaderCellRegistration = UICollectionView.CellRegistration { [weak self] cell, _, sortMode in - cell.sortButton.setImage(sortMode.image, for: .normal) - cell.sortButton.menu = self?.makeSortMenu() + private lazy var sortHeaderCellRegistration = UICollectionView.CellRegistration { [weak self] cell, _, sortHeader in + cell.sortButton.setImage(sortHeader.sortMode.image, for: .normal) + cell.sortButton.menu = self?.makeSortMenu(includesDistanceSortModes: sortHeader.includesDistanceSortModes) } private lazy var backupBannerCellRegistration = UICollectionView.CellRegistration { [weak self] cell, _, _ in cell.contentView.subviews.forEach { $0.removeFromSuperview() } @@ -301,6 +317,16 @@ final class FavoriteListViewController: UIViewController { } private lazy var emptyStateCellRegistration = 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: UIImage.templateImageNamed("ic_custom_search") ?? .icCustomFavorites, + 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"), @@ -436,8 +462,9 @@ final class FavoriteListViewController: UIViewController { 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 selectAllButton = UIBarButtonItem(title: localizedString("shared_string_select_all"), style: .plain, target: self, action: #selector(selectAllButtonPressed)) - selectAllButton.accessibilityLabel = localizedString("shared_string_select_all") + let selectAllTitle = localizedString(areAllSelectableItemsSelected() ? "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 { @@ -475,6 +502,13 @@ final class FavoriteListViewController: UIViewController { } private 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) @@ -494,6 +528,23 @@ final class FavoriteListViewController: UIViewController { } } + 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 updateSelectionUI() { + updateNavigationBarTitle() + configureNavigationButtons() + configureToolbar() + } + private func updateNavigationBarTitle() { if collectionView.isEditing { let selectedItems = bridgeItems(for: collectionView.indexPathsForSelectedItems ?? []) @@ -521,7 +572,7 @@ final class FavoriteListViewController: UIViewController { } private func updateSegmentedControlVisibility() { - myPlacesDelegate?.updateSegmentedControlVisibility(isRootFolder && !collectionView.isEditing && !isSearchActive) + myPlacesDelegate?.updateSegmentedControlVisibility(isRootFolder && !collectionView.isEditing && !isSearchResultsMode) } private func favoriteSortMode(entryId: String? = nil) -> FavoriteSortMode { @@ -536,7 +587,7 @@ final class FavoriteListViewController: UIViewController { } private func setFavoriteSortMode(_ sortMode: FavoriteSortMode) { - if isSearchActive { + if isSearchResultsMode { settings.searchFavoriteSortMode.set(sortMode.title) } else { var sortModes = settings.getFavoriteSortModes() @@ -560,8 +611,8 @@ final class FavoriteListViewController: UIViewController { settings.saveFavoriteSortModes(sortModes) } - private func makeSortMenu() -> UIMenu { - let modes: [FavoriteSortMode] = isRootFolder && !isSearchActive ? [.lastModified, .nameAZ, .nameZA, .newestDateFirst, .oldestDateFirst] : FavoriteSortMode.allCases + private 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) } @@ -587,8 +638,8 @@ final class FavoriteListViewController: UIViewController { let emptyStateCellRegistration = emptyStateCellRegistration return DataSource(collectionView: collectionView) { collectionView, indexPath, item in switch item { - case .sortHeader(let sortMode): - return collectionView.dequeueConfiguredReusableCell(using: sortHeaderCellRegistration, for: indexPath, item: sortMode) + 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): @@ -616,6 +667,11 @@ final class FavoriteListViewController: UIViewController { private func applyRootSnapshot(animatingDifferences: Bool) { let allFolders = favoriteFolders() + if isSearchResultsMode { + applySearchSnapshot(allFolders: allFolders, parentGroupName: nil, animatingDifferences: animatingDifferences) + return + } + var snapshot = Snapshot() if allFolders.isEmpty { layoutSections = [] @@ -643,7 +699,7 @@ final class FavoriteListViewController: UIViewController { layoutSections = sections collectionView.collectionViewLayout.invalidateLayout() snapshot.appendSections(sections) - snapshot.appendItems([.sortHeader(currentSortMode)], toSection: .sortHeader) + snapshot.appendItems([.sortHeader(currentSortHeader)], toSection: .sortHeader) if isPaymentBannerVisible { snapshot.appendItems([.backupBanner], toSection: .backupBanner) } @@ -666,6 +722,11 @@ final class FavoriteListViewController: UIViewController { 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(OAFavoritesSwiftHelper.favoritePoints(forGroupName: folder.bridgeItem.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.bridgeItem.address) }, mode: currentSortMode) var snapshot = Snapshot() @@ -681,7 +742,7 @@ final class FavoriteListViewController: UIViewController { layoutSections = stats == nil ? [.sortHeader, .content] : [.sortHeader, .content, .statsFooter] collectionView.collectionViewLayout.invalidateLayout() snapshot.appendSections(layoutSections) - snapshot.appendItems([.sortHeader(currentSortMode)], toSection: .sortHeader) + 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 { @@ -691,6 +752,26 @@ final class FavoriteListViewController: UIViewController { 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) + var snapshot = Snapshot() + if favorites.isEmpty { + layoutSections = [] + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections([.emptyState]) + snapshot.appendItems([.emptyState]) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + return + } + + 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 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 }] @@ -702,7 +783,7 @@ final class FavoriteListViewController: UIViewController { sections.append(.pinned) } - if !isSearchActive || !(foldersBySection[.visible] ?? []).isEmpty { + if !isSearchResultsMode || !(foldersBySection[.visible] ?? []).isEmpty { sections.append(.visible) } @@ -723,7 +804,7 @@ final class FavoriteListViewController: UIViewController { } private func folderStats(allFolders: [FavoriteFolderRow], currentGroupName: String?) -> FavoriteFolderStats? { - guard !isSearchActive else { return nil } + 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 } @@ -768,7 +849,52 @@ final class FavoriteListViewController: UIViewController { private func matchesSearch(_ text: String?) -> Bool { guard !searchText.isEmpty else { return true } - return text?.localizedCaseInsensitiveContains(searchText) ?? false + return text?.range(of: searchText, options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive], locale: Locale.current) != nil + } + + private func hasSearchResults() -> Bool { + !searchFavoritePointRows(allFolders: favoriteFolders(), parentGroupName: searchParentGroupName).isEmpty + } + + private func shouldHideSearchToolbar() -> Bool { + !collectionView.isEditing && (!isSearchActive || !hasSearchResults()) + } + + private func searchFavoritePointRows(allFolders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoritePointRow] { + favoritePointRows(allFolders: allFolders, parentGroupName: parentGroupName).filter { matchesSearch($0.title) } + } + + private func clearSearchControllerText() { + if isRootFolder { + navigationController?.navigationBar.topItem?.searchController?.searchBar.text = "" + } else { + subfolderSearchController.searchBar.text = "" + } + } + + private func selectableIndexPaths() -> [IndexPath] { + var indexPaths: [IndexPath] = [] + for section in 0.. Bool { + let selectableIndexPaths = selectableIndexPaths() + guard !selectableIndexPaths.isEmpty else { return false } + let selectedIndexPaths = Set(collectionView.indexPathsForSelectedItems ?? []) + return selectableIndexPaths.allSatisfy { selectedIndexPaths.contains($0) } } private func openNewFavoriteGroupEditor() { @@ -816,16 +942,26 @@ final class FavoriteListViewController: UIViewController { } private func favoritePointRows(forGroupName groupName: String) -> [FavoritePointRow] { - let sortMode = isSearchActive ? searchFavoriteSortMode() : favoriteSortMode(entryId: groupName) + let sortMode = isSearchResultsMode ? searchFavoriteSortMode() : favoriteSortMode(entryId: groupName) let favorites = OAFavoritesSwiftHelper.favoritePoints(forGroupName: groupName).map { FavoritePointRow(item: $0) } return FavoriteSortModeHelper.sortFavoritePointsWithMode(favorites, mode: sortMode) } + private func favoritePointRows(allFolders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoritePointRow] { + allFolders.filter { isSearchGroup($0.bridgeItem.groupName, parentGroupName: parentGroupName) }.flatMap { OAFavoritesSwiftHelper.favoritePoints(forGroupName: $0.bridgeItem.groupName).map { FavoritePointRow(item: $0) } } + } + + private func isSearchGroup(_ groupName: String, parentGroupName: String?) -> Bool { + guard let parentGroupName else { return true } + guard !parentGroupName.isEmpty else { return groupName.isEmpty } + return groupName == parentGroupName || isNestedFolder(groupName, in: parentGroupName) + } + private 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.resizedMenuImage()) { [weak self] _ in + let importAction = UIAction(title: localizedString("shared_string_import"), image: .icCustomImportOutlined) { [weak self] _ in self?.openPickerToImport() } @@ -835,8 +971,12 @@ final class FavoriteListViewController: UIViewController { } private func setEdit(_ isEdit: Bool) { + let shouldResetSearchSelection = !isEdit && isSelectionModeInSearch if !isEdit { collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) } + isSelectionModeInSearch = false + isSearchActive = false + searchText = "" } collectionView.isEditing = isEdit @@ -844,6 +984,10 @@ final class FavoriteListViewController: UIViewController { myPlacesDelegate?.updateEditMode(isEdit) configureNavigation() navigationController?.setToolbarHidden(!isEdit, animated: true) + if shouldResetSearchSelection { + clearSearchControllerText() + applySnapshot(animatingDifferences: false) + } } private func makeFolderContextMenu(for folder: FavoriteFolderRow, indexPath: IndexPath) -> UIMenu { @@ -1151,26 +1295,32 @@ final class FavoriteListViewController: UIViewController { setEdit(true) } + @objc private func searchSelectButtonPressed() { + isSelectionModeInSearch = true + isSearchActive = false + if isRootFolder { + let searchController = navigationController?.navigationBar.topItem?.searchController + searchController?.isActive = false + } else { + subfolderSearchController.isActive = false + } + + selectButtonPressed() + } + @objc private func cancelButtonPressed() { setEdit(false) } @objc private func selectAllButtonPressed() { - for section in 0.. UIContextMenuConfiguration? { @@ -1297,15 +1452,23 @@ extension FavoriteListViewController: MyPlacesSearchable, UISearchResultsUpdatin func searchResults(for searchController: UISearchController) { isSearchActive = searchController.isActive - searchText = searchController.searchBar.searchTextField.text ?? "" + if isSearchActive || !isSelectionModeInSearch { + searchText = searchController.searchBar.searchTextField.text ?? "" + } updateSegmentedControlVisibility() + configureToolbar() + navigationController?.setToolbarHidden(shouldHideSearchToolbar(), animated: true) applySnapshot(animatingDifferences: false) } func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { isSearchActive = false - searchText = "" + if !isSelectionModeInSearch { + searchText = "" + } updateSegmentedControlVisibility() + configureToolbar() + navigationController?.setToolbarHidden(!collectionView.isEditing, animated: true) applySnapshot(animatingDifferences: false) } } From 00d0c87e98f354bae2143f6006496964c429b26f Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Thu, 11 Jun 2026 13:52:23 +0300 Subject: [PATCH 20/41] Sort Folders fix --- .../MyPlaces/FavoriteListViewController.swift | 65 +++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index d7b045d242..70001a9a69 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -181,9 +181,10 @@ final class FavoriteListViewController: UIViewController { private var groupController: OAEditGroupViewController? private var colorController: OAEditColorViewController? private var favoriteItemsToMove: [Any]? + private var favoriteGroupAppearanceGroupName: String? + private var favoriteGroupAppearanceEditor: OAFavoriteGroupEditorViewController? private var addToTrackGroupName: String? private var addToTrackFavoriteItems: [Any]? - private var searchText = "" private var isSearchActive = false private var isSelectionModeInSearch = false @@ -602,7 +603,7 @@ final class FavoriteListViewController: UIViewController { var sortModes = settings.getFavoriteSortModes() let keysToRemove = sortModes.keys.filter { key in groupNames.contains { groupName in - key == groupName || (!groupName.isEmpty && key.hasPrefix(groupName + "/")) + isFavoriteSortModeKey(key, insideOrEqualTo: groupName) } } @@ -611,6 +612,50 @@ final class FavoriteListViewController: UIViewController { settings.saveFavoriteSortModes(sortModes) } + private func renameFavoriteSortModeKeys(from oldGroupName: String, to newGroupName: String, existingGroupNames: Set? = nil) { + guard !oldGroupName.isEmpty, oldGroupName != newGroupName else { return } + let groupNames = existingGroupNames ?? Set(OAFavoritesSwiftHelper.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) + } + + private func updateFavoriteSortModeKeysAfterMove(_ favoriteItems: [Any], toGroupName targetGroupName: String) { + let groupNames = Set(OAFavoritesSwiftHelper.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) + } + } + + private 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(OAFavoritesSwiftHelper.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), OAFavoritesSwiftHelper.addFavoriteGroup(folderName, parentGroupName: parentGroupName.isEmpty ? nil : parentGroupName, iconName: nil, color: nil, backgroundIconName: nil) { + existingGroupNames.insert(newGroupName) + } + parentGroupName = newGroupName + } + } + + private func isFavoriteSortModeKey(_ key: String, insideOrEqualTo groupName: String) -> Bool { + key == groupName || (!groupName.isEmpty && key.hasPrefix(groupName + "/")) + } + private 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]] @@ -906,6 +951,8 @@ final class FavoriteListViewController: UIViewController { private func openFavoriteGroupAppearance(_ groupName: String) { guard let viewController = OAFavoriteGroupEditorViewController(group: OAFavoritesSwiftHelper.pointsGroup(forGroupName: groupName)) else { return } + favoriteGroupAppearanceGroupName = groupName + favoriteGroupAppearanceEditor = viewController viewController.delegate = self navigationController?.pushViewController(viewController, animated: true) } @@ -1056,8 +1103,10 @@ final class FavoriteListViewController: UIViewController { 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 text = alert?.textFields?.first?.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return } - let newGroupName = self?.groupName(folder.bridgeItem.groupName, replacingLastComponentWith: text) ?? text - OAFavoritesSwiftHelper.renameFavoriteGroup(folder.bridgeItem.groupName, newName: newGroupName) + let oldGroupName = folder.bridgeItem.groupName + let newGroupName = self?.groupName(oldGroupName, replacingLastComponentWith: text) ?? text + OAFavoritesSwiftHelper.renameFavoriteGroup(oldGroupName, newName: newGroupName) + self?.renameFavoriteSortModeKeys(from: oldGroupName, to: newGroupName) self?.applySnapshot(animatingDifferences: true) } @@ -1502,7 +1551,9 @@ extension FavoriteListViewController: OAEditGroupViewControllerDelegate { let targetGroupName = groupController.groupName ?? "" guard let favoriteItemsToMove else { return } + createFavoriteMoveTargetGroupIfNeeded(targetGroupName, favoriteItems: favoriteItemsToMove) OAFavoritesSwiftHelper.moveFavoriteItems(favoriteItemsToMove, toGroupName: targetGroupName) + updateFavoriteSortModeKeysAfterMove(favoriteItemsToMove, toGroupName: targetGroupName) setEdit(false) applySnapshot(animatingDifferences: true) } @@ -1531,6 +1582,12 @@ extension FavoriteListViewController: OAEditorDelegate { } func onEditorUpdated() { + if let oldGroupName = favoriteGroupAppearanceGroupName, let newGroupName = favoriteGroupAppearanceEditor?.editName { + renameFavoriteSortModeKeys(from: oldGroupName, to: newGroupName) + } + + favoriteGroupAppearanceGroupName = nil + favoriteGroupAppearanceEditor = nil applySnapshot(animatingDifferences: true) } From 5567c9b199ee2775dbd25acdd5b29093648caa88 Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Thu, 11 Jun 2026 18:45:24 +0300 Subject: [PATCH 21/41] Import fix --- .../OAFavoriteImportViewController.mm | 25 --------- Sources/Helpers/OAFavoritesHelper.mm | 51 ++++++++++++++++++- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm b/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm index 3f275b0fb0..c1b9437cb2 100644 --- a/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm +++ b/Sources/Controllers/TargetMenu/Favorites/OAFavoriteImportViewController.mm @@ -159,8 +159,6 @@ - (void)onRightNavbarButtonPressed { if (_gpxFile && _gpxFile.pointsGroups.count > 0) { - // IOS-214 - [self normalizeSingleNestedFavoriteGroupIfNeeded]; if (![self isFavoritesValid]) return; @@ -184,29 +182,6 @@ - (void)dismissViewController #pragma mark - Additions -- (void)normalizeSingleNestedFavoriteGroupIfNeeded -{ - NSArray *groupKeys = _gpxFile.pointsGroups.allKeys; - if (groupKeys.count != 1) - return; - - NSString *groupKey = groupKeys.firstObject; - OASGpxUtilitiesPointsGroup *pointsGroup = _gpxFile.pointsGroups[groupKey]; - NSString *groupName = pointsGroup.name ?: groupKey; - NSArray *components = [groupName componentsSeparatedByString:@"/"]; - if (components.count < 2) - return; - - NSString *targetGroupName = components.lastObject; - if (targetGroupName.length == 0) - return; - - for (OASWptPt *point in pointsGroup.points) - { - point.category = targetGroupName; - } -} - - (BOOL)isFavoritesValid { if (!_gpxFile) diff --git a/Sources/Helpers/OAFavoritesHelper.mm b/Sources/Helpers/OAFavoritesHelper.mm index 5a263d71d2..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 From c1297ee2513d532fff50ffbdbbf0f5cd66236d7b Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Thu, 11 Jun 2026 19:13:41 +0300 Subject: [PATCH 22/41] updated favorite point --- .../en.lproj/Localizable.strings | 1 + .../MyPlaces/FavoriteListViewController.swift | 341 +++++++++++++++++- .../MyPlaces/FavoriteSortModeHelper.swift | 4 + .../MyPlaces/OAFavoritesSwiftHelper.h | 13 +- .../MyPlaces/OAFavoritesSwiftHelper.mm | 105 +++++- .../TargetMenu/OAEditPointViewController.h | 7 + .../TargetMenu/OAEditPointViewController.mm | 1 + Sources/OsmAnd Maps-Bridging-Header.h | 2 + .../DateFormatter+Extension.swift | 8 + 9 files changed, 474 insertions(+), 8 deletions(-) diff --git a/Resources/Localizations/en.lproj/Localizable.strings b/Resources/Localizations/en.lproj/Localizable.strings index 9619ad3996..d6a15fcfb9 100644 --- a/Resources/Localizations/en.lproj/Localizable.strings +++ b/Resources/Localizations/en.lproj/Localizable.strings @@ -1076,6 +1076,7 @@ "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"; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 70001a9a69..62ef150ab9 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -185,9 +185,14 @@ final class FavoriteListViewController: UIViewController { private var favoriteGroupAppearanceEditor: OAFavoriteGroupEditorViewController? private var addToTrackGroupName: String? private var addToTrackFavoriteItems: [Any]? + private var pointToShare: OAFavoritePointBridgeItem? private var searchText = "" private var isSearchActive = false private var isSelectionModeInSearch = false + private var isDecelerating = false + private var lastDistanceDirectionUpdate: TimeInterval = 0.0 + private var locationUpdateObserver: OAAutoObserverProxy? + private var headingUpdateObserver: OAAutoObserverProxy? private var isSearchResultsMode: Bool { isSearchActive || isSelectionModeInSearch } @@ -293,10 +298,12 @@ final class FavoriteListViewController: UIViewController { content.directionalLayoutMargins = Self.rowContentInsets 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.secondaryText = favorite.bridgeItem.address + 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()] @@ -335,6 +342,7 @@ final class FavoriteListViewController: UIViewController { cell.button.setTitle(localizedString("shared_string_import"), for: .normal) cell.button.addTarget(self, action: #selector(self.importButtonClicked), for: .touchUpInside) } + private lazy var subfolderSearchController: UISearchController = { let searchController = UISearchController(searchResultsController: nil) searchController.searchResultsUpdater = self @@ -345,6 +353,144 @@ final class FavoriteListViewController: UIViewController { }() private lazy var dataSource: DataSource = makeDataSource() + private func favoriteSecondaryAttributedText(for favorite: FavoritePointRow, includesGroupName: Bool) -> NSAttributedString { + let font = UIFont.systemFont(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 = rotatedFavoriteDirectionIcon(directionIcon, radians: 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) + } + + private func rotatedFavoriteDirectionIcon(_ image: UIImage, radians: CGFloat) -> UIImage { + let format = UIGraphicsImageRendererFormat() + format.scale = image.scale + format.opaque = false + let renderer = UIGraphicsImageRenderer(size: image.size, format: format) + return renderer.image { context in + let rect = CGRect(origin: CGPoint(x: -image.size.width / 2.0, y: -image.size.height / 2.0), + size: image.size) + context.cgContext.translateBy(x: image.size.width / 2.0, y: image.size.height / 2.0) + context.cgContext.rotate(by: radians) + image.draw(in: rect) + } + } + + 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 + } + + @objc private func updateDistanceAndDirection() { + updateDistanceAndDirection(false) + } + + private func updateDistanceAndDirection(_ forceUpdate: Bool) { + guard Thread.isMainThread else { + DispatchQueue.main.async { [weak self] in + self?.updateDistanceAndDirection(forceUpdate) + } + return + } + + guard !collectionView.isEditing + && !isDecelerating + && 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 + applySnapshot(animatingDifferences: false) + } + convenience init(frame: CGRect) { self.init(frame: frame, screenMode: .root) } @@ -361,6 +507,7 @@ final class FavoriteListViewController: UIViewController { } deinit { + unregisterDistanceAndDirectionObservers() NotificationCenter.default.removeObserver(self, name: .favoriteImportViewControllerDidDismiss, object: nil) NotificationCenter.default.removeObserver(self, name: Notification.Name(NSNotification.Name.OAIAPProductPurchased.rawValue), object: nil) } @@ -376,14 +523,19 @@ final class FavoriteListViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) + isDecelerating = false definesPresentationContext = true configureNavigation() navigationController?.setToolbarHidden(true, animated: false) configureToolbar() applySnapshot() + registerDistanceAndDirectionObservers() + updateDistanceAndDirection(true) } override func viewWillDisappear(_ animated: Bool) { + unregisterDistanceAndDirectionObservers() + isDecelerating = false if !isRootFolder { navigationItem.searchController = nil navigationController?.setNavigationBarHidden(true, animated: false) @@ -1099,6 +1251,51 @@ final class FavoriteListViewController: UIViewController { return UIMenu(title: "", children: [firstButtonsSection, secondButtonsSection, thirdButtonsSection, fourthButtonsSection, lastButtonsSection].filter { !$0.children.isEmpty }) } + private 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 = OAFavoritesSwiftHelper.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 + OAFavoritesSwiftHelper.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 + OAFavoritesSwiftHelper.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]) + } + private 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 @@ -1141,6 +1338,81 @@ final class FavoriteListViewController: UIViewController { present(alert, animated: true) } + private 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 OAFavoritesSwiftHelper.deleteFavoritePoint(favorite.bridgeItem) else { return } + self?.applySnapshot(animatingDifferences: true) + }) + + alert.addAction(UIAlertAction(title: localizedString("shared_string_cancel"), style: .cancel)) + present(alert, animated: true) + } + + private 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 + } + } + + private func favoritePointShareItems(for point: OAFavoritePointBridgeItem) -> [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: OAFavoritesSwiftHelper.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 = OAFavoritesSwiftHelper.geoURLString(forFavoritePoint: point) + if !geoURLString.isEmpty { + sharingText.append("\n") + sharingText.append("Location: \(geoURLString)") + } + + let shareURLString = OAFavoritesSwiftHelper.sharePoiURLString(forFavoritePoint: point) + if !shareURLString.isEmpty { + sharingText.append("\n") + sharingText.append(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( @@ -1484,14 +1756,69 @@ extension FavoriteListViewController: UICollectionViewDelegate { } func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - guard !collectionView.isEditing else { return nil } - guard let item = dataSource.itemIdentifier(for: indexPath), case .folder(let folder) = item else { return nil } + guard !collectionView.isEditing, let item = dataSource.itemIdentifier(for: indexPath) else { return nil } let menuProvider: UIContextMenuActionProvider = { [weak self] _ in - self?.makeFolderContextMenu(for: folder, indexPath: indexPath) + 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) } + + func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + isDecelerating = true + } + + func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { + guard !decelerate else { return } + isDecelerating = false + updateDistanceAndDirection(true) + } + + func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { + isDecelerating = false + updateDistanceAndDirection(true) + } +} + +extension FavoriteListViewController: OAShareMenuDelegate { + func onCopy(_ type: OAShareMenuActivityType) { + guard let pointToShare else { return } + switch type { + case .clipboard: + copyFavoritePointShareText(OAFavoritesSwiftHelper.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(OAFavoritesSwiftHelper.formattedCoordinates(forFavoritePoint: pointToShare)) + case .geo: + copyFavoritePointShareText(OAFavoritesSwiftHelper.geoURLString(forFavoritePoint: pointToShare)) + default: + break + } + } + + private 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 { @@ -1620,6 +1947,12 @@ extension FavoriteListViewController: OAEditorDelegate { } } +extension FavoriteListViewController: OAEditPointViewControllerDelegate { + func saveTapped() { + applySnapshot() + } +} + extension FavoriteListViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let url = urls.first else { return } diff --git a/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift b/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift index 3d682b84d2..77c6c9b85d 100644 --- a/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift +++ b/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift @@ -54,6 +54,10 @@ protocol FavoriteSortablePoint { case .oldestDateFirst: return .icCustomSortDateOldest } } + + var isDateOriented: Bool { + self == .newestDateFirst || self == .oldestDateFirst + } static func byTitle(_ title: String) -> FavoriteSortMode { allCases.first { $0.title == title } ?? .nameAZ diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h index e062dd853a..a2d408e8a1 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h @@ -10,7 +10,7 @@ NS_ASSUME_NONNULL_BEGIN -@class UIColor, UIImage, OAFavoriteItem, OASGpxUtilitiesPointsGroup; +@class UIColor, UIImage, OAFavoriteItem, OASGpxUtilitiesPointsGroup, OAEditPointViewController; @interface OAFavoriteFolderBridgeItem : NSObject @@ -33,7 +33,13 @@ NS_ASSUME_NONNULL_BEGIN @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; @@ -46,6 +52,9 @@ NS_ASSUME_NONNULL_BEGIN + (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; @@ -67,9 +76,11 @@ NS_ASSUME_NONNULL_BEGIN + (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; diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm index 211955fa58..b194330e80 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm @@ -9,6 +9,7 @@ #import "OAFavoritesSwiftHelper.h" #import "OAAppSettings.h" #import "OAEditGroupViewController.h" +#import "OAEditPointViewController.h" #import "OAFavoriteItem.h" #import "OAFavoriteGroupEditorViewController.h" #import "OAFavoritesHelper.h" @@ -16,6 +17,8 @@ #import "OAIndexConstants.h" #import "OALocationServices.h" #import "OAMapActions.h" +#import "OAMapRendererView.h" +#import "OAMapViewController.h" #import "OAMapPanelViewController.h" #import "OAObservable.h" #import "OAOpenAddTrackViewController.h" @@ -76,6 +79,7 @@ + (NSString *)titleForGroupName:(NSString *)groupName @interface OAFavoritePointBridgeItem () + (nullable NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite; ++ (CGFloat)directionForFavorite:(OAFavoriteItem *)favorite; @end @@ -90,7 +94,13 @@ - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite _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]; @@ -112,6 +122,22 @@ + (nullable NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite 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 @interface OAFavoritesSwiftHelper () @@ -131,6 +157,7 @@ + (NSString *)lastComponentForGroupName:(NSString *)groupName; + (OAFavoriteGroup *)favoriteGroupForSharingGroup:(OAFavoriteGroup *)group points:(NSArray *)points; + (nullable NSURL *)fileURLForSharingFavoriteGroups:(NSArray *)favoriteGroups; + (CLLocation *)locationForFavorite:(OAFavoriteItem *)favorite; ++ (int)currentMapZoomLevel; @end @@ -163,6 +190,37 @@ @implementation OAFavoritesSwiftHelper 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 { OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; @@ -537,6 +595,16 @@ + (BOOL)deleteFavoriteGroup:(NSString *)groupName return [OAFavoritesHelper deleteFavoriteGroups:groupsToDelete andFavoritesItems:nil]; } ++ (BOOL)deleteFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem +{ + OAFavoriteItem *favorite = [self favoritePointWithIdentifier:favoriteItem.identifier]; + if (!favorite) + return NO; + + [OAFavoritesHelper deleteFavorites:@[favorite] saveImmediately:YES]; + return YES; +} + + (BOOL)deleteFavoriteItems:(NSArray *)favoriteItems { if (favoriteItems.count == 0) @@ -602,8 +670,17 @@ + (BOOL)deleteFavoriteItems:(NSArray *)favoriteItems if (groupsToDelete.count == 0 && itemsToDelete.count == 0) return NO; - return [OAFavoritesHelper deleteFavoriteGroups:groupsToDelete.count > 0 ? groupsToDelete : nil - andFavoritesItems:itemsToDelete.count > 0 ? itemsToDelete : nil]; + 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; + } + + return didDelete; } + (void)openFavoritePointWithIdentifier:(NSString *)identifier @@ -624,6 +701,15 @@ + (void)openFavoritePointWithIdentifier:(NSString *)identifier [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) @@ -775,7 +861,8 @@ + (void)addFavoriteItemsToTrack:(NSArray *)favoriteItems gpxFileName:(nullable N continue; OAFavoritePointBridgeItem *pointItem = (OAFavoritePointBridgeItem *) item; - OAFavoriteItem *favorite = [self favoritePointWithIdentifier:pointItem.identifier]; + OAFavoriteItem *favorite = [self standaloneFavoritePoint:[self favoritePointWithIdentifier:pointItem.identifier]]; + if (!favorite) continue; @@ -1107,4 +1194,16 @@ + (CLLocation *)locationForFavorite:(OAFavoriteItem *)favorite 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/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 5525fc1f7a..e2863eb465 100644 --- a/Sources/Controllers/TargetMenu/OAEditPointViewController.mm +++ b/Sources/Controllers/TargetMenu/OAEditPointViewController.mm @@ -1046,6 +1046,7 @@ - (void)onRightNavbarButtonPressed if (_editPointType == EOAEditPointTypeFavorite) [OAAppSettings.sharedManager.lastFavCategoryEntered set:savingGroup]; } + [self.delegate saveTapped]; [self dismissViewController]; } diff --git a/Sources/OsmAnd Maps-Bridging-Header.h b/Sources/OsmAnd Maps-Bridging-Header.h index 91fbb17a92..089f5b9ec1 100644 --- a/Sources/OsmAnd Maps-Bridging-Header.h +++ b/Sources/OsmAnd Maps-Bridging-Header.h @@ -115,6 +115,7 @@ #import "OAOsmBugsDBHelper.h" #import "OAOpenStreetMapPoint.h" #import "OAOsmNotePoint.h" +#import "OAShareMenuActivity.h" // Widgets #import "OAMapWidgetRegistry.h" @@ -226,6 +227,7 @@ #import "OAOsmNoteViewController.h" #import "OAOsmEditingViewController.h" #import "OAOsmUploadPOIViewController.h" +#import "OAEditPointViewController.h" // Cells #import "OAValueTableViewCell.h" 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 + }() } From 3f2b6b803b31c1f241419324f2117f088ca901b8 Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Fri, 12 Jun 2026 10:51:37 +0300 Subject: [PATCH 23/41] OAFavoriteFoldersBridge del --- OsmAnd.xcodeproj/project.pbxproj | 12 +- .../MyPlaces/OAFavoriteFoldersBridge.h | 36 -- .../MyPlaces/OAFavoriteFoldersBridge.mm | 515 ------------------ 3 files changed, 3 insertions(+), 560 deletions(-) delete mode 100644 Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h delete mode 100644 Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index a3029add2a..1fc3e5965b 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -1586,7 +1586,6 @@ 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 */; }; - C5EB56EC2FD188A900D01657 /* OAFavoriteFoldersBridge.mm in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56EB2FD188A900D01657 /* OAFavoriteFoldersBridge.mm */; }; C5EB56EE2FD18B8A00D01657 /* FavoriteListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */; }; C5EB72C42BD12F4D00C50C23 /* RouteParameterDevelopmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EB72C32BD12F4C00C50C23 /* RouteParameterDevelopmentViewController.swift */; }; C5EE465B2E9D1F8B0019C9E0 /* CarPlayNavigationModeProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5EE465A2E9D1F8B0019C9E0 /* CarPlayNavigationModeProvider.swift */; }; @@ -1636,13 +1635,13 @@ CE8A82A92FCFE11F00EADFD8 /* MapVariantReplacementManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE8A82A82FCFE11F00EADFD8 /* MapVariantReplacementManager.swift */; }; D1A0B0012F50000100A0B001 /* OpeningHoursParserTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0AFFE2F50000100A0B001 /* OpeningHoursParserTest.swift */; }; D1A0B0032F50000100A0B001 /* OpeningHoursParserTestSupport.mm in Sources */ = {isa = PBXBuildFile; fileRef = D1A0B0002F50000100A0B001 /* OpeningHoursParserTestSupport.mm */; }; + D1A0B0112F50001100A0B001 /* URLExtractionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0B0102F50001100A0B001 /* URLExtractionTests.swift */; }; + D1A0B0122F50001100A0B001 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3216E9522BA097BD0087D0EF /* StringExtensions.swift */; }; D71B9A8C2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71B9A8B2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift */; }; D71B9A8E2FC96D2D00FBB0F3 /* OrganizeByTypeExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71B9A8D2FC96D2D00FBB0F3 /* OrganizeByTypeExtension.swift */; }; D71B9A902FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71B9A8F2FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift */; }; D7B76D0D2FD16070004EE3E9 /* OrganizeByStepSizeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7B76D0C2FD16070004EE3E9 /* OrganizeByStepSizeViewController.swift */; }; D7BF04782FD2DB4400BABB31 /* TracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7BF04772FD2DB4400BABB31 /* TracksViewController.swift */; }; - D1A0B0112F50001100A0B001 /* URLExtractionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A0B0102F50001100A0B001 /* URLExtractionTests.swift */; }; - D1A0B0122F50001100A0B001 /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3216E9522BA097BD0087D0EF /* StringExtensions.swift */; }; DA0132D42A1E0AB500920C14 /* WidgetsListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0132D32A1E0AB500920C14 /* WidgetsListViewController.swift */; }; DA0132DD2A1E4A6300920C14 /* ic_custom20_screen_side_right@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DA0132D52A1E4A6100920C14 /* ic_custom20_screen_side_right@2x.png */; }; DA0132DE2A1E4A6300920C14 /* ic_custom20_screen_side_top@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = DA0132D62A1E4A6100920C14 /* ic_custom20_screen_side_top@2x.png */; }; @@ -5495,8 +5494,6 @@ 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 = ""; }; - C5EB56EA2FD188A900D01657 /* OAFavoriteFoldersBridge.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OAFavoriteFoldersBridge.h; sourceTree = ""; }; - C5EB56EB2FD188A900D01657 /* OAFavoriteFoldersBridge.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OAFavoriteFoldersBridge.mm; sourceTree = ""; }; C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FavoriteListViewController.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 = ""; }; @@ -5586,12 +5583,12 @@ D1A0AFFE2F50000100A0B001 /* OpeningHoursParserTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpeningHoursParserTest.swift; sourceTree = ""; }; D1A0AFFF2F50000100A0B001 /* OpeningHoursParserTestSupport.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OpeningHoursParserTestSupport.h; sourceTree = ""; }; D1A0B0002F50000100A0B001 /* OpeningHoursParserTestSupport.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OpeningHoursParserTestSupport.mm; sourceTree = ""; }; + D1A0B0102F50001100A0B001 /* URLExtractionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtractionTests.swift; sourceTree = ""; }; D71B9A8B2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizeTracksByViewController.swift; sourceTree = ""; }; D71B9A8D2FC96D2D00FBB0F3 /* OrganizeByTypeExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizeByTypeExtension.swift; sourceTree = ""; }; D71B9A8F2FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizeByTypeCell.swift; sourceTree = ""; }; D7B76D0C2FD16070004EE3E9 /* OrganizeByStepSizeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrganizeByStepSizeViewController.swift; sourceTree = ""; }; D7BF04772FD2DB4400BABB31 /* TracksViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracksViewController.swift; sourceTree = ""; }; - D1A0B0102F50001100A0B001 /* URLExtractionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtractionTests.swift; sourceTree = ""; }; DA0132D32A1E0AB500920C14 /* WidgetsListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetsListViewController.swift; sourceTree = ""; }; DA0132D52A1E4A6100920C14 /* ic_custom20_screen_side_right@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom20_screen_side_right@2x.png"; path = "Resources/Icons/ic_custom20_screen_side_right@2x.png"; sourceTree = ""; }; DA0132D62A1E4A6100920C14 /* ic_custom20_screen_side_top@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "ic_custom20_screen_side_top@2x.png"; path = "Resources/Icons/ic_custom20_screen_side_top@2x.png"; sourceTree = ""; }; @@ -12695,8 +12692,6 @@ DA5A7E0C26C563A300F274C7 /* OAFavoriteListViewController.mm */, C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */, C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */, - C5EB56EA2FD188A900D01657 /* OAFavoriteFoldersBridge.h */, - C5EB56EB2FD188A900D01657 /* OAFavoriteFoldersBridge.mm */, 272AFB972FD2DC42006C2E21 /* OAFavoritesSwiftHelper.h */, 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */, 327D68A42A8F9BA80076D1E0 /* SavedArticlesTabViewController.swift */, @@ -17787,7 +17782,6 @@ 46C841412C32F3CA00E284B0 /* OANavStartStopAction.m in Sources */, DA5A81A926C563A700F274C7 /* OAAbbreviations.mm in Sources */, 32AB48692C9C50CB005EF1D4 /* DownloadingListHelper.swift in Sources */, - C5EB56EC2FD188A900D01657 /* OAFavoriteFoldersBridge.mm in Sources */, DAD3FD242A4ACB2E00BFA03A /* WidgetGroupListViewController.swift in Sources */, FAA650532ADD42C50020DCEA /* Sensor.swift in Sources */, FAC166282B21BEF900D63755 /* Central.swift in Sources */, diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h deleted file mode 100644 index dbcde7edd3..0000000000 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.h +++ /dev/null @@ -1,36 +0,0 @@ -// -// OAFavoriteFoldersBridge.h -// OsmAnd Maps -// -// Created by Dmitry Svetlichny on 04.06.2026. -// Copyright © 2026 OsmAnd. All rights reserved. -// - -#import - -NS_ASSUME_NONNULL_BEGIN - -@class UIColor, UIImage, UINavigationController, UIView, UIViewController, OAFavoritePointBridgeItem, OAFavoriteFolderBridgeItem; - -@interface OAFavoriteFoldersBridge : NSObject - -+ (NSArray *)favoriteFolders; -+ (NSArray *)favoritePointsForGroupName:(NSString *)groupName; -+ (void)openFavoritePointWithIdentifier:(NSString *)identifier; -+ (void)openNewFavoriteGroupEditorWithParentGroupName:(nullable NSString *)parentGroupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion; -+ (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible; -+ (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned; -+ (void)renameFavoriteGroup:(NSString *)groupName newName:(NSString *)newName; -+ (void)openFavoriteGroupAppearance:(NSString *)groupName navigationController:(UINavigationController *)navigationController; -+ (void)shareFavoriteGroup:(NSString *)groupName sourceView:(nullable UIView *)sourceView viewController:(UIViewController *)viewController; -+ (void)openFavoriteGroupMove:(NSString *)groupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion; -+ (void)deleteFavoriteGroup:(NSString *)groupName; -+ (void)addFavoriteGroupToMapMarkers:(NSString *)groupName; -+ (void)openFavoriteGroupAddToTrack:(NSString *)groupName navigationController:(UINavigationController *)navigationController; -+ (void)addFavoriteGroupToNavigation:(NSString *)groupName; -+ (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; -+ (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(NSString *)gpxFileName; - -@end - -NS_ASSUME_NONNULL_END diff --git a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm b/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm deleted file mode 100644 index eae51e483c..0000000000 --- a/Sources/Controllers/MyPlaces/OAFavoriteFoldersBridge.mm +++ /dev/null @@ -1,515 +0,0 @@ -// -// OAFavoriteFoldersBridge.mm -// OsmAnd Maps -// -// Created by Dmitry Svetlichny on 04.06.2026. -// Copyright © 2026 OsmAnd. All rights reserved. -// - -#import "OAFavoriteFoldersBridge.h" -#import "OAAppSettings.h" -#import "OAEditGroupViewController.h" -#import "OAFavoriteItem.h" -#import "OAFavoriteGroupEditorViewController.h" -#import "OAFavoritesHelper.h" -#import "OAGPXDatabase.h" -#import "OAIndexConstants.h" -#import "OALocationServices.h" -#import "OAMapPanelViewController.h" -#import "OAOpenAddTrackViewController.h" -#import "OAPointDescription.h" -#import "OARootViewController.h" -#import "OASavingTrackHelper.h" -#import "OASelectedGPXHelper.h" -#import "OATargetPointsHelper.h" -#import "OAUtilities.h" -#import "OsmAndApp.h" -#import "OsmAndSharedWrapper.h" -#import -#import -#import "OAFavoritesSwiftHelper.h" - -#include - - -@interface OAFavoriteFoldersBridge () - -+ (NSArray *)sortedFavoritePoints:(NSArray *)points; -+ (NSArray *)sortedFavoritePointsForGroup:(OAFavoriteGroup *)group; -+ (NSArray *)favoriteGroupsInsideOrEqualToGroupName:(NSString *)groupName; -+ (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier; -+ (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName; -+ (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; -+ (BOOL)renameFavoriteGroupTreeFromGroupName:(NSString *)sourceGroupName toGroupName:(NSString *)targetGroupName; -+ (BOOL)isGroupName:(NSString *)groupName insideOrEqualToGroupName:(NSString *)parentGroupName; -+ (NSString *)groupNameByMovingGroupName:(NSString *)groupName toParentGroupName:(NSString *)parentGroupName; -+ (NSString *)suffixForGroupName:(NSString *)groupName parentGroupName:(NSString *)parentGroupName; -+ (NSString *)lastComponentForGroupName:(NSString *)groupName; -+ (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(NSString *)gpxFileName; -+ (CLLocation *)locationForFavorite:(OAFavoriteItem *)favorite; - -@end - -@interface OAFavoriteGroupMoveDelegate : NSObject - -@property (nonatomic, copy) NSString *groupName; -@property (nonatomic, weak) OAEditGroupViewController *controller; -@property (nonatomic, copy, nullable) void (^completion)(void); - -@end - -@implementation OAFavoriteGroupMoveDelegate - -- (void)groupChanged -{ - if (!self.controller.saveChanges) - return; - - if ([OAFavoriteFoldersBridge moveFavoriteGroup:self.groupName toGroupName:self.controller.groupName] && self.completion) - self.completion(); -} - -@end - -@interface OAFavoriteGroupAddToTrackDelegate : NSObject - -@property (nonatomic, copy) NSString *groupName; - -@end - -@implementation OAFavoriteGroupAddToTrackDelegate - -- (void)onFileSelected:(NSString *)gpxFileName -{ - [OAFavoriteFoldersBridge addFavoriteGroupToTrack:self.groupName gpxFileName:gpxFileName]; -} - -@end - -@interface OAFavoriteGroupCreationHandler : NSObject - -@property (nonatomic, copy) NSString *parentGroupName; -@property (nonatomic, copy, nullable) void (^completion)(void); - -@end - -@implementation OAFavoriteGroupCreationHandler - -- (void)addNewItemWithName:(NSString *)name iconName:(NSString *)iconName color:(UIColor *)color backgroundIconName:(NSString *)backgroundIconName -{ - NSString *trimmedName = [(name ?: @"") stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet]; - NSString *groupName = self.parentGroupName.length > 0 && trimmedName.length > 0 ? [NSString stringWithFormat:@"%@/%@", self.parentGroupName, trimmedName] : trimmedName; - if (groupName.length == 0 || [OAFavoritesHelper getGroupByName:groupName]) - return; - - [OAFavoritesHelper addFavoriteGroup:groupName color:color iconName:iconName backgroundIconName:backgroundIconName]; - [OAFavoritesHelper saveCurrentPointsIntoFile]; - if (self.completion) - self.completion(); -} - -@end - -@implementation OAFavoriteFoldersBridge - -+ (NSArray *)favoriteFolders -{ - return [OAFavoritesSwiftHelper favoriteFolders]; -} - -+ (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; -} - -+ (void)openNewFavoriteGroupEditorWithParentGroupName:(nullable NSString *)parentGroupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion -{ - if (!navigationController) - return; - - OAFavoriteGroupEditorViewController *viewController = [[OAFavoriteGroupEditorViewController alloc] initWithNew]; - OAFavoriteGroupCreationHandler *handler = [[OAFavoriteGroupCreationHandler alloc] init]; - handler.parentGroupName = parentGroupName ?: @""; - handler.completion = completion; - viewController.delegate = (id) handler; - objc_setAssociatedObject(viewController, @selector(openNewFavoriteGroupEditorWithParentGroupName:navigationController:completion:), handler, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - UINavigationController *modalNavigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; - [navigationController presentViewController:modalNavigationController animated:YES completion:nil]; -} - -+ (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]; -} - -+ (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) - return; - - [OAFavoritesHelper updateGroup:group visible:visible saveImmediately:YES]; -} - -+ (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) - return; - - [OAFavoritesHelper updateGroup:group pinned:pinned saveImmediately: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]; -} - -+ (void)openFavoriteGroupAppearance:(NSString *)groupName navigationController:(UINavigationController *)navigationController -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group || !navigationController) - return; - - OAFavoriteGroupEditorViewController *viewController = [[OAFavoriteGroupEditorViewController alloc] initWithGroup:[group toPointsGroup]]; - [navigationController pushViewController:viewController animated:YES]; -} - -+ (void)shareFavoriteGroup:(NSString *)groupName sourceView:(nullable UIView *)sourceView viewController:(UIViewController *)viewController -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group || !viewController) - return; - - OAFavoriteGroup *groupToShare = [[OAFavoriteGroup alloc] initWithPoints:group.points.copy name:group.name isVisible:group.isVisible color:group.color]; - groupToShare.isPinned = group.isPinned; - groupToShare.iconName = group.iconName; - groupToShare.backgroundType = group.backgroundType; - OsmAndAppInstance app = [OsmAndApp instance]; - NSString *groupFileName = [app getGroupFileName:group.name]; - NSString *filename = [NSString stringWithFormat:@"%@%@%@%@", app.favoritesFilePrefix, groupFileName.length > 0 ? app.favoritesGroupNameSeparator : @"", groupFileName ?: @"", GPX_FILE_EXT]; - NSString *fullFilename = [NSTemporaryDirectory() stringByAppendingPathComponent:filename]; - [OAFavoritesHelper saveFile:@[groupToShare] file:fullFilename]; - NSURL *favoritesUrl = [NSURL fileURLWithPath:fullFilename]; - UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[favoritesUrl] applicationActivities:nil]; - activityViewController.completionWithItemsHandler = ^(UIActivityType _Nullable activityType, BOOL completed, NSArray * _Nullable returnedItems, NSError * _Nullable activityError) { - [NSFileManager.defaultManager removeItemAtURL:favoritesUrl error:nil]; - }; - - UIPopoverPresentationController *popover = activityViewController.popoverPresentationController; - if (popover) - { - UIView *popoverSourceView = sourceView ?: viewController.view; - popover.sourceView = popoverSourceView; - popover.sourceRect = popoverSourceView.bounds; - popover.permittedArrowDirections = UIPopoverArrowDirectionAny; - } - - [viewController presentViewController:activityViewController animated:YES completion:nil]; -} - -+ (void)openFavoriteGroupMove:(NSString *)groupName navigationController:(UINavigationController *)navigationController completion:(void (^ _Nullable)(void))completion -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group || !navigationController) - return; - - NSMutableArray *groupNames = [NSMutableArray array]; - for (OAFavoriteGroup *favoriteGroup in [OAFavoritesHelper getFavoriteGroups]) - { - NSString *favoriteGroupName = favoriteGroup.name ?: @""; - if (![self isGroupName:favoriteGroupName insideOrEqualToGroupName:group.name ?: @""]) - [groupNames addObject:favoriteGroupName]; - } - - if (![groupNames containsObject:@""]) - [groupNames addObject:@""]; - - OAEditGroupViewController *groupController = [[OAEditGroupViewController alloc] initWithGroupName:nil groups:groupNames]; - OAFavoriteGroupMoveDelegate *delegate = [[OAFavoriteGroupMoveDelegate alloc] init]; - delegate.groupName = group.name ?: @""; - delegate.controller = groupController; - delegate.completion = completion; - groupController.delegate = delegate; - objc_setAssociatedObject(groupController, @selector(groupChanged), delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - UINavigationController *modalNavigationController = [[UINavigationController alloc] initWithRootViewController:groupController]; - [navigationController presentViewController:modalNavigationController animated:YES completion:nil]; -} - -+ (void)deleteFavoriteGroup:(NSString *)groupName -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) - return; - - NSArray *groupsToDelete = [self favoriteGroupsInsideOrEqualToGroupName:group.name ?: @""]; - if (groupsToDelete.count > 0) - [OAFavoritesHelper deleteFavoriteGroups:groupsToDelete andFavoritesItems:nil]; -} - -+ (void)addFavoriteGroupToMapMarkers:(NSString *)groupName -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) - return; - - OAMapPanelViewController *mapPanel = [OARootViewController instance].mapPanel; - for (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) - { - CLLocation *location = [self locationForFavorite:favorite]; - if (!location) - continue; - - [mapPanel addMapMarker:location.coordinate.latitude lon:location.coordinate.longitude description:[favorite getDisplayName]]; - } -} - -+ (void)openFavoriteGroupAddToTrack:(NSString *)groupName navigationController:(UINavigationController *)navigationController -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group || group.points.count == 0 || !navigationController) - return; - - OAOpenAddTrackViewController *viewController = [[OAOpenAddTrackViewController alloc] initWithScreenType:EOAAddToATrack]; - OAFavoriteGroupAddToTrackDelegate *delegate = [[OAFavoriteGroupAddToTrackDelegate alloc] init]; - delegate.groupName = group.name ?: @""; - viewController.delegate = delegate; - objc_setAssociatedObject(viewController, @selector(onFileSelected:), delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC); - UINavigationController *modalNavigationController = [[UINavigationController alloc] initWithRootViewController:viewController]; - [navigationController presentViewController:modalNavigationController animated:YES completion:nil]; -} - -+ (void)addFavoriteGroupToNavigation:(NSString *)groupName -{ - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) - return; - - NSArray *points = [self sortedFavoritePointsForGroup:group]; - if (points.count == 0) - return; - - OATargetPointsHelper *targetPointsHelper = OATargetPointsHelper.sharedInstance; - [targetPointsHelper clearAllPoints:NO]; - for (NSUInteger index = 0; index < points.count; index++) - { - OAFavoriteItem *favorite = points[index]; - CLLocation *location = [self locationForFavorite:favorite]; - if (!location) - continue; - - OAPointDescription *description = [[OAPointDescription alloc] initWithType:POINT_TYPE_FAVORITE name:[favorite getDisplayName]]; - BOOL isDestination = index == points.count - 1; - [targetPointsHelper navigateToPoint:location updateRoute:isDestination intermediate:isDestination ? -1 : (int)index historyName:description]; - } - - OARootViewController *rootViewController = [OARootViewController instance]; - [rootViewController.navigationController popToRootViewControllerAnimated:YES]; - [rootViewController.mapPanel showRouteInfo]; -} - -+ (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]]; - }]; -} - -+ (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; -} - -+ (NSArray *)sortedFavoritePointsForGroup:(OAFavoriteGroup *)group -{ - return [self sortedFavoritePoints:group.points ?: @[]]; -} - -+ (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; -} - -+ (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName -{ - return [OAFavoritesHelper getGroupByName:groupName ?: @""]; -} - -+ (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]; -} - -+ (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]; - - 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 ?: @""; -} - -+ (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(NSString *)gpxFileName -{ - NSArray *points = [self sortedFavoritePointsForGroup:[self favoriteGroupWithName: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:[OAUtilities getGpxShortPath:dataItem.file.absolutePath]]; -} - -+ (CLLocation *)locationForFavorite:(OAFavoriteItem *)favorite -{ - if (!favorite.favorite) - return nil; - - return [[CLLocation alloc] initWithLatitude:[favorite getLatitude] longitude:[favorite getLongitude]]; -} - -@end From 7a567650c1d53c2e6585e7185737c37b74f21753 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Fri, 12 Jun 2026 12:03:25 +0300 Subject: [PATCH 24/41] updating list if context menu open fixed --- .../MyPlaces/FavoriteListViewController.swift | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 62ef150ab9..9c0a93b464 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -189,8 +189,9 @@ final class FavoriteListViewController: UIViewController { private var searchText = "" private var isSearchActive = false private var isSelectionModeInSearch = false - private var isDecelerating = false private var lastDistanceDirectionUpdate: TimeInterval = 0.0 + private var isContextMenuVisible = false + private var shouldReloadCollectionView = false private var locationUpdateObserver: OAAutoObserverProxy? private var headingUpdateObserver: OAAutoObserverProxy? private var isSearchResultsMode: Bool { @@ -473,8 +474,12 @@ final class FavoriteListViewController: UIViewController { return } + if isContextMenuVisible { + shouldReloadCollectionView = true + return + } + guard !collectionView.isEditing - && !isDecelerating && OsmAndApp.swiftInstance().locationServices.lastKnownLocation != nil && dataSource.snapshot().itemIdentifiers.contains(where: { item in if case .favorite = item { @@ -523,7 +528,6 @@ final class FavoriteListViewController: UIViewController { override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - isDecelerating = false definesPresentationContext = true configureNavigation() navigationController?.setToolbarHidden(true, animated: false) @@ -535,7 +539,6 @@ final class FavoriteListViewController: UIViewController { override func viewWillDisappear(_ animated: Bool) { unregisterDistanceAndDirectionObservers() - isDecelerating = false if !isRootFolder { navigationItem.searchController = nil navigationController?.setNavigationBarHidden(true, animated: false) @@ -1755,6 +1758,22 @@ extension FavoriteListViewController: UICollectionViewDelegate { 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 @@ -1771,21 +1790,6 @@ extension FavoriteListViewController: UICollectionViewDelegate { return UIContextMenuConfiguration(identifier: nil, previewProvider: nil, actionProvider: menuProvider) } - - func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - isDecelerating = true - } - - func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - guard !decelerate else { return } - isDecelerating = false - updateDistanceAndDirection(true) - } - - func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - isDecelerating = false - updateDistanceAndDirection(true) - } } extension FavoriteListViewController: OAShareMenuDelegate { From 4e0dea1b78f2e8a098458cd77381f910274efe1a Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Fri, 12 Jun 2026 15:00:50 +0300 Subject: [PATCH 25/41] updating toolbar, import fixed, old my places removed --- OsmAnd.xcodeproj/project.pbxproj | 10 - .../MyPlaces/FavoriteListViewController.swift | 1 + .../Controllers/MyPlaces/MyPlaces.storyboard | 113 - .../MyPlaces/OAFavoriteListViewController.h | 30 - .../MyPlaces/OAFavoriteListViewController.mm | 2109 ----------------- .../OAImportCompleteViewController.mm | 13 +- .../OATripRecordingSettingsViewController.mm | 9 +- .../TargetMenu/OACollapsableWaypointsView.mm | 5 +- Sources/OsmAnd Maps-Bridging-Header.h | 1 - 9 files changed, 13 insertions(+), 2278 deletions(-) delete mode 100644 Sources/Controllers/MyPlaces/MyPlaces.storyboard delete mode 100644 Sources/Controllers/MyPlaces/OAFavoriteListViewController.h delete mode 100644 Sources/Controllers/MyPlaces/OAFavoriteListViewController.mm diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index 1fc3e5965b..88df622070 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -2511,7 +2511,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 */; }; @@ -2540,7 +2539,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 */; }; @@ -6919,7 +6917,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 = ""; }; @@ -6963,13 +6960,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 = ""; }; @@ -12687,9 +12682,6 @@ D7BF04772FD2DB4400BABB31 /* TracksViewController.swift */, D71B9A8B2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift */, D71B9A8F2FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift */, - DA5A7DC426C563A300F274C7 /* MyPlaces.storyboard */, - DA5A7E0126C563A300F274C7 /* OAFavoriteListViewController.h */, - DA5A7E0C26C563A300F274C7 /* OAFavoriteListViewController.mm */, C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */, C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */, 272AFB972FD2DC42006C2E21 /* OAFavoritesSwiftHelper.h */, @@ -15682,7 +15674,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 */, @@ -17352,7 +17343,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 */, diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 9c0a93b464..57c9c9b353 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -1634,6 +1634,7 @@ final class FavoriteListViewController: UIViewController { @objc private func cancelButtonPressed() { setEdit(false) + configureToolbar() } @objc private func selectAllButtonPressed() { 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/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/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/OsmAnd Maps-Bridging-Header.h b/Sources/OsmAnd Maps-Bridging-Header.h index 089f5b9ec1..945f1613b4 100644 --- a/Sources/OsmAnd Maps-Bridging-Header.h +++ b/Sources/OsmAnd Maps-Bridging-Header.h @@ -219,7 +219,6 @@ #import "OACloudIntroductionViewController.h" #import "OAHelpViewController.h" #import "InitialRoutePlanningBottomSheetViewController.h" -#import "OAFavoriteListViewController.h" #import "OATripRecordingSettingsViewController.h" #import "OAPluginsViewController.h" #import "OAPluginDetailsViewController.h" From 63d7ae0dc29e79c1c66784dd6e6a13cc98c251eb Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Fri, 12 Jun 2026 17:42:43 +0300 Subject: [PATCH 26/41] setFavoriteGroupVisible fix --- .../MyPlaces/FavoriteListViewController.swift | 20 +++++++++----- .../MyPlaces/FavoriteSortModeHelper.swift | 2 +- .../MyPlaces/OAFavoritesSwiftHelper.mm | 26 ++++++++++++------- 3 files changed, 32 insertions(+), 16 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 57c9c9b353..82f152df45 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -1193,6 +1193,10 @@ final class FavoriteListViewController: UIViewController { } private 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 hasDirectFavoritePoints = folder.bridgeItem.pointsCount > 0 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 } OAFavoritesSwiftHelper.setFavoriteGroupVisible(folder.bridgeItem.groupName, visible: !folder.isVisible) @@ -1227,21 +1231,26 @@ final class FavoriteListViewController: UIViewController { guard let self else { return } self.openFavoriteItemsMove([folder.bridgeItem]) } - let thirdButtons: [UIMenuElement] = (folder.bridgeItem.pointsCount > 0 ? [shareAction] : []) + (folder.bridgeItem.groupName.isEmpty ? [] : [moveAction]) + 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 - OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: [folder.bridgeItem]) + OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: hasDirectFavoritePoints ? folderFavoriteItem : subtreeFavoriteItems) } let trackAction = UIAction(title: localizedString("add_to_a_track"), image: .icCustomTrip) { [weak self] _ in guard let self else { return } - self.openFavoriteGroupAddToTrack(folder.bridgeItem.groupName) + if hasDirectFavoritePoints { + self.openFavoriteGroupAddToTrack(folder.bridgeItem.groupName) + } else { + self.openFavoriteItemsAddToTrack(subtreeFavoriteItems) + } } let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in guard let self else { return } - OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: self.favoritePointRows(forGroupName: folder.bridgeItem.groupName).map { $0.bridgeItem }) + let directFavoriteItems: [Any] = self.favoritePointRows(forGroupName: folder.bridgeItem.groupName).map { $0.bridgeItem } + OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: hasDirectFavoritePoints ? directFavoriteItems : subtreeFavoriteItems) } - let addToActions: [UIMenuElement] = folder.bridgeItem.pointsCount > 0 ? [mapMarkersAction, trackAction, navigationAction] : [] + let addToActions: [UIMenuElement] = hasFavoritePoints ? [mapMarkersAction, trackAction, navigationAction] : [] let fourthButtons: [UIMenuElement] = addToActions.isEmpty ? [] : [UIMenu(title: localizedString("shared_string_add"), image: .icCustomAdd, children: addToActions)] let fourthButtonsSection = UIMenu(title: "", options: .displayInline, children: fourthButtons) @@ -1599,7 +1608,6 @@ final class FavoriteListViewController: UIViewController { } guard let navigationController else { return } - let colorController = OAEditColorViewController() colorController.delegate = self self.colorController = colorController diff --git a/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift b/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift index 77c6c9b85d..99f0543380 100644 --- a/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift +++ b/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift @@ -56,7 +56,7 @@ protocol FavoriteSortablePoint { } var isDateOriented: Bool { - self == .newestDateFirst || self == .oldestDateFirst + self == .lastModified || self == .newestDateFirst || self == .oldestDateFirst } static func byTitle(_ title: String) -> FavoriteSortMode { diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm index b194330e80..00921b69ea 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm @@ -223,11 +223,16 @@ + (NSString *)formattedCoordinatesForFavoritePoint:(OAFavoritePointBridgeItem *) + (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible { - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) + NSArray *groups = [self favoriteGroupsInsideOrEqualToGroupName:groupName]; + if (groups.count == 0) return; - [OAFavoritesHelper updateGroup:group visible:visible saveImmediately:YES]; + for (OAFavoriteGroup *group in groups) + { + [OAFavoritesHelper updateGroup:group visible:visible saveImmediately:NO]; + } + + [OAFavoritesHelper saveCurrentPointsIntoFile]; } + (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned @@ -248,13 +253,16 @@ + (void)setFavoriteGroupsVisible:(NSArray *)groupNames visible:(BOOL NSMutableSet *handledGroupNames = [NSMutableSet set]; for (NSString *groupName in groupNames) { - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) - continue; + for (OAFavoriteGroup *group in [self favoriteGroupsInsideOrEqualToGroupName:groupName]) + { + NSString *currentGroupName = group.name ?: @""; + if ([handledGroupNames containsObject:currentGroupName]) + continue; - [handledGroupNames addObject:groupName]; - [OAFavoritesHelper updateGroup:group visible:visible saveImmediately:NO]; - changed = YES; + [handledGroupNames addObject:currentGroupName]; + [OAFavoritesHelper updateGroup:group visible:visible saveImmediately:NO]; + changed = YES; + } } if (changed) From b474350c2e42bdf5995b9431ba43b010baf62d06 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Fri, 12 Jun 2026 18:17:42 +0300 Subject: [PATCH 27/41] refactored --- .../MyPlaces/FavoriteListViewController.swift | 58 ++++++------------- .../MyPlaces/OAFavoritesSwiftHelper.mm | 41 +------------ 2 files changed, 21 insertions(+), 78 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift index 82f152df45..4b69abffe7 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController.swift @@ -21,12 +21,9 @@ private enum FavoriteFolderSection: Hashable { var title: String { switch self { - case .pinned: - localizedString("shared_string_pinned") - case .visible: - localizedString("shared_string_visible") - case .hidden: - localizedString("shared_string_hidden") + case .pinned: localizedString("shared_string_pinned") + case .visible: localizedString("shared_string_visible") + case .hidden: localizedString("shared_string_hidden") } } } @@ -64,21 +61,13 @@ private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { let bridgeItem: OAFavoriteFolderBridgeItem - var title: String { - bridgeItem.title - } + var title: String { bridgeItem.title } - var isVisible: Bool { - bridgeItem.isVisible - } + var isVisible: Bool { bridgeItem.isVisible } - var isPinned: Bool { - bridgeItem.isPinned - } + var isPinned: Bool { bridgeItem.isPinned } - var lastModified: Date? { - bridgeItem.lastModifiedDate - } + var lastModified: Date? { bridgeItem.lastModifiedDate } var subtitle: String { let pointsText = "\(bridgeItem.subtreePointsCount) \(localizedString("shared_string_gpx_points").lowercased())" @@ -111,17 +100,11 @@ private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { private struct FavoritePointRow: Hashable, FavoriteSortablePoint { let bridgeItem: OAFavoritePointBridgeItem - var title: String { - bridgeItem.title - } + var title: String { bridgeItem.title } - var distance: CLLocationDistance? { - bridgeItem.distance?.doubleValue - } + var distance: CLLocationDistance? { bridgeItem.distance?.doubleValue } - var lastModified: Date? { - bridgeItem.timestampDate - } + var lastModified: Date? { bridgeItem.timestampDate } var titleColor: UIColor { bridgeItem.isVisible ? .textColorPrimary : .textColorSecondary @@ -206,18 +189,14 @@ final class FavoriteListViewController: UIViewController { } private var normalTitle: String { switch screenMode { - case .root: - localizedString("shared_string_favorites") - case .folder(let folder, _): - folder.title + case .root: localizedString("shared_string_favorites") + case .folder(let folder, _): folder.title } } private var normalSubtitle: String { switch screenMode { - case .root: - localizedString("shared_string_my_places") - case .folder(_, let previousTitle): - previousTitle + case .root: localizedString("shared_string_my_places") + case .folder(_, let previousTitle): previousTitle } } private var parentGroupName: String? { @@ -1030,8 +1009,7 @@ final class FavoriteListViewController: UIViewController { } private func favoriteFolders() -> [FavoriteFolderRow] { - OAFavoritesSwiftHelper.favoriteFolders() - .map { FavoriteFolderRow(item: $0) } + OAFavoritesSwiftHelper.favoriteFolders().map { FavoriteFolderRow(item: $0) } } private func isDirectFolder(_ groupName: String, parentGroupName: String?) -> Bool { @@ -1081,7 +1059,7 @@ final class FavoriteListViewController: UIViewController { switch itemIdentifier { case .folder, .favorite: indexPaths.append(indexPath) - case .sortHeader, .backupBanner, .header, .statsFooter, .emptyState: + default: continue } } @@ -1507,7 +1485,7 @@ final class FavoriteListViewController: UIViewController { return folder.bridgeItem case .favorite(let favorite): return favorite.bridgeItem - case .backupBanner, .header, .statsFooter, .sortHeader, .emptyState: + default: return nil } } @@ -1755,7 +1733,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { return } OAFavoritesSwiftHelper.openFavoritePoint(withIdentifier: favorite.bridgeItem.identifier) - case .sortHeader, .backupBanner, .header, .statsFooter, .emptyState: + default: break } diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm index 00921b69ea..f9feec6235 100644 --- a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm +++ b/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm @@ -38,13 +38,6 @@ #include -@interface OAFavoriteFolderBridgeItem () - -- (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize subtreePointsCount:(NSUInteger)subtreePointsCount; -+ (NSString *)titleForGroupName:(NSString *)groupName; - -@end - @implementation OAFavoriteFolderBridgeItem - (instancetype)initWithGroup:(OAFavoriteGroup *)group index:(NSUInteger)index lastModifiedDate:(nullable NSDate *)lastModifiedDate fileSize:(long long)fileSize subtreePointsCount:(NSUInteger)subtreePointsCount @@ -76,13 +69,6 @@ + (NSString *)titleForGroupName:(NSString *)groupName @end -@interface OAFavoritePointBridgeItem () - -+ (nullable NSNumber *)distanceForFavorite:(OAFavoriteItem *)favorite; -+ (CGFloat)directionForFavorite:(OAFavoriteItem *)favorite; - -@end - @implementation OAFavoritePointBridgeItem - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite @@ -140,27 +126,6 @@ + (CGFloat)directionForFavorite:(OAFavoriteItem *)favorite @end -@interface OAFavoritesSwiftHelper () - -+ (NSArray *)sortedFavoritePoints:(NSArray *)points; -+ (NSArray *)sortedFavoritePointsForGroup:(OAFavoriteGroup *)group; -+ (NSArray *)favoriteGroupsInsideOrEqualToGroupName:(NSString *)groupName; -+ (NSUInteger)subtreePointsCountForGroupName:(NSString *)groupName groups:(NSArray *)groups; -+ (OAFavoriteItem *)favoritePointWithIdentifier:(NSString *)identifier; -+ (OAFavoriteGroup *)favoriteGroupWithName:(NSString *)groupName; -+ (BOOL)renameFavoriteGroupTreeFromGroupName:(NSString *)sourceGroupName toGroupName:(NSString *)targetGroupName; -+ (BOOL)moveFavoriteGroup:(NSString *)groupName toGroupName:(NSString *)targetGroupName; -+ (BOOL)isGroupName:(NSString *)groupName insideOrEqualToGroupName:(NSString *)parentGroupName; -+ (NSString *)groupNameByMovingGroupName:(NSString *)groupName toParentGroupName:(NSString *)parentGroupName; -+ (NSString *)suffixForGroupName:(NSString *)groupName parentGroupName:(NSString *)parentGroupName; -+ (NSString *)lastComponentForGroupName:(NSString *)groupName; -+ (OAFavoriteGroup *)favoriteGroupForSharingGroup:(OAFavoriteGroup *)group points:(NSArray *)points; -+ (nullable NSURL *)fileURLForSharingFavoriteGroups:(NSArray *)favoriteGroups; -+ (CLLocation *)locationForFavorite:(OAFavoriteItem *)favorite; -+ (int)currentMapZoomLevel; - -@end - @implementation OAFavoritesSwiftHelper + (NSArray *)favoriteFolders @@ -510,7 +475,7 @@ + (BOOL)canUseGroupWithName:(NSString *)groupName return group && group.points.count > 0; } -+ (nullable NSURL *)shareFavoriteItems:(NSArray *)favoriteItems ++ (NSURL *)shareFavoriteItems:(NSArray *)favoriteItems { if (favoriteItems.count == 0) return nil; @@ -1014,7 +979,7 @@ + (void)addFavoriteItemsToNavigation:(NSArray *)favoriteItems return [self sortedFavoritePoints:group.points ?: @[]]; } -+ (nullable NSDate *)lastModifiedDateForGroupName:(NSString *)groupName groups:(NSArray *)groups fileAttributesByGroupName:(NSDictionary *> *)fileAttributesByGroupName ++ (NSDate *)lastModifiedDateForGroupName:(NSString *)groupName groups:(NSArray *)groups fileAttributesByGroupName:(NSDictionary *> *)fileAttributesByGroupName { NSDate *lastModifiedDate = nil; NSString *parentGroupName = groupName ?: @""; @@ -1173,7 +1138,7 @@ + (OAFavoriteGroup *)favoriteGroupForSharingGroup:(OAFavoriteGroup *)group point return groupToShare; } -+ (nullable NSURL *)fileURLForSharingFavoriteGroups:(NSArray *)favoriteGroups ++ (NSURL *)fileURLForSharingFavoriteGroups:(NSArray *)favoriteGroups { if (favoriteGroups.count == 0) return nil; From b2b338f35e22533999a9e34842621a74db2a2863 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Fri, 12 Jun 2026 18:19:02 +0300 Subject: [PATCH 28/41] refactored --- OsmAnd.xcodeproj/project.pbxproj | 16 ++++++++++++---- .../FavoriteListViewController.swift | 0 .../FavoriteSortModeHelper.swift | 0 .../OAFavoritesSwiftHelper.h | 0 .../OAFavoritesSwiftHelper.mm | 0 5 files changed, 12 insertions(+), 4 deletions(-) rename Sources/Controllers/MyPlaces/{ => FavoriteListViewController}/FavoriteListViewController.swift (100%) rename Sources/Controllers/MyPlaces/{ => FavoriteListViewController}/FavoriteSortModeHelper.swift (100%) rename Sources/Controllers/MyPlaces/{ => FavoriteListViewController}/OAFavoritesSwiftHelper.h (100%) rename Sources/Controllers/MyPlaces/{ => FavoriteListViewController}/OAFavoritesSwiftHelper.mm (100%) diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index 88df622070..d3d5d6741f 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -8379,6 +8379,17 @@ path = ExternalInputDevice; sourceTree = ""; }; + 275ED6502FDC5AA60088D42B /* FavoriteListViewController */ = { + isa = PBXGroup; + children = ( + C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */, + C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */, + 272AFB972FD2DC42006C2E21 /* OAFavoritesSwiftHelper.h */, + 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */, + ); + path = FavoriteListViewController; + sourceTree = ""; + }; 2783B4342E66E68F00682723 /* Devices */ = { isa = PBXGroup; children = ( @@ -12682,10 +12693,7 @@ D7BF04772FD2DB4400BABB31 /* TracksViewController.swift */, D71B9A8B2FC95D8500FBB0F3 /* OrganizeTracksByViewController.swift */, D71B9A8F2FC99D5D00FBB0F3 /* OrganizeByTypeCell.swift */, - C5EB56ED2FD18B8A00D01657 /* FavoriteListViewController.swift */, - C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */, - 272AFB972FD2DC42006C2E21 /* OAFavoritesSwiftHelper.h */, - 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */, + 275ED6502FDC5AA60088D42B /* FavoriteListViewController */, 327D68A42A8F9BA80076D1E0 /* SavedArticlesTabViewController.swift */, C50E32812CA3FBDF00EEC41F /* TracksFiltersViewController.swift */, C50E32832CA57DDB00EEC41F /* TracksFilterDetailsViewController.swift */, diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift similarity index 100% rename from Sources/Controllers/MyPlaces/FavoriteListViewController.swift rename to Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift diff --git a/Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteSortModeHelper.swift similarity index 100% rename from Sources/Controllers/MyPlaces/FavoriteSortModeHelper.swift rename to Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteSortModeHelper.swift diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.h similarity index 100% rename from Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.h rename to Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.h diff --git a/Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.mm similarity index 100% rename from Sources/Controllers/MyPlaces/OAFavoritesSwiftHelper.mm rename to Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.mm From d3f0ec92c4f626bf83b1259e8e9750130f52594d Mon Sep 17 00:00:00 2001 From: Dmitry Svetlichny Date: Mon, 15 Jun 2026 12:04:41 +0300 Subject: [PATCH 29/41] Add FavoriteListCell --- .../FavoriteListViewController.swift | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift index 4b69abffe7..8f7ea2ebe3 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift @@ -138,10 +138,21 @@ private struct FavoriteFolderStats: Hashable { } } +private 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 = Self.rowHeight + return attributes + } +} + final class FavoriteListViewController: UIViewController { private typealias DataSource = UICollectionViewDiffableDataSource private typealias Snapshot = NSDiffableDataSourceSnapshot private typealias CellRegistration = UICollectionView.CellRegistration + private typealias RowCellRegistration = UICollectionView.CellRegistration weak var myPlacesDelegate: MyPlacesDelegate? @@ -153,7 +164,6 @@ final class FavoriteListViewController: UIViewController { private static let navigationTitleMaximumSize: CGFloat = 22.0 private static let navigationSubtitleFontSize: CGFloat = 12.0 private static let navigationSubtitleMaximumSize: CGFloat = 18.0 - private static let rowContentInsets = NSDirectionalEdgeInsets(top: 12.0, leading: 0.0, bottom: 12.0, trailing: 0.0) private static let statsFooterInsets = NSDirectionalEdgeInsets(top: 12.0, leading: 20.0, bottom: 12.0, trailing: 20.0) private static let wasClosedFreeBackupFavoritesBannerKey = "wasClosedFreeBackupFavoritesBanner" @@ -258,9 +268,8 @@ final class FavoriteListViewController: UIViewController { NSLayoutConstraint.activate([banner.topAnchor.constraint(equalTo: cell.contentView.topAnchor), banner.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), banner.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor)]) NSLayoutConstraint.activate([banner.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), banner.heightAnchor.constraint(equalToConstant: self.backupBannerHeight(banner, fittingWidth: fittingWidth))]) } - private lazy var folderCellRegistration = CellRegistration { [weak self] cell, _, folder in + private lazy var folderCellRegistration = RowCellRegistration { [weak self] cell, _, folder in var content = cell.defaultContentConfiguration() - content.directionalLayoutMargins = Self.rowContentInsets let iconName = folder.isPinned ? "ic_custom_folder_pin" : folder.iconName content.image = UIImage.templateImageNamed(iconName)?.resizedTemplateImage(with: FavoriteListViewController.imageSize) content.imageProperties.tintColor = folder.iconColor @@ -273,9 +282,8 @@ final class FavoriteListViewController: UIViewController { cell.backgroundConfiguration = self?.listCellBackgroundConfiguration() cell.accessories = self?.collectionView.isEditing == true ? [.multiselect()] : [.multiselect(), .disclosureIndicator()] } - private lazy var favoriteCellRegistration = CellRegistration { [weak self] cell, _, favorite in + private lazy var favoriteCellRegistration = RowCellRegistration { [weak self] cell, _, favorite in var content = cell.defaultContentConfiguration() - content.directionalLayoutMargins = Self.rowContentInsets content.image = OAUtilities.resize(favorite.bridgeItem.icon, newSize: CGSize(width: Self.favoriteIconSize, height: Self.favoriteIconSize)) content.text = favorite.title content.textProperties.numberOfLines = 2 From 96c6105f3ac301c347cbc2969001bd3339294b0d Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Mon, 15 Jun 2026 15:42:54 +0300 Subject: [PATCH 30/41] refactored --- OsmAnd.xcodeproj/project.pbxproj | 24 + .../FavoriteListViewController+Actions.swift | 478 +++++ .../FavoriteListViewController+Cells.swift | 214 ++ ...voriteListViewController+ContextMenu.swift | 214 ++ ...avoriteListViewController+DataSource.swift | 352 +++ ...FavoriteListViewController+Delegates.swift | 246 +++ .../FavoriteListViewController+Models.swift | 149 ++ .../FavoriteListViewController.swift | 1897 ++--------------- 8 files changed, 1842 insertions(+), 1732 deletions(-) create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index d3d5d6741f..ac44d7ad88 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -1587,6 +1587,12 @@ 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 */; }; @@ -5493,6 +5499,12 @@ 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 = ""; }; @@ -8383,6 +8395,12 @@ 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 /* OAFavoritesSwiftHelper.h */, 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */, @@ -17794,6 +17812,12 @@ 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 */, diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift new file mode 100644 index 0000000000..4cb6b7bda8 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift @@ -0,0 +1,478 @@ +// +// 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 areAllSelectableItemsSelected() -> Bool { + let selectableIndexPaths = selectableIndexPaths() + guard !selectableIndexPaths.isEmpty else { return false } + let selectedIndexPaths = Set(collectionView.indexPathsForSelectedItems ?? []) + return selectableIndexPaths.allSatisfy { selectedIndexPaths.contains($0) } + } + + func openFavoriteGroupAppearance(_ groupName: String) { + guard let viewController = OAFavoriteGroupEditorViewController(group: OAFavoritesSwiftHelper.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: OAFavoritesSwiftHelper.favoriteGroupNames(forMovingFavoriteItems: favoriteItems)) else { + return + } + self.groupController = groupController + favoriteItemsToMove = favoriteItems + groupController.delegate = self + let modalNavigationController = UINavigationController(rootViewController: groupController) + navigationController.present(modalNavigationController, animated: true) + } + + func openFavoriteGroupAddToTrack(_ groupName: String) { + guard OAFavoritesSwiftHelper.canUseGroup(withName: groupName), let navigationController, let viewController = OAOpenAddTrackViewController(screenType: .addToATrack) else { return } + addToTrackGroupName = groupName + addToTrackFavoriteItems = nil + viewController.delegate = self + let modalNavigationController = UINavigationController(rootViewController: viewController) + navigationController.present(modalNavigationController, 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 + let modalNavigationController = UINavigationController(rootViewController: viewController) + navigationController.present(modalNavigationController, animated: true) + } + + func favoritePointRows(forGroupName groupName: String) -> [FavoritePointRow] { + let sortMode = isSearchResultsMode ? searchFavoriteSortMode() : favoriteSortMode(entryId: groupName) + let favorites = OAFavoritesSwiftHelper.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 { OAFavoritesSwiftHelper.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 setEdit(_ isEdit: Bool) { + let shouldResetSearchSelection = !isEdit && isSelectionModeInSearch + if !isEdit { + collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) } + isSelectionModeInSearch = false + isSearchActive = false + searchText = "" + } + + collectionView.isEditing = isEdit + collectionView.reloadData() + myPlacesDelegate?.updateEditMode(isEdit) + configureNavigation() + navigationController?.setToolbarHidden(!isEdit, 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 text = alert?.textFields?.first?.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return } + let oldGroupName = folder.bridgeItem.groupName + let newGroupName = self?.groupName(oldGroupName, replacingLastComponentWith: text) ?? text + OAFavoritesSwiftHelper.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 OAFavoritesSwiftHelper.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 OAFavoritesSwiftHelper.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) + } + + @objc func selectButtonPressed() { + setEdit(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() { + setEdit(false) + configureToolbar() + } + + @objc func selectAllButtonPressed() { + let selectableIndexPaths = selectableIndexPaths() + if areAllSelectableItemsSelected() { + selectableIndexPaths.forEach { collectionView.deselectItem(at: $0, animated: false) } + } else { + selectableIndexPaths.forEach { collectionView.selectItem(at: $0, animated: false, scrollPosition: []) } + } + + updateSelectionUI() + } + + @objc func favoriteDataDidChange() { + 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) + setEdit(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: OAFavoritesSwiftHelper.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 = OAFavoritesSwiftHelper.geoURLString(forFavoritePoint: point) + if !geoURLString.isEmpty { + sharingText.append("\n") + sharingText.append("Location: \(geoURLString)") + } + + let shareURLString = OAFavoritesSwiftHelper.sharePoiURLString(forFavoritePoint: point) + if !shareURLString.isEmpty { + sharingText.append("\n") + sharingText.append(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 = OAFavoritesSwiftHelper.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 OAFavoritesSwiftHelper.deleteFavoriteItems(items) { + clearFavoriteSortModes(forGroupNames: groupNames) + } + + setEdit(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..2c804d7173 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift @@ -0,0 +1,214 @@ +// +// FavoriteListViewController+Cells.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import CoreLocation +import UniformTypeIdentifiers + +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.contentView.subviews.forEach { $0.removeFromSuperview() } + guard let self, let banner = Bundle.main.loadNibNamed("FreeBackupBanner", owner: self)?.first as? FreeBackupBanner else { return } + banner.configure(bannerType: .favorite) + banner.didOsmAndCloudButtonAction = { [weak self] in + self?.navigationController?.pushViewController(OACloudIntroductionViewController(), animated: true) + } + banner.didCloseButtonAction = { [weak self] in + self?.closeFreeBackupBanner() + } + banner.translatesAutoresizingMaskIntoConstraints = false + cell.contentView.addSubview(banner) + let fittingWidth = cell.contentView.bounds.width > 0.0 ? cell.contentView.bounds.width : cell.bounds.width + NSLayoutConstraint.activate([banner.topAnchor.constraint(equalTo: cell.contentView.topAnchor), banner.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), banner.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor)]) + NSLayoutConstraint.activate([banner.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), banner.heightAnchor.constraint(equalToConstant: self.backupBannerHeight(banner, fittingWidth: fittingWidth))]) + } + } + + var folderCellRegistration: RowCellRegistration { + RowCellRegistration { [weak self] cell, _, folder in + var content = cell.defaultContentConfiguration() + let iconName = folder.isPinned ? "ic_custom_folder_pin" : folder.iconName + content.image = UIImage.templateImageNamed(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.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 + 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.contentView.subviews.forEach { $0.removeFromSuperview() } + let label = UILabel() + label.font = .preferredFont(forTextStyle: .footnote) + label.adjustsFontForContentSizeCategory = true + label.textColor = .textColorSecondary + label.textAlignment = .center + label.numberOfLines = 0 + label.text = stats.text + label.translatesAutoresizingMaskIntoConstraints = false + cell.contentView.addSubview(label) + NSLayoutConstraint.activate([label.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: Self.statsFooterInsets.top), label.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: Self.statsFooterInsets.leading), label.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -Self.statsFooterInsets.trailing), label.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -Self.statsFooterInsets.bottom)]) + } + } + + 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: UIImage.templateImageNamed("ic_custom_search") ?? .icCustomFavorites, + 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) + } + } + + private func favoriteSecondaryAttributedText(for favorite: FavoritePointRow, includesGroupName: Bool) -> NSAttributedString { + let font = UIFont.systemFont(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 = rotatedFavoriteDirectionIcon(directionIcon, radians: 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) + } + + private func rotatedFavoriteDirectionIcon(_ image: UIImage, radians: CGFloat) -> UIImage { + let format = UIGraphicsImageRendererFormat() + format.scale = image.scale + format.opaque = false + let renderer = UIGraphicsImageRenderer(size: image.size, format: format) + return renderer.image { context in + let rect = CGRect(origin: CGPoint(x: -image.size.width / 2.0, y: -image.size.height / 2.0), + size: image.size) + context.cgContext.translateBy(x: image.size.width / 2.0, y: image.size.height / 2.0) + context.cgContext.rotate(by: radians) + image.draw(in: rect) + } + } +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift new file mode 100644 index 0000000000..3b98452a69 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift @@ -0,0 +1,214 @@ +// +// FavoriteListViewController+ContextMenu.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import CoreLocation +import UniformTypeIdentifiers + +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 hasDirectFavoritePoints = folder.bridgeItem.pointsCount > 0 + 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 } + OAFavoritesSwiftHelper.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 } + OAFavoritesSwiftHelper.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 = OAFavoritesSwiftHelper.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 + OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: hasDirectFavoritePoints ? folderFavoriteItem : subtreeFavoriteItems) + } + let trackAction = UIAction(title: localizedString("add_to_a_track"), image: .icCustomTrip) { [weak self] _ in + guard let self else { return } + if hasDirectFavoritePoints { + self.openFavoriteGroupAddToTrack(folder.bridgeItem.groupName) + } else { + self.openFavoriteItemsAddToTrack(subtreeFavoriteItems) + } + } + let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in + guard let self else { return } + let directFavoriteItems: [Any] = self.favoritePointRows(forGroupName: folder.bridgeItem.groupName).map { $0.bridgeItem } + OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: hasDirectFavoritePoints ? directFavoriteItems : subtreeFavoriteItems) + } + let addToActions: [UIMenuElement] = hasFavoritePoints ? [mapMarkersAction, trackAction, navigationAction] : [] + let fourthButtons: [UIMenuElement] = addToActions.isEmpty ? [] : [UIMenu(title: localizedString("shared_string_add"), 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 = OAFavoritesSwiftHelper.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 + OAFavoritesSwiftHelper.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 + OAFavoritesSwiftHelper.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 + OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: selectedBridgeItems) + self?.setEdit(false) + self?.applySnapshot(animatingDifferences: true) + } + let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: .icCustomTrip) { [weak self] _ in + self?.openFavoriteItemsAddToTrack(selectedBridgeItems) + self?.setEdit(false) + self?.applySnapshot(animatingDifferences: true) + } + let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in + OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: selectedBridgeItems) + self?.applySnapshot(animatingDifferences: true) + } + let addToMenu = UIMenu(title: localizedString("add_to"), image: .icCustomAdd, children: [trackAction, navigationAction, 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 + OAFavoritesSwiftHelper.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 + OAFavoritesSwiftHelper.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 + OAFavoritesSwiftHelper.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 + OAFavoritesSwiftHelper.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..0b6c1947e8 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift @@ -0,0 +1,352 @@ +// +// FavoriteListViewController+DataSource.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import CoreLocation +import UniformTypeIdentifiers + +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(OAFavoritesSwiftHelper.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(OAFavoritesSwiftHelper.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(OAFavoritesSwiftHelper.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), OAFavoritesSwiftHelper.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 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) + } + + func closeFreeBackupBanner() { + UserDefaults.standard.set(true, forKey: Self.wasClosedFreeBackupFavoritesBannerKey) + applySnapshot(animatingDifferences: true) + } + + func favoriteFolders() -> [FavoriteFolderRow] { + OAFavoritesSwiftHelper.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 + } + + var snapshot = Snapshot() + if allFolders.isEmpty { + layoutSections = [] + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections([.emptyState]) + snapshot.appendItems([.emptyState]) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + return + } + + 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(OAFavoritesSwiftHelper.favoritePoints(forGroupName: folder.bridgeItem.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.bridgeItem.address) }, mode: currentSortMode) + var snapshot = Snapshot() + if favorites.isEmpty && folders.isEmpty { + layoutSections = [] + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections([.emptyState]) + snapshot.appendItems([.emptyState]) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + return + } + 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) + var snapshot = Snapshot() + if favorites.isEmpty { + layoutSections = [] + collectionView.collectionViewLayout.invalidateLayout() + snapshot.appendSections([.emptyState]) + snapshot.appendItems([.emptyState]) + dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + return + } + + 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 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 else { return false } + guard 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..32284d1aec --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift @@ -0,0 +1,246 @@ +// +// FavoriteListViewController+Delegates.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import CoreLocation +import UniformTypeIdentifiers + +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 { + 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 { + updateSelectionUI() + return + } + OAFavoritesSwiftHelper.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 } + 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(OAFavoritesSwiftHelper.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(OAFavoritesSwiftHelper.formattedCoordinates(forFavoritePoint: pointToShare)) + case .geo: + copyFavoritePointShareText(OAFavoritesSwiftHelper.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 { + OAFavoritesSwiftHelper.changeFavoriteItems(bridgeItems(for: selectedItems), colorIndex: colorController.colorIndex) + } + + setEdit(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) + OAFavoritesSwiftHelper.moveFavoriteItems(favoriteItemsToMove, toGroupName: targetGroupName) + updateFavoriteSortModeKeysAfterMove(favoriteItemsToMove, toGroupName: targetGroupName) + setEdit(false) + applySnapshot(animatingDifferences: true) + } +} + +extension FavoriteListViewController: OAOpenAddTrackDelegate { + func onFileSelected(_ gpxFilePath: String) { + if let addToTrackFavoriteItems { + OAFavoritesSwiftHelper.addFavoriteItems(toTrack: addToTrackFavoriteItems, gpxFileName: gpxFilePath) + self.addToTrackFavoriteItems = nil + } else if let addToTrackGroupName { + OAFavoritesSwiftHelper.addFavoriteGroup(toTrack: addToTrackGroupName, gpxFileName: gpxFilePath) + self.addToTrackGroupName = nil + } + } +} + +extension FavoriteListViewController: OAEditorDelegate { + func addNewItem(withName name: String?, iconName: String, color: UIColor, backgroundIconName: String) { + guard OAFavoritesSwiftHelper.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 + 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() { + applySnapshot() + } +} + +extension FavoriteListViewController: UIDocumentPickerDelegate { + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url = urls.first else { return } + OARootViewController.instance().import(asFavorites: url) + } +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift new file mode 100644 index 0000000000..6fb05a90c6 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift @@ -0,0 +1,149 @@ +// +// FavoriteListViewController+Models.swift +// OsmAnd Maps +// +// Created by Dmitry Svetlichny on 04.06.2026. +// Copyright © 2026 OsmAnd. All rights reserved. +// + +import CoreLocation +import UniformTypeIdentifiers + +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 +} + +struct FavoriteSortHeader: Hashable { + let sortMode: FavoriteSortMode + let includesDistanceSortModes: Bool +} + +struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { + static let subtitleDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "d MMM" + 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 = Self.rowHeight + return attributes + } +} diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift index 8f7ea2ebe3..94e1851bcc 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift @@ -6,228 +6,78 @@ // Copyright © 2026 OsmAnd. All rights reserved. // -import CoreLocation -import UniformTypeIdentifiers - -private enum ScreenMode { - case root - case folder(FavoriteFolderRow, previousTitle: String) -} - -private 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") - } - } -} - -private enum FavoriteListSection: Hashable { - case sortHeader - case backupBanner - case folderSection(FavoriteFolderSection) - case content - case statsFooter - case emptyState -} - -private enum FavoriteListItem: Hashable { - case sortHeader(FavoriteSortHeader) - case backupBanner - case header(FavoriteFolderSection) - case folder(FavoriteFolderRow) - case favorite(FavoritePointRow) - case statsFooter(FavoriteFolderStats) - case emptyState -} - -private struct FavoriteSortHeader: Hashable { - let sortMode: FavoriteSortMode - let includesDistanceSortModes: Bool -} - -private struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { - private static let subtitleDateFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "d MMM" - 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 - } -} - -private 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 - } -} - -private 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()) - } -} - -private 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 = Self.rowHeight - return attributes - } -} - final class FavoriteListViewController: UIViewController { - private typealias DataSource = UICollectionViewDiffableDataSource - private typealias Snapshot = NSDiffableDataSourceSnapshot - private typealias CellRegistration = UICollectionView.CellRegistration - private typealias RowCellRegistration = UICollectionView.CellRegistration - - weak var myPlacesDelegate: MyPlacesDelegate? - - private static let imageSize: CGFloat = 30.0 - private static let favoriteIconSize: CGFloat = 36.0 - private static let sortHeaderHeight: CGFloat = 44.0 - private static let sortHeaderLeadingInset: CGFloat = 16.0 - private static let navigationTitleFontSize: CGFloat = 17.0 - private static let navigationTitleMaximumSize: CGFloat = 22.0 - private static let navigationSubtitleFontSize: CGFloat = 12.0 - private static let navigationSubtitleMaximumSize: CGFloat = 18.0 - private static let statsFooterInsets = NSDirectionalEdgeInsets(top: 12.0, leading: 20.0, bottom: 12.0, trailing: 20.0) - private static let wasClosedFreeBackupFavoritesBannerKey = "wasClosedFreeBackupFavoritesBanner" - - private let screenMode: ScreenMode - private let settings = OAAppSettings.sharedManager() - private var layoutSections: [FavoriteListSection] = [] - private let appearanceCollection: OAGPXAppearanceCollection = .sharedInstance() - private var groupController: OAEditGroupViewController? - private var colorController: OAEditColorViewController? - private var favoriteItemsToMove: [Any]? - private var favoriteGroupAppearanceGroupName: String? - private var favoriteGroupAppearanceEditor: OAFavoriteGroupEditorViewController? - private var addToTrackGroupName: String? - private var addToTrackFavoriteItems: [Any]? - private var pointToShare: OAFavoritePointBridgeItem? - private var searchText = "" - private var isSearchActive = false - private var isSelectionModeInSearch = false - private var lastDistanceDirectionUpdate: TimeInterval = 0.0 - private var isContextMenuVisible = false - private var shouldReloadCollectionView = false - private var locationUpdateObserver: OAAutoObserverProxy? - private var headingUpdateObserver: OAAutoObserverProxy? - private var isSearchResultsMode: Bool { + 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 statsFooterInsets = NSDirectionalEdgeInsets(top: 12.0, leading: 20.0, bottom: 12.0, trailing: 20.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 } - private var isAvailablePaymentBanner: Bool { + var isAvailablePaymentBanner: Bool { isRootFolder && !isSearchResultsMode && !UserDefaults.standard.bool(forKey: Self.wasClosedFreeBackupFavoritesBannerKey) && !OAIAPHelper.isOsmAndProAvailable() && !OABackupHelper.sharedInstance().isRegistered() } - private var isRootFolder: Bool { + var isRootFolder: Bool { guard case .root = screenMode else { return false } return true } - private var normalTitle: String { + var normalTitle: String { switch screenMode { case .root: localizedString("shared_string_favorites") case .folder(let folder, _): folder.title } } - private var normalSubtitle: String { - switch screenMode { - case .root: localizedString("shared_string_my_places") - case .folder(_, let previousTitle): previousTitle - } - } - private var parentGroupName: String? { + var parentGroupName: String? { guard case .folder(let folder, _) = screenMode, !folder.bridgeItem.groupName.isEmpty else { return nil } return folder.bridgeItem.groupName } - private var searchParentGroupName: String? { + var searchParentGroupName: String? { guard case .folder(let folder, _) = screenMode else { return nil } return folder.bridgeItem.groupName } - private var currentSortMode: FavoriteSortMode { + var currentSortMode: FavoriteSortMode { isSearchResultsMode ? searchFavoriteSortMode() : favoriteSortMode() } - private var currentSortHeader: FavoriteSortHeader { + var currentSortHeader: FavoriteSortHeader { FavoriteSortHeader(sortMode: currentSortMode, includesDistanceSortModes: isSearchResultsMode || !isRootFolder) } - private var currentSortEntryId: String { + var currentSortEntryId: String { parentGroupName ?? "" } - private lazy var collectionView: UICollectionView = { + lazy var collectionView: UICollectionView = { let collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout()) collectionView.backgroundColor = .clear collectionView.tintColor = .iconColorActive @@ -238,100 +88,7 @@ final class FavoriteListViewController: UIViewController { collectionView.allowsMultipleSelectionDuringEditing = true return collectionView }() - private lazy var headerCellRegistration = 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 - } - private lazy var sortHeaderCellRegistration = UICollectionView.CellRegistration { [weak self] cell, _, sortHeader in - cell.sortButton.setImage(sortHeader.sortMode.image, for: .normal) - cell.sortButton.menu = self?.makeSortMenu(includesDistanceSortModes: sortHeader.includesDistanceSortModes) - } - private lazy var backupBannerCellRegistration = UICollectionView.CellRegistration { [weak self] cell, _, _ in - cell.contentView.subviews.forEach { $0.removeFromSuperview() } - guard let self, let banner = Bundle.main.loadNibNamed("FreeBackupBanner", owner: self)?.first as? FreeBackupBanner else { return } - banner.configure(bannerType: .favorite) - banner.didOsmAndCloudButtonAction = { [weak self] in - self?.navigationController?.pushViewController(OACloudIntroductionViewController(), animated: true) - } - banner.didCloseButtonAction = { [weak self] in - self?.closeFreeBackupBanner() - } - banner.translatesAutoresizingMaskIntoConstraints = false - cell.contentView.addSubview(banner) - let fittingWidth = cell.contentView.bounds.width > 0.0 ? cell.contentView.bounds.width : cell.bounds.width - NSLayoutConstraint.activate([banner.topAnchor.constraint(equalTo: cell.contentView.topAnchor), banner.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), banner.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor)]) - NSLayoutConstraint.activate([banner.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), banner.heightAnchor.constraint(equalToConstant: self.backupBannerHeight(banner, fittingWidth: fittingWidth))]) - } - private lazy var folderCellRegistration = RowCellRegistration { [weak self] cell, _, folder in - var content = cell.defaultContentConfiguration() - let iconName = folder.isPinned ? "ic_custom_folder_pin" : folder.iconName - content.image = UIImage.templateImageNamed(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.secondaryText = folder.subtitle - content.secondaryTextProperties.color = .textColorSecondary - cell.contentConfiguration = content - cell.backgroundConfiguration = self?.listCellBackgroundConfiguration() - cell.accessories = self?.collectionView.isEditing == true ? [.multiselect()] : [.multiselect(), .disclosureIndicator()] - } - private lazy var favoriteCellRegistration = RowCellRegistration { [weak self] cell, _, favorite in - 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()] - } - private lazy var statsFooterCellRegistration = UICollectionView.CellRegistration { cell, _, stats in - cell.backgroundColor = .clear - cell.contentView.backgroundColor = .clear - cell.contentView.subviews.forEach { $0.removeFromSuperview() } - let label = UILabel() - label.font = .preferredFont(forTextStyle: .footnote) - label.adjustsFontForContentSizeCategory = true - label.textColor = .textColorSecondary - label.textAlignment = .center - label.numberOfLines = 0 - label.text = stats.text - label.translatesAutoresizingMaskIntoConstraints = false - cell.contentView.addSubview(label) - NSLayoutConstraint.activate([label.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: Self.statsFooterInsets.top), label.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: Self.statsFooterInsets.leading), label.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -Self.statsFooterInsets.trailing), label.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -Self.statsFooterInsets.bottom)]) - } - private lazy var emptyStateCellRegistration = 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: UIImage.templateImageNamed("ic_custom_search") ?? .icCustomFavorites, - 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) - } - - private lazy var subfolderSearchController: UISearchController = { + lazy var subfolderSearchController: UISearchController = { let searchController = UISearchController(searchResultsController: nil) searchController.searchResultsUpdater = self searchController.searchBar.delegate = self @@ -339,121 +96,64 @@ final class FavoriteListViewController: UIViewController { searchController.searchBar.searchTextField.placeholder = localizedString("search_activity") return searchController }() - private lazy var dataSource: DataSource = makeDataSource() - - private func favoriteSecondaryAttributedText(for favorite: FavoritePointRow, includesGroupName: Bool) -> NSAttributedString { - let font = UIFont.systemFont(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) + 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 } - - 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)) + + convenience init(frame: CGRect) { + self.init(frame: frame, screenMode: .root) } - 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 = rotatedFavoriteDirectionIcon(directionIcon, radians: 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)) + init(frame: CGRect, screenMode: ScreenMode) { + self.screenMode = screenMode + super.init(nibName: nil, bundle: nil) + view.frame = frame } - private func appendFavoriteSecondarySeparatorIfNeeded(to result: NSMutableAttributedString, attributes: [NSAttributedString.Key: Any]) { - guard result.length > 0 else { return } - result.append(NSAttributedString(string: " • ", attributes: attributes)) + required init?(coder: NSCoder) { + screenMode = .root + super.init(coder: coder) } - - 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) + + 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) } - private func rotatedFavoriteDirectionIcon(_ image: UIImage, radians: CGFloat) -> UIImage { - let format = UIGraphicsImageRendererFormat() - format.scale = image.scale - format.opaque = false - let renderer = UIGraphicsImageRenderer(size: image.size, format: format) - return renderer.image { context in - let rect = CGRect(origin: CGPoint(x: -image.size.width / 2.0, y: -image.size.height / 2.0), - size: image.size) - context.cgContext.translateBy(x: image.size.width / 2.0, y: image.size.height / 2.0) - context.cgContext.rotate(by: radians) - image.draw(in: rect) - } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + definesPresentationContext = true + configureNavigation() + navigationController?.setToolbarHidden(true, animated: false) + configureToolbar() + applySnapshot() + registerDistanceAndDirectionObservers() + updateDistanceAndDirection(true) } - private func registerDistanceAndDirectionObservers() { + override func viewWillDisappear(_ animated: Bool) { 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 - } + if !isRootFolder { + navigationItem.searchController = nil + navigationController?.setNavigationBarHidden(true, animated: false) + } - @objc private func updateDistanceAndDirection() { - updateDistanceAndDirection(false) + definesPresentationContext = false + super.viewWillDisappear(animated) } - - private func updateDistanceAndDirection(_ forceUpdate: Bool) { + + func updateDistanceAndDirection(_ forceUpdate: Bool) { guard Thread.isMainThread else { DispatchQueue.main.async { [weak self] in self?.updateDistanceAndDirection(forceUpdate) @@ -483,56 +183,83 @@ final class FavoriteListViewController: UIViewController { applySnapshot(animatingDifferences: false) } - convenience init(frame: CGRect) { - self.init(frame: frame, screenMode: .root) - } - - private init(frame: CGRect, screenMode: ScreenMode) { - self.screenMode = screenMode - super.init(nibName: nil, bundle: nil) - view.frame = frame + func listCellBackgroundConfiguration() -> UIBackgroundConfiguration { + var configuration = UIBackgroundConfiguration.listGroupedCell() + configuration.backgroundColor = .groupBg + return configuration } - required init?(coder: NSCoder) { - screenMode = .root - super.init(coder: coder) - } + 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 + } - deinit { - unregisterDistanceAndDirectionObservers() - NotificationCenter.default.removeObserver(self, name: .favoriteImportViewControllerDidDismiss, object: nil) - NotificationCenter.default.removeObserver(self, name: Notification.Name(NSNotification.Name.OAIAPProductPurchased.rawValue), object: nil) + navigationController?.navigationBar.prefersLargeTitles = false + configureNavigationButtons() + configureSearchVisibility() + updateNavigationBarTitle() + updateSegmentedControlVisibility() } + + func configureToolbar() { + guard !isSearchActive || collectionView.isEditing else { + if hasSearchResults() { + configureSearchToolbar() + } + return + } - 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) + 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 + } } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - definesPresentationContext = true - configureNavigation() - navigationController?.setToolbarHidden(true, animated: false) + + func updateSelectionUI() { + updateNavigationBarTitle() + configureNavigationButtons() configureToolbar() - applySnapshot() - registerDistanceAndDirectionObservers() - updateDistanceAndDirection(true) } - - override func viewWillDisappear(_ animated: Bool) { + + func updateSegmentedControlVisibility() { + myPlacesDelegate?.updateSegmentedControlVisibility(isRootFolder && !collectionView.isEditing && !isSearchResultsMode) + } + + private func registerDistanceAndDirectionObservers() { unregisterDistanceAndDirectionObservers() - if !isRootFolder { - navigationItem.searchController = nil - navigationController?.setNavigationBarHidden(true, animated: false) - } + 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) + } - definesPresentationContext = false - super.viewWillDisappear(animated) + private func unregisterDistanceAndDirectionObservers() { + locationUpdateObserver?.detach() + locationUpdateObserver = nil + headingUpdateObserver?.detach() + headingUpdateObserver = nil } private func configureCollectionView() { @@ -576,30 +303,7 @@ final class FavoriteListViewController: UIViewController { let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item]) return NSCollectionLayoutSection(group: group) } - - private func listCellBackgroundConfiguration() -> UIBackgroundConfiguration { - var configuration = UIBackgroundConfiguration.listGroupedCell() - configuration.backgroundColor = .groupBg - return configuration - } - - private 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() - } - + private func configureNavigationButtons() { let targetNavigationItem = isRootFolder ? navigationController?.navigationBar.topItem : navigationItem if collectionView.isEditing { @@ -643,34 +347,7 @@ final class FavoriteListViewController: UIViewController { navigationItem.hidesSearchBarWhenScrolling = false navigationItem.searchController = collectionView.isEditing ? nil : subfolderSearchController } - - private 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 - } - } - + private func configureSearchToolbar() { let selectButton = UIBarButtonItem(title: localizedString("shared_string_select"), style: .plain, target: self, action: #selector(searchSelectButtonPressed)) selectButton.accessibilityLabel = localizedString("shared_string_select") @@ -681,13 +358,7 @@ final class FavoriteListViewController: UIViewController { toolbarItems = items } } - - private func updateSelectionUI() { - updateNavigationBarTitle() - configureNavigationButtons() - configureToolbar() - } - + private func updateNavigationBarTitle() { if collectionView.isEditing { let selectedItems = bridgeItems(for: collectionView.indexPathsForSelectedItems ?? []) @@ -713,1249 +384,11 @@ final class FavoriteListViewController: UIViewController { 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)) } } - - private func updateSegmentedControlVisibility() { - myPlacesDelegate?.updateSegmentedControlVisibility(isRootFolder && !collectionView.isEditing && !isSearchResultsMode) - } - - private func favoriteSortMode(entryId: String? = nil) -> FavoriteSortMode { - let sortModes = settings.getFavoriteSortModes() - guard let sortModeTitle = sortModes[entryId ?? currentSortEntryId] else { return FavoriteSortModeHelper.defaultSortMode() } - return FavoriteSortMode.byTitle(sortModeTitle) - } - - private func searchFavoriteSortMode() -> FavoriteSortMode { - let sortModeTitle = settings.searchFavoriteSortMode.get() - return FavoriteSortMode.byTitle(sortModeTitle) - } - - 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 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) - } - - private func renameFavoriteSortModeKeys(from oldGroupName: String, to newGroupName: String, existingGroupNames: Set? = nil) { - guard !oldGroupName.isEmpty, oldGroupName != newGroupName else { return } - let groupNames = existingGroupNames ?? Set(OAFavoritesSwiftHelper.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) - } - - private func updateFavoriteSortModeKeysAfterMove(_ favoriteItems: [Any], toGroupName targetGroupName: String) { - let groupNames = Set(OAFavoritesSwiftHelper.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) - } - } - - private 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(OAFavoritesSwiftHelper.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), OAFavoritesSwiftHelper.addFavoriteGroup(folderName, parentGroupName: parentGroupName.isEmpty ? nil : parentGroupName, iconName: nil, color: nil, backgroundIconName: nil) { - existingGroupNames.insert(newGroupName) - } - parentGroupName = newGroupName - } - } - - private func isFavoriteSortModeKey(_ key: String, insideOrEqualTo groupName: String) -> Bool { - key == groupName || (!groupName.isEmpty && key.hasPrefix(groupName + "/")) - } - - private 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) - } - - 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 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: ()) - } - } - } - - private func applySnapshot(animatingDifferences: Bool = false) { - switch screenMode { - case .root: - applyRootSnapshot(animatingDifferences: animatingDifferences) - case .folder(let folder, _): - applyFolderSnapshot(folder: folder, animatingDifferences: animatingDifferences) - } - } - - private func applyRootSnapshot(animatingDifferences: Bool) { - let allFolders = favoriteFolders() - if isSearchResultsMode { - applySearchSnapshot(allFolders: allFolders, parentGroupName: nil, animatingDifferences: animatingDifferences) - return - } - - var snapshot = Snapshot() - if allFolders.isEmpty { - layoutSections = [] - collectionView.collectionViewLayout.invalidateLayout() - snapshot.appendSections([.emptyState]) - snapshot.appendItems([.emptyState]) - dataSource.apply(snapshot, animatingDifferences: animatingDifferences) - return - } - - 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(OAFavoritesSwiftHelper.favoritePoints(forGroupName: folder.bridgeItem.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.bridgeItem.address) }, mode: currentSortMode) - var snapshot = Snapshot() - if favorites.isEmpty && folders.isEmpty { - layoutSections = [] - collectionView.collectionViewLayout.invalidateLayout() - snapshot.appendSections([.emptyState]) - snapshot.appendItems([.emptyState]) - dataSource.apply(snapshot, animatingDifferences: animatingDifferences) - return - } - 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) - var snapshot = Snapshot() - if favorites.isEmpty { - layoutSections = [] - collectionView.collectionViewLayout.invalidateLayout() - snapshot.appendSections([.emptyState]) - snapshot.appendItems([.emptyState]) - dataSource.apply(snapshot, animatingDifferences: animatingDifferences) - return - } - - 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 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 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) - } - - 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 closeFreeBackupBanner() { - UserDefaults.standard.set(true, forKey: Self.wasClosedFreeBackupFavoritesBannerKey) - applySnapshot(animatingDifferences: true) - } - - private func directFavoriteFolders(_ folders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoriteFolderRow] { - folders.filter { isDirectFolder($0.bridgeItem.groupName, parentGroupName: parentGroupName) } - } - - private func favoriteFolders() -> [FavoriteFolderRow] { - OAFavoritesSwiftHelper.favoriteFolders().map { FavoriteFolderRow(item: $0) } - } - - private func isDirectFolder(_ groupName: String, parentGroupName: String?) -> Bool { - guard let parentGroupName else { return groupName.isEmpty || !groupName.contains("/") } - guard !parentGroupName.isEmpty else { return false } - guard groupName.hasPrefix(parentGroupName + "/") else { return false } - let childPath = groupName.dropFirst(parentGroupName.count + 1) - return !childPath.isEmpty && !childPath.contains("/") - } - - private func isNestedFolder(_ groupName: String, in parentGroupName: String) -> Bool { - guard !parentGroupName.isEmpty else { return false } - return groupName.hasPrefix(parentGroupName + "/") - } - - 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 hasSearchResults() -> Bool { - !searchFavoritePointRows(allFolders: favoriteFolders(), parentGroupName: searchParentGroupName).isEmpty - } - - private func shouldHideSearchToolbar() -> Bool { - !collectionView.isEditing && (!isSearchActive || !hasSearchResults()) - } - - private func searchFavoritePointRows(allFolders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoritePointRow] { - favoritePointRows(allFolders: allFolders, parentGroupName: parentGroupName).filter { matchesSearch($0.title) } - } - - private func clearSearchControllerText() { - if isRootFolder { - navigationController?.navigationBar.topItem?.searchController?.searchBar.text = "" - } else { - subfolderSearchController.searchBar.text = "" - } - } - - private func selectableIndexPaths() -> [IndexPath] { - var indexPaths: [IndexPath] = [] - for section in 0.. Bool { - let selectableIndexPaths = selectableIndexPaths() - guard !selectableIndexPaths.isEmpty else { return false } - let selectedIndexPaths = Set(collectionView.indexPathsForSelectedItems ?? []) - return selectableIndexPaths.allSatisfy { selectedIndexPaths.contains($0) } - } - - private func openNewFavoriteGroupEditor() { - guard let navigationController, let viewController = OAFavoriteGroupEditorViewController(new: ()) else { return } - viewController.delegate = self - let modalNavigationController = UINavigationController(rootViewController: viewController) - navigationController.present(modalNavigationController, animated: true) - } - - private func openFavoriteGroupAppearance(_ groupName: String) { - guard let viewController = OAFavoriteGroupEditorViewController(group: OAFavoritesSwiftHelper.pointsGroup(forGroupName: groupName)) else { return } - favoriteGroupAppearanceGroupName = groupName - favoriteGroupAppearanceEditor = viewController - viewController.delegate = self - navigationController?.pushViewController(viewController, animated: true) - } - - private func openFavoriteItemsMove(_ favoriteItems: [Any]) { - guard !favoriteItems.isEmpty, - let navigationController, - let groupController = OAEditGroupViewController(groupName: nil, groups: OAFavoritesSwiftHelper.favoriteGroupNames(forMovingFavoriteItems: favoriteItems)) else { - return - } - self.groupController = groupController - favoriteItemsToMove = favoriteItems - groupController.delegate = self - let modalNavigationController = UINavigationController(rootViewController: groupController) - navigationController.present(modalNavigationController, animated: true) - } - - private func openFavoriteGroupAddToTrack(_ groupName: String) { - guard OAFavoritesSwiftHelper.canUseGroup(withName: groupName), let navigationController, let viewController = OAOpenAddTrackViewController(screenType: .addToATrack) else { return } - addToTrackGroupName = groupName - addToTrackFavoriteItems = nil - viewController.delegate = self - let modalNavigationController = UINavigationController(rootViewController: viewController) - navigationController.present(modalNavigationController, animated: true) - } - - private func openFavoriteItemsAddToTrack(_ favoriteItems: [Any]) { - guard !favoriteItems.isEmpty, let navigationController, let viewController = OAOpenAddTrackViewController(screenType: .addToATrack) else { return } - addToTrackFavoriteItems = favoriteItems - addToTrackGroupName = nil - viewController.delegate = self - let modalNavigationController = UINavigationController(rootViewController: viewController) - navigationController.present(modalNavigationController, animated: true) - } - - private func favoritePointRows(forGroupName groupName: String) -> [FavoritePointRow] { - let sortMode = isSearchResultsMode ? searchFavoriteSortMode() : favoriteSortMode(entryId: groupName) - let favorites = OAFavoritesSwiftHelper.favoritePoints(forGroupName: groupName).map { FavoritePointRow(item: $0) } - return FavoriteSortModeHelper.sortFavoritePointsWithMode(favorites, mode: sortMode) - } - - private func favoritePointRows(allFolders: [FavoriteFolderRow], parentGroupName: String?) -> [FavoritePointRow] { - allFolders.filter { isSearchGroup($0.bridgeItem.groupName, parentGroupName: parentGroupName) }.flatMap { OAFavoritesSwiftHelper.favoritePoints(forGroupName: $0.bridgeItem.groupName).map { FavoritePointRow(item: $0) } } - } - - private func isSearchGroup(_ groupName: String, parentGroupName: String?) -> Bool { - guard let parentGroupName else { return true } - guard !parentGroupName.isEmpty else { return groupName.isEmpty } - return groupName == parentGroupName || isNestedFolder(groupName, in: parentGroupName) - } - - private 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]) - } - - private func setEdit(_ isEdit: Bool) { - let shouldResetSearchSelection = !isEdit && isSelectionModeInSearch - if !isEdit { - collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) } - isSelectionModeInSearch = false - isSearchActive = false - searchText = "" - } - - collectionView.isEditing = isEdit - collectionView.reloadData() - myPlacesDelegate?.updateEditMode(isEdit) - configureNavigation() - navigationController?.setToolbarHidden(!isEdit, animated: true) - if shouldResetSearchSelection { - clearSearchControllerText() - applySnapshot(animatingDifferences: false) - } - } - - private 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 hasDirectFavoritePoints = folder.bridgeItem.pointsCount > 0 - 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 } - OAFavoritesSwiftHelper.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 } - OAFavoritesSwiftHelper.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 = OAFavoritesSwiftHelper.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 - OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: hasDirectFavoritePoints ? folderFavoriteItem : subtreeFavoriteItems) - } - let trackAction = UIAction(title: localizedString("add_to_a_track"), image: .icCustomTrip) { [weak self] _ in - guard let self else { return } - if hasDirectFavoritePoints { - self.openFavoriteGroupAddToTrack(folder.bridgeItem.groupName) - } else { - self.openFavoriteItemsAddToTrack(subtreeFavoriteItems) - } - } - let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in - guard let self else { return } - let directFavoriteItems: [Any] = self.favoritePointRows(forGroupName: folder.bridgeItem.groupName).map { $0.bridgeItem } - OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: hasDirectFavoritePoints ? directFavoriteItems : subtreeFavoriteItems) - } - let addToActions: [UIMenuElement] = hasFavoritePoints ? [mapMarkersAction, trackAction, navigationAction] : [] - let fourthButtons: [UIMenuElement] = addToActions.isEmpty ? [] : [UIMenu(title: localizedString("shared_string_add"), 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 }) - } - - private 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 = OAFavoritesSwiftHelper.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 - OAFavoritesSwiftHelper.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 - OAFavoritesSwiftHelper.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]) - } - - private 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 text = alert?.textFields?.first?.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return } - let oldGroupName = folder.bridgeItem.groupName - let newGroupName = self?.groupName(oldGroupName, replacingLastComponentWith: text) ?? text - OAFavoritesSwiftHelper.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) - } - - 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: OAFavoritesSwiftHelper.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 = OAFavoritesSwiftHelper.geoURLString(forFavoritePoint: point) - if !geoURLString.isEmpty { - sharingText.append("\n") - sharingText.append("Location: \(geoURLString)") - } - - let shareURLString = OAFavoritesSwiftHelper.sharePoiURLString(forFavoritePoint: point) - if !shareURLString.isEmpty { - sharingText.append("\n") - sharingText.append(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 = OAFavoritesSwiftHelper.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 OAFavoritesSwiftHelper.deleteFavoriteItems(items) { - clearFavoriteSortModes(forGroupNames: groupNames) - } - - setEdit(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 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 - } - - private 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 - } - } - } - - private 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 - OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: selectedBridgeItems) - self?.setEdit(false) - self?.applySnapshot(animatingDifferences: true) - } - let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: .icCustomTrip) { [weak self] _ in - self?.openFavoriteItemsAddToTrack(selectedBridgeItems) - self?.setEdit(false) - self?.applySnapshot(animatingDifferences: true) - } - let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in - OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: selectedBridgeItems) - self?.applySnapshot(animatingDifferences: true) - } - let addToMenu = UIMenu(title: localizedString("add_to"), image: .icCustomAdd, children: [trackAction, navigationAction, 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 - OAFavoritesSwiftHelper.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 - OAFavoritesSwiftHelper.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 - OAFavoritesSwiftHelper.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 - OAFavoritesSwiftHelper.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) - } - - private 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) - } - 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) - } - - @objc private func selectButtonPressed() { - setEdit(true) - } - - @objc private func searchSelectButtonPressed() { - isSelectionModeInSearch = true - isSearchActive = false - if isRootFolder { - let searchController = navigationController?.navigationBar.topItem?.searchController - searchController?.isActive = false - } else { - subfolderSearchController.isActive = false - } - - selectButtonPressed() - } - - @objc private func cancelButtonPressed() { - setEdit(false) - configureToolbar() - } - - @objc private func selectAllButtonPressed() { - let selectableIndexPaths = selectableIndexPaths() - if areAllSelectableItemsSelected() { - selectableIndexPaths.forEach { collectionView.deselectItem(at: $0, animated: false) } - } else { - selectableIndexPaths.forEach { collectionView.selectItem(at: $0, animated: false, scrollPosition: []) } - } - - updateSelectionUI() - } - - @objc private func favoriteDataDidChange() { - applySnapshot(animatingDifferences: true) - } - - @objc private func productPurchased() { - DispatchQueue.main.async { [weak self] in - self?.applySnapshot(animatingDifferences: true) - } - } - - @objc private func shareButtonClicked(_ sender: Any) { - let sourceView = sender as? UIView ?? collectionView - shareItems(for: sourceView) - setEdit(false) - applySnapshot() - } - - @objc private 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 private 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 private func importButtonClicked(_ sender: Any) { - openPickerToImport() - } - - @objc private func clearSearchButtonClicked(_ sender: Any) { - searchText = "" - clearSearchControllerText() - configureToolbar() - navigationController?.setToolbarHidden(shouldHideSearchToolbar(), animated: true) - applySnapshot(animatingDifferences: false) - } -} - -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 { - 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 { - updateSelectionUI() - return - } - OAFavoritesSwiftHelper.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 } - 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(OAFavoritesSwiftHelper.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(OAFavoritesSwiftHelper.formattedCoordinates(forFavoritePoint: pointToShare)) - case .geo: - copyFavoritePointShareText(OAFavoritesSwiftHelper.geoURLString(forFavoritePoint: pointToShare)) - default: - break - } - } - - private 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 { - OAFavoritesSwiftHelper.changeFavoriteItems(bridgeItems(for: selectedItems), colorIndex: colorController.colorIndex) - } - - setEdit(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) - OAFavoritesSwiftHelper.moveFavoriteItems(favoriteItemsToMove, toGroupName: targetGroupName) - updateFavoriteSortModeKeysAfterMove(favoriteItemsToMove, toGroupName: targetGroupName) - setEdit(false) - applySnapshot(animatingDifferences: true) - } -} - -extension FavoriteListViewController: OAOpenAddTrackDelegate { - func onFileSelected(_ gpxFilePath: String) { - if let addToTrackFavoriteItems { - OAFavoritesSwiftHelper.addFavoriteItems(toTrack: addToTrackFavoriteItems, gpxFileName: gpxFilePath) - self.addToTrackFavoriteItems = nil - } else if let addToTrackGroupName { - OAFavoritesSwiftHelper.addFavoriteGroup(toTrack: addToTrackGroupName, gpxFileName: gpxFilePath) - self.addToTrackGroupName = nil - } - } -} - -extension FavoriteListViewController: OAEditorDelegate { - func addNewItem(withName name: String?, iconName: String, color: UIColor, backgroundIconName: String) { - guard OAFavoritesSwiftHelper.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 - 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() { - applySnapshot() - } -} - -extension FavoriteListViewController: UIDocumentPickerDelegate { - func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { - guard let url = urls.first else { return } - OARootViewController.instance().import(asFavorites: url) + deinit { + unregisterDistanceAndDirectionObservers() + NotificationCenter.default.removeObserver(self, name: .favoriteImportViewControllerDidDismiss, object: nil) + NotificationCenter.default.removeObserver(self, name: Notification.Name(NSNotification.Name.OAIAPProductPurchased.rawValue), object: nil) } } From 121130106b6cae7f7a4856d497aafe4c4a683604 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Mon, 15 Jun 2026 16:58:46 +0300 Subject: [PATCH 31/41] refactored --- OsmAnd.xcodeproj/project.pbxproj | 12 +++ .../FavoriteListViewController+Cells.swift | 7 +- ...avoriteListViewController+DataSource.swift | 3 +- .../OAFavoriteFolderBridgeItem.h | 30 ++++++ .../OAFavoriteFolderBridgeItem.mm | 41 +++++++++ .../OAFavoritePointBridgeItem.h | 34 +++++++ .../OAFavoritePointBridgeItem.mm | 71 +++++++++++++++ .../OAFavoritesSwiftHelper.h | 38 +------- .../OAFavoritesSwiftHelper.mm | 91 +------------------ Sources/OsmAnd Maps-Bridging-Header.h | 2 + 10 files changed, 197 insertions(+), 132 deletions(-) create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoriteFolderBridgeItem.h create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoriteFolderBridgeItem.mm create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.h create mode 100644 Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.mm diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index ac44d7ad88..4a41e4eafb 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -232,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 */; }; @@ -3756,6 +3758,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 = ""; }; @@ -8404,6 +8410,10 @@ C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */, 272AFB972FD2DC42006C2E21 /* OAFavoritesSwiftHelper.h */, 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */, + 275ED6592FE032290088D42B /* OAFavoriteFolderBridgeItem.h */, + 275ED65A2FE032600088D42B /* OAFavoriteFolderBridgeItem.mm */, + 275ED65C2FE032A90088D42B /* OAFavoritePointBridgeItem.h */, + 275ED65D2FE032C80088D42B /* OAFavoritePointBridgeItem.mm */, ); path = FavoriteListViewController; sourceTree = ""; @@ -18603,6 +18613,7 @@ 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 */, DA5A847226C563A900F274C7 /* OATurnResource.m in Sources */, 32AC14832D6DF0B7009BE64E /* ChipsCollectionHandler.swift in Sources */, @@ -18691,6 +18702,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/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift index 2c804d7173..5c54e09ff8 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift @@ -33,7 +33,7 @@ extension FavoriteListViewController { var backupBannerCellRegistration: UICollectionView.CellRegistration { UICollectionView.CellRegistration { [weak self] cell, _, _ in cell.contentView.subviews.forEach { $0.removeFromSuperview() } - guard let self, let banner = Bundle.main.loadNibNamed("FreeBackupBanner", owner: self)?.first as? FreeBackupBanner else { return } + guard let self, let banner = Bundle.main.loadNibNamed(FreeBackupBanner.reuseIdentifier, owner: self)?.first as? FreeBackupBanner else { return } banner.configure(bannerType: .favorite) banner.didOsmAndCloudButtonAction = { [weak self] in self?.navigationController?.pushViewController(OACloudIntroductionViewController(), animated: true) @@ -52,8 +52,7 @@ extension FavoriteListViewController { var folderCellRegistration: RowCellRegistration { RowCellRegistration { [weak self] cell, _, folder in var content = cell.defaultContentConfiguration() - let iconName = folder.isPinned ? "ic_custom_folder_pin" : folder.iconName - content.image = UIImage.templateImageNamed(iconName)?.resizedTemplateImage(with: FavoriteListViewController.imageSize) + 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 @@ -106,7 +105,7 @@ extension FavoriteListViewController { guard let self else { return } cell.button.removeTarget(nil, action: nil, for: .touchUpInside) if self.isSearchResultsMode { - cell.configure(image: UIImage.templateImageNamed("ic_custom_search") ?? .icCustomFavorites, + 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) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift index 0b6c1947e8..6b5d21ee2e 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift @@ -335,8 +335,7 @@ extension FavoriteListViewController { private func isDirectFolder(_ groupName: String, parentGroupName: String?) -> Bool { guard let parentGroupName else { return groupName.isEmpty || !groupName.contains("/") } - guard !parentGroupName.isEmpty else { return false } - guard groupName.hasPrefix(parentGroupName + "/") else { return false } + guard !parentGroupName.isEmpty && groupName.hasPrefix(parentGroupName + "/") else { return false } let childPath = groupName.dropFirst(parentGroupName.count + 1) return !childPath.isEmpty && !childPath.contains("/") } 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..b40772d08a --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.h @@ -0,0 +1,34 @@ +// +// 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; + +@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..f7253a7546 --- /dev/null +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.mm @@ -0,0 +1,71 @@ +// +// 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 + +- (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]; + } + + return self; +} + ++ (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/OAFavoritesSwiftHelper.h b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.h index a2d408e8a1..295cb57465 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.h +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.h @@ -10,43 +10,7 @@ NS_ASSUME_NONNULL_BEGIN -@class UIColor, UIImage, OAFavoriteItem, OASGpxUtilitiesPointsGroup, OAEditPointViewController; - -@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; - -@end - -@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; - -@end +@class UIColor, UIImage, OAFavoriteItem, OASGpxUtilitiesPointsGroup, OAEditPointViewController, OAFavoriteFolderBridgeItem, OAFavoritePointBridgeItem; @interface OAFavoritesSwiftHelper : NSObject diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.mm index f9feec6235..5b47572c61 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.mm +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.mm @@ -34,98 +34,11 @@ #import "OsmAndApp.h" #import "OsmAndSharedWrapper.h" #import -#import "OAObservable.h" +#import "OAFavoriteFolderBridgeItem.h" +#import "OAFavoritePointBridgeItem.h" #include -@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 - -@implementation OAFavoritePointBridgeItem - -- (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]; - } - - return self; -} - -+ (nullable 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 - @implementation OAFavoritesSwiftHelper + (NSArray *)favoriteFolders diff --git a/Sources/OsmAnd Maps-Bridging-Header.h b/Sources/OsmAnd Maps-Bridging-Header.h index 945f1613b4..623cd3ecea 100644 --- a/Sources/OsmAnd Maps-Bridging-Header.h +++ b/Sources/OsmAnd Maps-Bridging-Header.h @@ -116,6 +116,8 @@ #import "OAOpenStreetMapPoint.h" #import "OAOsmNotePoint.h" #import "OAShareMenuActivity.h" +#import "OAFavoriteFolderBridgeItem.h" +#import "OAFavoritePointBridgeItem.h" // Widgets #import "OAMapWidgetRegistry.h" From 8f667d749271492357789ce1f8a35dbc4f8de42c Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Mon, 15 Jun 2026 17:59:15 +0300 Subject: [PATCH 32/41] two line name and selection mode context menu fixed --- .../FavoriteListViewController+Cells.swift | 1 + .../FavoriteListViewController+ContextMenu.swift | 6 +++--- .../FavoriteListViewController+Models.swift | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift index 5c54e09ff8..5e6606d8a0 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift @@ -57,6 +57,7 @@ extension FavoriteListViewController { 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 diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift index 3b98452a69..7072ecaf53 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift @@ -55,7 +55,7 @@ extension FavoriteListViewController { let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { _ in OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: hasDirectFavoritePoints ? folderFavoriteItem : subtreeFavoriteItems) } - let trackAction = UIAction(title: localizedString("add_to_a_track"), image: .icCustomTrip) { [weak self] _ in + let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: .icCustomTrip) { [weak self] _ in guard let self else { return } if hasDirectFavoritePoints { self.openFavoriteGroupAddToTrack(folder.bridgeItem.groupName) @@ -69,7 +69,7 @@ extension FavoriteListViewController { OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: hasDirectFavoritePoints ? directFavoriteItems : subtreeFavoriteItems) } let addToActions: [UIMenuElement] = hasFavoritePoints ? [mapMarkersAction, trackAction, navigationAction] : [] - let fourthButtons: [UIMenuElement] = addToActions.isEmpty ? [] : [UIMenu(title: localizedString("shared_string_add"), image: .icCustomAdd, children: addToActions)] + 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 @@ -149,7 +149,7 @@ extension FavoriteListViewController { OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: selectedBridgeItems) self?.applySnapshot(animatingDifferences: true) } - let addToMenu = UIMenu(title: localizedString("add_to"), image: .icCustomAdd, children: [trackAction, navigationAction, mapMarkersAction]) + let addToMenu = UIMenu(title: localizedString("add_to"), image: .icCustomAdd, children: [navigationAction, trackAction, mapMarkersAction]) let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: [addToMenu]) menuElements.append(thirdButtonsSection) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift index 6fb05a90c6..b659259c94 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift @@ -140,10 +140,10 @@ struct FavoriteFolderStats: Hashable { 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 = Self.rowHeight + attributes.frame.size.height = max(Self.rowHeight, attributes.frame.height) return attributes } } From 12210e8b468790af718ca2b740a4848007c75218 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Tue, 16 Jun 2026 18:24:53 +0300 Subject: [PATCH 33/41] fixed comments --- OsmAnd.xcodeproj/project.pbxproj | 12 ++-- .../FavoriteListViewController+Actions.swift | 69 +++++++++---------- .../FavoriteListViewController+Cells.swift | 3 - ...voriteListViewController+ContextMenu.swift | 35 +++++----- ...avoriteListViewController+DataSource.swift | 42 +++++------ ...FavoriteListViewController+Delegates.swift | 25 +++---- .../FavoriteListViewController+Models.swift | 7 +- ...wiftHelper.h => OAFavoritesBridgeHelper.h} | 4 +- ...ftHelper.mm => OAFavoritesBridgeHelper.mm} | 6 +- Sources/OsmAnd Maps-Bridging-Header.h | 2 +- 10 files changed, 91 insertions(+), 114 deletions(-) rename Sources/Controllers/MyPlaces/FavoriteListViewController/{OAFavoritesSwiftHelper.h => OAFavoritesBridgeHelper.h} (97%) rename Sources/Controllers/MyPlaces/FavoriteListViewController/{OAFavoritesSwiftHelper.mm => OAFavoritesBridgeHelper.mm} (99%) diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index 4a41e4eafb..2cff1d7d0a 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -219,7 +219,7 @@ 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 /* OAFavoritesSwiftHelper.mm in Sources */ = {isa = PBXBuildFile; fileRef = 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */; }; + 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 */; }; @@ -3744,8 +3744,8 @@ 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 /* OAFavoritesSwiftHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = OAFavoritesSwiftHelper.h; sourceTree = ""; }; - 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = OAFavoritesSwiftHelper.mm; 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 = ""; }; @@ -8408,8 +8408,8 @@ C5EB56F82FD18B8A00D01657 /* FavoriteListViewController+Actions.swift */, C5EB56FA2FD18B8A00D01657 /* FavoriteListViewController+Delegates.swift */, C58ACDE02FD2D6E8004E34C9 /* FavoriteSortModeHelper.swift */, - 272AFB972FD2DC42006C2E21 /* OAFavoritesSwiftHelper.h */, - 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm */, + 272AFB972FD2DC42006C2E21 /* OAFavoritesBridgeHelper.h */, + 272AFB9A2FD2DD3C006C2E21 /* OAFavoritesBridgeHelper.mm */, 275ED6592FE032290088D42B /* OAFavoriteFolderBridgeItem.h */, 275ED65A2FE032600088D42B /* OAFavoriteFolderBridgeItem.mm */, 275ED65C2FE032A90088D42B /* OAFavoritePointBridgeItem.h */, @@ -17712,7 +17712,7 @@ DA5A814D26C563A700F274C7 /* OASearchCategoriesListController.mm in Sources */, DA5A81F726C563A700F274C7 /* OAChoosePlanViewController.mm in Sources */, FAA6505A2ADD42C50020DCEA /* DeviceFactory.swift in Sources */, - 272AFB9B2FD2DD3C006C2E21 /* OAFavoritesSwiftHelper.mm in Sources */, + 272AFB9B2FD2DD3C006C2E21 /* OAFavoritesBridgeHelper.mm in Sources */, DA5A84F126C563A900F274C7 /* OAMapSource.m in Sources */, FA282AF62C2C456700CC7AC1 /* WeatherNavigationBarView.swift in Sources */, DA5A825526C563A700F274C7 /* OAArrivalAnnouncementViewController.m in Sources */, diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift index 4cb6b7bda8..670e4cd28b 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift @@ -18,7 +18,7 @@ extension FavoriteListViewController { } func openFavoriteGroupAppearance(_ groupName: String) { - guard let viewController = OAFavoriteGroupEditorViewController(group: OAFavoritesSwiftHelper.pointsGroup(forGroupName: groupName)) else { return } + guard let viewController = OAFavoriteGroupEditorViewController(group: OAFavoritesBridgeHelper.pointsGroup(forGroupName: groupName)) else { return } favoriteGroupAppearanceGroupName = groupName favoriteGroupAppearanceEditor = viewController viewController.delegate = self @@ -28,23 +28,21 @@ extension FavoriteListViewController { func openFavoriteItemsMove(_ favoriteItems: [Any]) { guard !favoriteItems.isEmpty, let navigationController, - let groupController = OAEditGroupViewController(groupName: nil, groups: OAFavoritesSwiftHelper.favoriteGroupNames(forMovingFavoriteItems: favoriteItems)) else { + let groupController = OAEditGroupViewController(groupName: nil, groups: OAFavoritesBridgeHelper.favoriteGroupNames(forMovingFavoriteItems: favoriteItems)) else { return } self.groupController = groupController favoriteItemsToMove = favoriteItems groupController.delegate = self - let modalNavigationController = UINavigationController(rootViewController: groupController) - navigationController.present(modalNavigationController, animated: true) + navigationController.present(UINavigationController(rootViewController: groupController), animated: true) } func openFavoriteGroupAddToTrack(_ groupName: String) { - guard OAFavoritesSwiftHelper.canUseGroup(withName: groupName), let navigationController, let viewController = OAOpenAddTrackViewController(screenType: .addToATrack) else { return } + guard OAFavoritesBridgeHelper.canUseGroup(withName: groupName), let navigationController, let viewController = OAOpenAddTrackViewController(screenType: .addToATrack) else { return } addToTrackGroupName = groupName addToTrackFavoriteItems = nil viewController.delegate = self - let modalNavigationController = UINavigationController(rootViewController: viewController) - navigationController.present(modalNavigationController, animated: true) + navigationController.present(UINavigationController(rootViewController: viewController), animated: true) } func openFavoriteItemsAddToTrack(_ favoriteItems: [Any]) { @@ -52,18 +50,17 @@ extension FavoriteListViewController { addToTrackFavoriteItems = favoriteItems addToTrackGroupName = nil viewController.delegate = self - let modalNavigationController = UINavigationController(rootViewController: viewController) - navigationController.present(modalNavigationController, animated: true) + navigationController.present(UINavigationController(rootViewController: viewController), animated: true) } func favoritePointRows(forGroupName groupName: String) -> [FavoritePointRow] { let sortMode = isSearchResultsMode ? searchFavoriteSortMode() : favoriteSortMode(entryId: groupName) - let favorites = OAFavoritesSwiftHelper.favoritePoints(forGroupName: groupName).map { FavoritePointRow(item: $0) } + 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 { OAFavoritesSwiftHelper.favoritePoints(forGroupName: $0.bridgeItem.groupName).map { FavoritePointRow(item: $0) } } + allFolders.filter { isSearchGroup($0.bridgeItem.groupName, parentGroupName: parentGroupName) }.flatMap { OAFavoritesBridgeHelper.favoritePoints(forGroupName: $0.bridgeItem.groupName).map { FavoritePointRow(item: $0) } } } func makeActionsMenu() -> UIMenu { @@ -79,20 +76,20 @@ extension FavoriteListViewController { return UIMenu(title: "", children: [addFolderSection, importSection]) } - func setEdit(_ isEdit: Bool) { - let shouldResetSearchSelection = !isEdit && isSelectionModeInSearch - if !isEdit { + func setEditing(_ isEditing: Bool) { + let shouldResetSearchSelection = !isEditing && isSelectionModeInSearch + if !isEditing { collectionView.indexPathsForSelectedItems?.forEach { collectionView.deselectItem(at: $0, animated: false) } isSelectionModeInSearch = false isSearchActive = false searchText = "" } - collectionView.isEditing = isEdit + collectionView.isEditing = isEditing collectionView.reloadData() - myPlacesDelegate?.updateEditMode(isEdit) + myPlacesDelegate?.updateEditMode(isEditing) configureNavigation() - navigationController?.setToolbarHidden(!isEdit, animated: true) + navigationController?.setToolbarHidden(!isEditing, animated: true) if shouldResetSearchSelection { clearSearchControllerText() applySnapshot(animatingDifferences: false) @@ -102,12 +99,12 @@ extension FavoriteListViewController { 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 text = alert?.textFields?.first?.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty else { return } + 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) ?? text - OAFavoritesSwiftHelper.renameFavoriteGroup(oldGroupName, newName: newGroupName) - self?.renameFavoriteSortModeKeys(from: oldGroupName, to: newGroupName) - self?.applySnapshot(animatingDifferences: true) + 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) @@ -125,7 +122,7 @@ extension FavoriteListViewController { 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 OAFavoritesSwiftHelper.deleteFavoriteGroup(folder.bridgeItem.groupName) else { return } + guard OAFavoritesBridgeHelper.deleteFavoriteGroup(folder.bridgeItem.groupName) else { return } self?.clearFavoriteSortModes(forGroupNames: [folder.bridgeItem.groupName]) self?.applySnapshot(animatingDifferences: true) }) @@ -138,7 +135,7 @@ extension FavoriteListViewController { 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 OAFavoritesSwiftHelper.deleteFavoritePoint(favorite.bridgeItem) else { return } + guard OAFavoritesBridgeHelper.deleteFavoritePoint(favorite.bridgeItem) else { return } self?.applySnapshot(animatingDifferences: true) }) @@ -199,7 +196,7 @@ extension FavoriteListViewController { } @objc func selectButtonPressed() { - setEdit(true) + setEditing(true) } @objc func searchSelectButtonPressed() { @@ -216,7 +213,7 @@ extension FavoriteListViewController { } @objc func cancelButtonPressed() { - setEdit(false) + setEditing(false) configureToolbar() } @@ -244,7 +241,7 @@ extension FavoriteListViewController { @objc func shareButtonClicked(_ sender: Any) { let sourceView = sender as? UIView ?? collectionView shareItems(for: sourceView) - setEdit(false) + setEditing(false) applySnapshot() } @@ -360,7 +357,7 @@ extension FavoriteListViewController { appendFavoritePointShareLine(point.displayGroupName, to: sharingText) appendFavoritePointShareLine(point.itemDescription, to: sharingText) appendFavoritePointCoordinatesAndURL(to: sharingText, point: point) - if let url = URL(string: OAFavoritesSwiftHelper.sharePoiURLString(forFavoritePoint: 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 { @@ -378,16 +375,14 @@ extension FavoriteListViewController { } private func appendFavoritePointCoordinatesAndURL(to sharingText: NSMutableString, point: OAFavoritePointBridgeItem) { - let geoURLString = OAFavoritesSwiftHelper.geoURLString(forFavoritePoint: point) + let geoURLString = OAFavoritesBridgeHelper.geoURLString(forFavoritePoint: point) if !geoURLString.isEmpty { - sharingText.append("\n") - sharingText.append("Location: \(geoURLString)") + sharingText.append("\nLocation: \(geoURLString)") } - let shareURLString = OAFavoritesSwiftHelper.sharePoiURLString(forFavoritePoint: point) + let shareURLString = OAFavoritesBridgeHelper.sharePoiURLString(forFavoritePoint: point) if !shareURLString.isEmpty { - sharingText.append("\n") - sharingText.append(shareURLString) + sharingText.append("\n\(shareURLString)") } } @@ -419,7 +414,7 @@ extension FavoriteListViewController { return } - guard let favoritesUrl = OAFavoritesSwiftHelper.shareFavoriteItems(bridgeItems(for: selectedItems)) else { return } + guard let favoritesUrl = OAFavoritesBridgeHelper.shareFavoriteItems(bridgeItems(for: selectedItems)) else { return } showActivity( [favoritesUrl], sourceView: sourceView, @@ -434,11 +429,11 @@ extension FavoriteListViewController { let selectedIndexPaths = collectionView.indexPathsForSelectedItems ?? [] let items = bridgeItems(for: selectedIndexPaths) let groupNames = items.compactMap { ($0 as? OAFavoriteFolderBridgeItem)?.groupName } - if OAFavoritesSwiftHelper.deleteFavoriteItems(items) { + if OAFavoritesBridgeHelper.deleteFavoriteItems(items) { clearFavoriteSortModes(forGroupNames: groupNames) } - setEdit(false) + setEditing(false) applySnapshot(animatingDifferences: true) } diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift index 5e6606d8a0..c964f915d8 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift @@ -6,9 +6,6 @@ // Copyright © 2026 OsmAnd. All rights reserved. // -import CoreLocation -import UniformTypeIdentifiers - extension FavoriteListViewController { var headerCellRegistration: CellRegistration { CellRegistration { cell, _, section in diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift index 7072ecaf53..bcd11c06dd 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift @@ -6,9 +6,6 @@ // Copyright © 2026 OsmAnd. All rights reserved. // -import CoreLocation -import UniformTypeIdentifiers - extension FavoriteListViewController { func makeFolderContextMenu(for folder: FavoriteFolderRow, indexPath: IndexPath) -> UIMenu { let folderFavoriteItem: [Any] = [folder.bridgeItem] @@ -17,12 +14,12 @@ extension FavoriteListViewController { let hasDirectFavoritePoints = folder.bridgeItem.pointsCount > 0 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 } - OAFavoritesSwiftHelper.setFavoriteGroupVisible(folder.bridgeItem.groupName, visible: !folder.isVisible) + 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 } - OAFavoritesSwiftHelper.setFavoriteGroupPinned(folder.bridgeItem.groupName, pinned: !folder.isPinned) + OAFavoritesBridgeHelper.setFavoriteGroupPinned(folder.bridgeItem.groupName, pinned: !folder.isPinned) self.applySnapshot(animatingDifferences: true) } let firstButtonsSection = UIMenu(title: "", options: .displayInline, children: [showHideAction, pinAction]) @@ -40,7 +37,7 @@ extension FavoriteListViewController { 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 = OAFavoritesSwiftHelper.shareFavoriteItems([folder.bridgeItem]) else { return } + guard let favoritesUrl = OAFavoritesBridgeHelper.shareFavoriteItems([folder.bridgeItem]) else { return } showActivity([favoritesUrl], sourceView: sourceView, barButtonItem: nil, completionWithItemsHandler: { try? FileManager.default.removeItem(at: favoritesUrl) }) @@ -53,7 +50,7 @@ extension FavoriteListViewController { let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: thirdButtons) let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { _ in - OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: hasDirectFavoritePoints ? folderFavoriteItem : subtreeFavoriteItems) + OAFavoritesBridgeHelper.addFavoriteItems(toMapMarkers: hasDirectFavoritePoints ? folderFavoriteItem : subtreeFavoriteItems) } let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: .icCustomTrip) { [weak self] _ in guard let self else { return } @@ -66,7 +63,7 @@ extension FavoriteListViewController { let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in guard let self else { return } let directFavoriteItems: [Any] = self.favoritePointRows(forGroupName: folder.bridgeItem.groupName).map { $0.bridgeItem } - OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: hasDirectFavoritePoints ? directFavoriteItems : subtreeFavoriteItems) + OAFavoritesBridgeHelper.addFavoriteItems(toNavigation: hasDirectFavoritePoints ? directFavoriteItems : subtreeFavoriteItems) } let addToActions: [UIMenuElement] = hasFavoritePoints ? [mapMarkersAction, trackAction, navigationAction] : [] let fourthButtons: [UIMenuElement] = addToActions.isEmpty ? [] : [UIMenu(title: localizedString("add_to"), image: .icCustomAdd, children: addToActions)] @@ -83,7 +80,7 @@ extension FavoriteListViewController { 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 = OAFavoritesSwiftHelper.editPointViewController(forFavoritePoint: point.bridgeItem) else { return } + 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) @@ -105,14 +102,14 @@ extension FavoriteListViewController { let secondButtonsSection = UIMenu(title: "", options: .displayInline, children: [moveAction, shareAction]) let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { _ in - OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: [point.bridgeItem]) + 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 - OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: [point.bridgeItem]) + 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]) @@ -136,17 +133,17 @@ extension FavoriteListViewController { } let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { [weak self] _ in - OAFavoritesSwiftHelper.addFavoriteItems(toMapMarkers: selectedBridgeItems) - self?.setEdit(false) + 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?.setEdit(false) + self?.setEditing(false) self?.applySnapshot(animatingDifferences: true) } let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in - OAFavoritesSwiftHelper.addFavoriteItems(toNavigation: selectedBridgeItems) + OAFavoritesBridgeHelper.addFavoriteItems(toNavigation: selectedBridgeItems) self?.applySnapshot(animatingDifferences: true) } let addToMenu = UIMenu(title: localizedString("add_to"), image: .icCustomAdd, children: [navigationAction, trackAction, mapMarkersAction]) @@ -171,7 +168,7 @@ extension FavoriteListViewController { 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 - OAFavoritesSwiftHelper.setFavoriteGroupsPinned(unpinnedGroupNames, pinned: true) + OAFavoritesBridgeHelper.setFavoriteGroupsPinned(unpinnedGroupNames, pinned: true) self?.applySnapshot(animatingDifferences: true) } folderMenuElements.append(pinAction) @@ -180,7 +177,7 @@ extension FavoriteListViewController { 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 - OAFavoritesSwiftHelper.setFavoriteGroupsPinned(pinnedGroupNames, pinned: false) + OAFavoritesBridgeHelper.setFavoriteGroupsPinned(pinnedGroupNames, pinned: false) self?.applySnapshot(animatingDifferences: true) } folderMenuElements.append(unpinAction) @@ -189,7 +186,7 @@ extension FavoriteListViewController { 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 - OAFavoritesSwiftHelper.setFavoriteGroupsVisible(visibleGroupNames, visible: false) + OAFavoritesBridgeHelper.setFavoriteGroupsVisible(visibleGroupNames, visible: false) self?.applySnapshot(animatingDifferences: true) } folderMenuElements.append(hideAction) @@ -198,7 +195,7 @@ extension FavoriteListViewController { 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 - OAFavoritesSwiftHelper.setFavoriteGroupsVisible(hiddenGroupNames, visible: true) + OAFavoritesBridgeHelper.setFavoriteGroupsVisible(hiddenGroupNames, visible: true) self?.applySnapshot(animatingDifferences: true) } folderMenuElements.append(showAction) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift index 6b5d21ee2e..02f0a174b6 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift @@ -6,9 +6,6 @@ // Copyright © 2026 OsmAnd. All rights reserved. // -import CoreLocation -import UniformTypeIdentifiers - extension FavoriteListViewController { func favoriteSortMode(entryId: String? = nil) -> FavoriteSortMode { let sortModes = settings.getFavoriteSortModes() @@ -36,7 +33,7 @@ extension FavoriteListViewController { func renameFavoriteSortModeKeys(from oldGroupName: String, to newGroupName: String, existingGroupNames: Set? = nil) { guard !oldGroupName.isEmpty, oldGroupName != newGroupName else { return } - let groupNames = existingGroupNames ?? Set(OAFavoritesSwiftHelper.favoriteFolders().map { $0.groupName }) + 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) } @@ -51,7 +48,7 @@ extension FavoriteListViewController { } func updateFavoriteSortModeKeysAfterMove(_ favoriteItems: [Any], toGroupName targetGroupName: String) { - let groupNames = Set(OAFavoritesSwiftHelper.favoriteFolders().map { $0.groupName }) + 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 @@ -63,11 +60,11 @@ extension FavoriteListViewController { 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(OAFavoritesSwiftHelper.favoriteFolders().map { $0.groupName }) + 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), OAFavoritesSwiftHelper.addFavoriteGroup(folderName, parentGroupName: parentGroupName.isEmpty ? nil : parentGroupName, iconName: nil, color: nil, backgroundIconName: nil) { + if !existingGroupNames.contains(newGroupName), OAFavoritesBridgeHelper.addFavoriteGroup(folderName, parentGroupName: parentGroupName.isEmpty ? nil : parentGroupName, iconName: nil, color: nil, backgroundIconName: nil) { existingGroupNames.insert(newGroupName) } parentGroupName = newGroupName @@ -137,7 +134,7 @@ extension FavoriteListViewController { } func favoriteFolders() -> [FavoriteFolderRow] { - OAFavoritesSwiftHelper.favoriteFolders().map { FavoriteFolderRow(item: $0) } + OAFavoritesBridgeHelper.favoriteFolders().map { FavoriteFolderRow(item: $0) } } func isNestedFolder(_ groupName: String, in parentGroupName: String) -> Bool { @@ -192,11 +189,7 @@ extension FavoriteListViewController { var snapshot = Snapshot() if allFolders.isEmpty { - layoutSections = [] - collectionView.collectionViewLayout.invalidateLayout() - snapshot.appendSections([.emptyState]) - snapshot.appendItems([.emptyState]) - dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + applyEmptyStateSnapshot(animatingDifferences: animatingDifferences) return } @@ -246,14 +239,10 @@ extension FavoriteListViewController { } let folders = FavoriteSortModeHelper.sortFoldersWithMode(directFavoriteFolders(allFolders, parentGroupName: folder.bridgeItem.groupName).filter { matchesSearch($0.title) }, mode: currentSortMode) - let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(OAFavoritesSwiftHelper.favoritePoints(forGroupName: folder.bridgeItem.groupName).map { FavoritePointRow(item: $0) }.filter { matchesSearch($0.title) || matchesSearch($0.bridgeItem.address) }, 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) var snapshot = Snapshot() if favorites.isEmpty && folders.isEmpty { - layoutSections = [] - collectionView.collectionViewLayout.invalidateLayout() - snapshot.appendSections([.emptyState]) - snapshot.appendItems([.emptyState]) - dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + applyEmptyStateSnapshot(animatingDifferences: animatingDifferences) return } let stats = folderStats(allFolders: allFolders, currentGroupName: folder.bridgeItem.groupName) @@ -274,11 +263,7 @@ extension FavoriteListViewController { let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(searchFavoritePointRows(allFolders: allFolders, parentGroupName: parentGroupName), mode: currentSortMode) var snapshot = Snapshot() if favorites.isEmpty { - layoutSections = [] - collectionView.collectionViewLayout.invalidateLayout() - snapshot.appendSections([.emptyState]) - snapshot.appendItems([.emptyState]) - dataSource.apply(snapshot, animatingDifferences: animatingDifferences) + applyEmptyStateSnapshot(animatingDifferences: animatingDifferences) return } @@ -289,6 +274,15 @@ extension FavoriteListViewController { 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) } diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift index 32284d1aec..90a3fe7392 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift @@ -6,9 +6,6 @@ // Copyright © 2026 OsmAnd. All rights reserved. // -import CoreLocation -import UniformTypeIdentifiers - extension FavoriteListViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { guard let item = dataSource.itemIdentifier(for: indexPath) else { return } @@ -26,7 +23,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { updateSelectionUI() return } - OAFavoritesSwiftHelper.openFavoritePoint(withIdentifier: favorite.bridgeItem.identifier) + OAFavoritesBridgeHelper.openFavoritePoint(withIdentifier: favorite.bridgeItem.identifier) default: break } @@ -78,7 +75,7 @@ extension FavoriteListViewController: OAShareMenuDelegate { guard let pointToShare else { return } switch type { case .clipboard: - copyFavoritePointShareText(OAFavoritesSwiftHelper.sharePoiURLString(forFavoritePoint: pointToShare)) + copyFavoritePointShareText(OAFavoritesBridgeHelper.sharePoiURLString(forFavoritePoint: pointToShare)) case .copyAddress: if let address = pointToShare.address, !address.isEmpty { copyFavoritePointShareText(address) @@ -92,9 +89,9 @@ extension FavoriteListViewController: OAShareMenuDelegate { OAUtilities.showToast(localizedString("toast_empty_name_error"), details: nil, duration: 4, in: view) } case .copyCoordinates: - copyFavoritePointShareText(OAFavoritesSwiftHelper.formattedCoordinates(forFavoritePoint: pointToShare)) + copyFavoritePointShareText(OAFavoritesBridgeHelper.formattedCoordinates(forFavoritePoint: pointToShare)) case .geo: - copyFavoritePointShareText(OAFavoritesSwiftHelper.geoURLString(forFavoritePoint: pointToShare)) + copyFavoritePointShareText(OAFavoritesBridgeHelper.geoURLString(forFavoritePoint: pointToShare)) default: break } @@ -143,10 +140,10 @@ extension FavoriteListViewController: OAEditColorViewControllerDelegate { guard let selectedItems = collectionView.indexPathsForSelectedItems, !selectedItems.isEmpty else { return } if colorController.saveChanges { - OAFavoritesSwiftHelper.changeFavoriteItems(bridgeItems(for: selectedItems), colorIndex: colorController.colorIndex) + OAFavoritesBridgeHelper.changeFavoriteItems(bridgeItems(for: selectedItems), colorIndex: colorController.colorIndex) } - setEdit(false) + setEditing(false) applySnapshot(animatingDifferences: true) } } @@ -164,9 +161,9 @@ extension FavoriteListViewController: OAEditGroupViewControllerDelegate { let targetGroupName = groupController.groupName ?? "" guard let favoriteItemsToMove else { return } createFavoriteMoveTargetGroupIfNeeded(targetGroupName, favoriteItems: favoriteItemsToMove) - OAFavoritesSwiftHelper.moveFavoriteItems(favoriteItemsToMove, toGroupName: targetGroupName) + OAFavoritesBridgeHelper.moveFavoriteItems(favoriteItemsToMove, toGroupName: targetGroupName) updateFavoriteSortModeKeysAfterMove(favoriteItemsToMove, toGroupName: targetGroupName) - setEdit(false) + setEditing(false) applySnapshot(animatingDifferences: true) } } @@ -174,10 +171,10 @@ extension FavoriteListViewController: OAEditGroupViewControllerDelegate { extension FavoriteListViewController: OAOpenAddTrackDelegate { func onFileSelected(_ gpxFilePath: String) { if let addToTrackFavoriteItems { - OAFavoritesSwiftHelper.addFavoriteItems(toTrack: addToTrackFavoriteItems, gpxFileName: gpxFilePath) + OAFavoritesBridgeHelper.addFavoriteItems(toTrack: addToTrackFavoriteItems, gpxFileName: gpxFilePath) self.addToTrackFavoriteItems = nil } else if let addToTrackGroupName { - OAFavoritesSwiftHelper.addFavoriteGroup(toTrack: addToTrackGroupName, gpxFileName: gpxFilePath) + OAFavoritesBridgeHelper.addFavoriteGroup(toTrack: addToTrackGroupName, gpxFileName: gpxFilePath) self.addToTrackGroupName = nil } } @@ -185,7 +182,7 @@ extension FavoriteListViewController: OAOpenAddTrackDelegate { extension FavoriteListViewController: OAEditorDelegate { func addNewItem(withName name: String?, iconName: String, color: UIColor, backgroundIconName: String) { - guard OAFavoritesSwiftHelper.addFavoriteGroup(name ?? "", + guard OAFavoritesBridgeHelper.addFavoriteGroup(name ?? "", parentGroupName: parentGroupName, iconName: iconName, color: color, diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift index b659259c94..69995d0f9b 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift @@ -6,9 +6,6 @@ // Copyright © 2026 OsmAnd. All rights reserved. // -import CoreLocation -import UniformTypeIdentifiers - enum ScreenMode { case root case folder(FavoriteFolderRow, previousTitle: String) @@ -53,9 +50,9 @@ struct FavoriteSortHeader: Hashable { } struct FavoriteFolderRow: Hashable, FavoriteSortableFolder { - static let subtitleDateFormatter: DateFormatter = { + private static let subtitleDateFormatter: DateFormatter = { let formatter = DateFormatter() - formatter.dateFormat = "d MMM" + formatter.setLocalizedDateFormatFromTemplate("dMMM") return formatter }() diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.h b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.h similarity index 97% rename from Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.h rename to Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.h index 295cb57465..db2fcf6ed8 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.h +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.h @@ -1,5 +1,5 @@ // -// OAFavoritesSwiftHelper.h +// OAFavoritesBridgeHelper.h // OsmAnd // // Created by Vladyslav Lysenko on 05.06.2026. @@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN @class UIColor, UIImage, OAFavoriteItem, OASGpxUtilitiesPointsGroup, OAEditPointViewController, OAFavoriteFolderBridgeItem, OAFavoritePointBridgeItem; -@interface OAFavoritesSwiftHelper : NSObject +@interface OAFavoritesBridgeHelper : NSObject + (NSArray *)favoriteFolders; + (NSArray *)favoritePointsForGroupName:(NSString *)groupName; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.mm b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm similarity index 99% rename from Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.mm rename to Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm index 5b47572c61..6a847dbb67 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesSwiftHelper.mm +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm @@ -1,12 +1,12 @@ // -// OAFavoritesSwiftHelper.mm +// OAFavoritesBridgeHelper.mm // OsmAnd Maps // // Created by Vladyslav Lysenko on 05.06.2026. // Copyright © 2026 OsmAnd. All rights reserved. // -#import "OAFavoritesSwiftHelper.h" +#import "OAFavoritesBridgeHelper.h" #import "OAAppSettings.h" #import "OAEditGroupViewController.h" #import "OAEditPointViewController.h" @@ -39,7 +39,7 @@ #include -@implementation OAFavoritesSwiftHelper +@implementation OAFavoritesBridgeHelper + (NSArray *)favoriteFolders { diff --git a/Sources/OsmAnd Maps-Bridging-Header.h b/Sources/OsmAnd Maps-Bridging-Header.h index 623cd3ecea..465d46e9a1 100644 --- a/Sources/OsmAnd Maps-Bridging-Header.h +++ b/Sources/OsmAnd Maps-Bridging-Header.h @@ -65,7 +65,7 @@ #import "OAResourcesUISwiftHelper.h" #import "OAEditGroupViewController.h" #import "OAFavoriteGroupEditorViewController.h" -#import "OAFavoritesSwiftHelper.h" +#import "OAFavoritesBridgeHelper.h" #import "OAOpenAddTrackViewController.h" #import "OAEditColorViewController.h" #import "OAResourcesInstaller.h" From 3ea189250b35d303a4844cda780fc6d5affb0825 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Tue, 16 Jun 2026 18:27:03 +0300 Subject: [PATCH 34/41] refactored --- .../FavoriteListViewController+DataSource.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift index 02f0a174b6..f291c1de92 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift @@ -187,12 +187,12 @@ extension FavoriteListViewController { return } - var snapshot = Snapshot() 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 @@ -240,11 +240,11 @@ extension FavoriteListViewController { 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) - var snapshot = Snapshot() 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() @@ -261,12 +261,12 @@ extension FavoriteListViewController { private func applySearchSnapshot(allFolders: [FavoriteFolderRow], parentGroupName: String?, animatingDifferences: Bool) { let favorites = FavoriteSortModeHelper.sortFavoritePointsWithMode(searchFavoritePointRows(allFolders: allFolders, parentGroupName: parentGroupName), mode: currentSortMode) - var snapshot = Snapshot() if favorites.isEmpty { applyEmptyStateSnapshot(animatingDifferences: animatingDifferences) return } + var snapshot = Snapshot() layoutSections = [.sortHeader, .content] collectionView.collectionViewLayout.invalidateLayout() snapshot.appendSections(layoutSections) From bff4ae1cd8e6ad09adf8ba72eb59634b690f6b86 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Wed, 17 Jun 2026 14:13:23 +0300 Subject: [PATCH 35/41] refactored --- .../FavoriteListViewController+Actions.swift | 17 +++++++++-------- .../FavoriteListViewController+Delegates.swift | 3 +++ .../FavoriteListViewController+Models.swift | 16 ++++++++++++++++ .../FavoriteListViewController.swift | 8 +++++++- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift index 670e4cd28b..5047cb995a 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift @@ -10,13 +10,6 @@ import CoreLocation import UniformTypeIdentifiers extension FavoriteListViewController { - func areAllSelectableItemsSelected() -> Bool { - let selectableIndexPaths = selectableIndexPaths() - guard !selectableIndexPaths.isEmpty else { return false } - let selectedIndexPaths = Set(collectionView.indexPathsForSelectedItems ?? []) - return selectableIndexPaths.allSatisfy { selectedIndexPaths.contains($0) } - } - func openFavoriteGroupAppearance(_ groupName: String) { guard let viewController = OAFavoriteGroupEditorViewController(group: OAFavoritesBridgeHelper.pointsGroup(forGroupName: groupName)) else { return } favoriteGroupAppearanceGroupName = groupName @@ -83,6 +76,7 @@ extension FavoriteListViewController { isSelectionModeInSearch = false isSearchActive = false searchText = "" + selectionManager.deselectAll() } collectionView.isEditing = isEditing @@ -194,6 +188,11 @@ extension FavoriteListViewController { 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) @@ -219,10 +218,12 @@ extension FavoriteListViewController { @objc func selectAllButtonPressed() { let selectableIndexPaths = selectableIndexPaths() - if areAllSelectableItemsSelected() { + 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() diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift index 90a3fe7392..78ee275b3b 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift @@ -12,6 +12,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { switch item { case .folder(let folder): if collectionView.isEditing { + updateSelection(at: indexPath) updateSelectionUI() return } @@ -20,6 +21,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { navigationController?.pushViewController(viewController, animated: true) case .favorite(let favorite): if collectionView.isEditing { + updateSelection(at: indexPath) updateSelectionUI() return } @@ -33,6 +35,7 @@ extension FavoriteListViewController: UICollectionViewDelegate { func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) { guard collectionView.isEditing else { return } + updateSelection(at: indexPath) updateSelectionUI() } diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift index 69995d0f9b..335c5f22be 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Models.swift @@ -42,6 +42,22 @@ enum FavoriteListItem: Hashable { 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 { diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift index 94e1851bcc..ac7ede70eb 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift @@ -96,6 +96,12 @@ final class FavoriteListViewController: UIViewController { 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? @@ -309,7 +315,7 @@ final class FavoriteListViewController: UIViewController { 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(areAllSelectableItemsSelected() ? "shared_string_deselect_all" : "shared_string_select_all") + 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 From f13c435ae965479c581785be59d1cc4742413690 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Wed, 17 Jun 2026 17:30:40 +0300 Subject: [PATCH 36/41] refactored --- .../FavoriteListViewController+Cells.swift | 16 +--------------- Sources/Helpers/Extensions/Extensions.swift | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift index c964f915d8..c9f3fc87b2 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift @@ -173,7 +173,7 @@ extension FavoriteListViewController { 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 = rotatedFavoriteDirectionIcon(directionIcon, radians: favorite.bridgeItem.direction) + let rotatedDirectionIcon = directionIcon.rotatedForAttributedString(with: favorite.bridgeItem.direction) let attachment = NSTextAttachment() attachment.image = rotatedDirectionIcon attachment.bounds = CGRect(x: 0.0, @@ -194,18 +194,4 @@ extension FavoriteListViewController { let size = UIFontMetrics.default.scaledValue(for: 18.0) return OAUtilities.resize(.icSmallDirection, newSize: CGSize(width: size, height: size))?.withTintColor(tintColor) } - - private func rotatedFavoriteDirectionIcon(_ image: UIImage, radians: CGFloat) -> UIImage { - let format = UIGraphicsImageRendererFormat() - format.scale = image.scale - format.opaque = false - let renderer = UIGraphicsImageRenderer(size: image.size, format: format) - return renderer.image { context in - let rect = CGRect(origin: CGPoint(x: -image.size.width / 2.0, y: -image.size.height / 2.0), - size: image.size) - context.cgContext.translateBy(x: image.size.width / 2.0, y: image.size.height / 2.0) - context.cgContext.rotate(by: radians) - image.draw(in: rect) - } - } } 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) From b11977ef0b22e782f872e13d696c39db01873a8f Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Wed, 17 Jun 2026 19:39:37 +0300 Subject: [PATCH 37/41] refactored --- OsmAnd.xcodeproj/project.pbxproj | 8 +++ .../BackupBannerCollectionViewCell.swift | 68 +++++++++++++++++++ .../Cells/StatsFooterCollectionViewCell.swift | 37 ++++++++++ .../FavoriteListViewController+Cells.swift | 35 ++-------- ...avoriteListViewController+DataSource.swift | 9 --- ...FavoriteListViewController+Delegates.swift | 19 ++++++ .../FavoriteListViewController.swift | 1 - 7 files changed, 138 insertions(+), 39 deletions(-) create mode 100644 Sources/Controllers/Cells/BackupBannerCollectionViewCell.swift create mode 100644 Sources/Controllers/Cells/StatsFooterCollectionViewCell.swift diff --git a/OsmAnd.xcodeproj/project.pbxproj b/OsmAnd.xcodeproj/project.pbxproj index 8eeb21e952..f55c665d21 100644 --- a/OsmAnd.xcodeproj/project.pbxproj +++ b/OsmAnd.xcodeproj/project.pbxproj @@ -306,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 */; }; @@ -3836,6 +3838,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 = ""; }; @@ -12395,6 +12399,8 @@ CE7818252FC07944005CCF47 /* WikipediaContextMenuCell.swift */, 271A5C432FCDE1D700C27411 /* SortButtonCollectionViewCell.swift */, 272AFBAA2FD810CC006C2E21 /* EmptyStateCollectionViewCell.swift */, + 27F055A92FE2EB860025F578 /* StatsFooterCollectionViewCell.swift */, + 27F055AB2FE2F76F0025F578 /* BackupBannerCollectionViewCell.swift */, DA5A7BB626C563A100F274C7 /* Xibs */, 468CD3822C809E3400CC3436 /* ElevationChartCell.swift */, 46BCB15F2CC2AC6D004E0283 /* GradientChartCell.swift */, @@ -18374,6 +18380,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 */, @@ -18627,6 +18634,7 @@ 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 */, 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/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/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift index c9f3fc87b2..4fa7c56167 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift @@ -27,22 +27,9 @@ extension FavoriteListViewController { } } - var backupBannerCellRegistration: UICollectionView.CellRegistration { - UICollectionView.CellRegistration { [weak self] cell, _, _ in - cell.contentView.subviews.forEach { $0.removeFromSuperview() } - guard let self, let banner = Bundle.main.loadNibNamed(FreeBackupBanner.reuseIdentifier, owner: self)?.first as? FreeBackupBanner else { return } - banner.configure(bannerType: .favorite) - banner.didOsmAndCloudButtonAction = { [weak self] in - self?.navigationController?.pushViewController(OACloudIntroductionViewController(), animated: true) - } - banner.didCloseButtonAction = { [weak self] in - self?.closeFreeBackupBanner() - } - banner.translatesAutoresizingMaskIntoConstraints = false - cell.contentView.addSubview(banner) - let fittingWidth = cell.contentView.bounds.width > 0.0 ? cell.contentView.bounds.width : cell.bounds.width - NSLayoutConstraint.activate([banner.topAnchor.constraint(equalTo: cell.contentView.topAnchor), banner.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor), banner.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor)]) - NSLayoutConstraint.activate([banner.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor), banner.heightAnchor.constraint(equalToConstant: self.backupBannerHeight(banner, fittingWidth: fittingWidth))]) + var backupBannerCellRegistration: UICollectionView.CellRegistration { + UICollectionView.CellRegistration { [weak self] cell, _, _ in + cell.delegate = self } } @@ -80,21 +67,11 @@ extension FavoriteListViewController { } } - var statsFooterCellRegistration: UICollectionView.CellRegistration { - UICollectionView.CellRegistration { cell, _, stats in + var statsFooterCellRegistration: UICollectionView.CellRegistration { + UICollectionView.CellRegistration { cell, _, stats in cell.backgroundColor = .clear cell.contentView.backgroundColor = .clear - cell.contentView.subviews.forEach { $0.removeFromSuperview() } - let label = UILabel() - label.font = .preferredFont(forTextStyle: .footnote) - label.adjustsFontForContentSizeCategory = true - label.textColor = .textColorSecondary - label.textAlignment = .center - label.numberOfLines = 0 - label.text = stats.text - label.translatesAutoresizingMaskIntoConstraints = false - cell.contentView.addSubview(label) - NSLayoutConstraint.activate([label.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: Self.statsFooterInsets.top), label.leadingAnchor.constraint(equalTo: cell.contentView.leadingAnchor, constant: Self.statsFooterInsets.leading), label.trailingAnchor.constraint(equalTo: cell.contentView.trailingAnchor, constant: -Self.statsFooterInsets.trailing), label.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -Self.statsFooterInsets.bottom)]) + cell.label.text = stats.text } } diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift index f291c1de92..d208453a0d 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+DataSource.swift @@ -119,15 +119,6 @@ extension FavoriteListViewController { } } - 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) - } - func closeFreeBackupBanner() { UserDefaults.standard.set(true, forKey: Self.wasClosedFreeBackupFavoritesBannerKey) applySnapshot(animatingDifferences: true) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift index 78ee275b3b..03e53aca4b 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift @@ -244,3 +244,22 @@ extension FavoriteListViewController: UIDocumentPickerDelegate { 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.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift index ac7ede70eb..5f1e6911dd 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift @@ -20,7 +20,6 @@ final class FavoriteListViewController: UIViewController { static let navigationTitleMaximumSize: CGFloat = 22.0 static let navigationSubtitleFontSize: CGFloat = 12.0 static let navigationSubtitleMaximumSize: CGFloat = 18.0 - static let statsFooterInsets = NSDirectionalEdgeInsets(top: 12.0, leading: 20.0, bottom: 12.0, trailing: 20.0) static let wasClosedFreeBackupFavoritesBannerKey = "wasClosedFreeBackupFavoritesBanner" let screenMode: ScreenMode From 8e04af265a073e6eb9c489ad691aa375eb1f2223 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Thu, 18 Jun 2026 10:48:08 +0300 Subject: [PATCH 38/41] fixed scaled font --- .../FavoriteListViewController+Cells.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift index 4fa7c56167..251345492f 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift @@ -98,7 +98,7 @@ extension FavoriteListViewController { } private func favoriteSecondaryAttributedText(for favorite: FavoritePointRow, includesGroupName: Bool) -> NSAttributedString { - let font = UIFont.systemFont(ofSize: 15) + let font = UIFont.scaledSystemFont(ofSize: 15) let directionAttributes: [NSAttributedString.Key: Any] = [ .font: font, .foregroundColor: UIColor.textColorDirectionActive From d4afca3d6245e4fca25fdbbfc77543663ca7b77f Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Fri, 26 Jun 2026 13:15:57 +0300 Subject: [PATCH 39/41] not adding points from subfolders fixed --- ...voriteListViewController+ContextMenu.swift | 15 +- .../OAFavoritesBridgeHelper.mm | 219 ++++++------------ 2 files changed, 80 insertions(+), 154 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift index bcd11c06dd..766f7dfe24 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+ContextMenu.swift @@ -11,7 +11,6 @@ extension FavoriteListViewController { let folderFavoriteItem: [Any] = [folder.bridgeItem] let subtreeFavoriteItems: [Any] = favoritePointRows(allFolders: favoriteFolders(), parentGroupName: folder.bridgeItem.groupName).map { $0.bridgeItem } let hasFavoritePoints = !subtreeFavoriteItems.isEmpty - let hasDirectFavoritePoints = folder.bridgeItem.pointsCount > 0 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) @@ -50,20 +49,14 @@ extension FavoriteListViewController { let thirdButtonsSection = UIMenu(title: "", options: .displayInline, children: thirdButtons) let mapMarkersAction = UIAction(title: localizedString("map_markers"), image: .icCustomMarker) { _ in - OAFavoritesBridgeHelper.addFavoriteItems(toMapMarkers: hasDirectFavoritePoints ? folderFavoriteItem : subtreeFavoriteItems) + OAFavoritesBridgeHelper.addFavoriteItems(toMapMarkers: folderFavoriteItem) } let trackAction = UIAction(title: localizedString("shared_string_gpx_track"), image: .icCustomTrip) { [weak self] _ in guard let self else { return } - if hasDirectFavoritePoints { - self.openFavoriteGroupAddToTrack(folder.bridgeItem.groupName) - } else { - self.openFavoriteItemsAddToTrack(subtreeFavoriteItems) - } + self.openFavoriteGroupAddToTrack(folder.bridgeItem.groupName) } - let navigationAction = UIAction(title: localizedString("shared_string_navigation"), image: .icCustomNavigationOutlined) { [weak self] _ in - guard let self else { return } - let directFavoriteItems: [Any] = self.favoritePointRows(forGroupName: folder.bridgeItem.groupName).map { $0.bridgeItem } - OAFavoritesBridgeHelper.addFavoriteItems(toNavigation: hasDirectFavoritePoints ? directFavoriteItems : subtreeFavoriteItems) + 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)] diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm index 6a847dbb67..3928cb931f 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm @@ -384,8 +384,7 @@ + (OASGpxUtilitiesPointsGroup *)pointsGroupForGroupName:(NSString *)groupName + (BOOL)canUseGroupWithName:(NSString *)groupName { - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - return group && group.points.count > 0; + return [self favoriteItemsInsideOrEqualToGroupName:groupName].count > 0; } + (NSURL *)shareFavoriteItems:(NSArray *)favoriteItems @@ -601,50 +600,7 @@ + (void)addFavoriteItemsToMapMarkers:(NSArray *)favoriteItems if (favoriteItems.count == 0) return; - NSMutableArray *itemsToAdd = [NSMutableArray array]; - NSMutableSet *addedPointKeys = [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 (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) - { - NSString *pointKey = [favorite getKey]; - if (pointKey.length > 0 && [addedPointKeys containsObject:pointKey]) - continue; - - if (pointKey.length > 0) - [addedPointKeys addObject:pointKey]; - [itemsToAdd addObject:favorite]; - } - } - - 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 ([addedPointKeys containsObject:pointKey]) - continue; - - if (pointKey.length > 0) - [addedPointKeys addObject:pointKey]; - [itemsToAdd addObject:favorite]; - } - + NSArray *itemsToAdd = [self favoriteItemsForBridgeItemsToAdd:favoriteItems standalonePointItems:NO]; if (itemsToAdd.count == 0) return; @@ -661,8 +617,7 @@ + (void)addFavoriteItemsToMapMarkers:(NSArray *)favoriteItems + (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(nullable NSString *)gpxFileName { - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - NSArray *points = [self sortedFavoritePointsForGroup:group]; + NSArray *points = [self favoriteItemsInsideOrEqualToGroupName:groupName]; if (points.count == 0) return; @@ -688,7 +643,9 @@ + (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(nullable NSSt if (!gpxFile) return; - [gpxFile addPointsGroupGroup:[group toPointsGroup]]; + 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]; @@ -697,11 +654,7 @@ + (void)addFavoriteGroupToTrack:(NSString *)groupName gpxFileName:(nullable NSSt + (void)addFavoriteGroupToNavigation:(NSString *)groupName { - OAFavoriteGroup *group = [self favoriteGroupWithName:groupName]; - if (!group) - return; - - NSArray *points = [self sortedFavoritePointsForGroup:group]; + NSArray *points = [self favoriteItemsInsideOrEqualToGroupName:groupName]; NSMutableArray *items = [NSMutableArray arrayWithCapacity:points.count]; for (OAFavoriteItem *point in points) { @@ -716,51 +669,7 @@ + (void)addFavoriteItemsToTrack:(NSArray *)favoriteItems gpxFileName:(nullable N if (favoriteItems.count == 0) return; - NSMutableArray *itemsToAdd = [NSMutableArray array]; - NSMutableSet *addedPointKeys = [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 (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) - { - NSString *pointKey = [favorite getKey]; - if (pointKey.length > 0 && [addedPointKeys containsObject:pointKey]) - continue; - - if (pointKey.length > 0) - [addedPointKeys addObject:pointKey]; - [itemsToAdd addObject:favorite]; - } - } - - for (id item in favoriteItems) - { - if (![item isKindOfClass:OAFavoritePointBridgeItem.class]) - continue; - - OAFavoritePointBridgeItem *pointItem = (OAFavoritePointBridgeItem *) item; - OAFavoriteItem *favorite = [self standaloneFavoritePoint:[self favoritePointWithIdentifier:pointItem.identifier]]; - - if (!favorite) - continue; - - NSString *pointKey = [favorite getKey] ?: pointItem.identifier; - if ([addedPointKeys containsObject:pointKey]) - continue; - - if (pointKey.length > 0) - [addedPointKeys addObject:pointKey]; - [itemsToAdd addObject:favorite]; - } - + NSArray *itemsToAdd = [self favoriteItemsForBridgeItemsToAdd:favoriteItems standalonePointItems:YES]; if (itemsToAdd.count == 0) return; @@ -800,50 +709,7 @@ + (void)addFavoriteItemsToNavigation:(NSArray *)favoriteItems if (favoriteItems.count == 0) return; - NSMutableArray *itemsToAdd = [NSMutableArray array]; - NSMutableSet *addedPointKeys = [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 (OAFavoriteItem *favorite in [self sortedFavoritePointsForGroup:group]) - { - NSString *pointKey = [favorite getKey]; - if (pointKey.length > 0 && [addedPointKeys containsObject:pointKey]) - continue; - - if (pointKey.length > 0) - [addedPointKeys addObject:pointKey]; - [itemsToAdd addObject:favorite]; - } - } - - 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 ([addedPointKeys containsObject:pointKey]) - continue; - - if (pointKey.length > 0) - [addedPointKeys addObject:pointKey]; - [itemsToAdd addObject:favorite]; - } - + NSArray *itemsToAdd = [self favoriteItemsForBridgeItemsToAdd:favoriteItems standalonePointItems:NO]; if (itemsToAdd.count == 0) return; @@ -892,6 +758,73 @@ + (void)addFavoriteItemsToNavigation:(NSArray *)favoriteItems 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; From 34e26baa2c6ff3ece689cd4bee91263e02cc4b82 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Fri, 26 Jun 2026 17:01:58 +0300 Subject: [PATCH 40/41] expensive distance updates fixed --- .../FavoriteListViewController+Actions.swift | 1 + .../FavoriteListViewController+Cells.swift | 17 ++++++++ ...FavoriteListViewController+Delegates.swift | 2 + .../FavoriteListViewController.swift | 6 ++- .../FavoriteSortModeHelper.swift | 4 ++ .../OAFavoritePointBridgeItem.h | 1 + .../OAFavoritePointBridgeItem.mm | 10 +++++ .../OAFavoritesBridgeHelper.h | 1 + .../OAFavoritesBridgeHelper.mm | 41 +++++++++++++++++-- 9 files changed, 79 insertions(+), 4 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift index 5047cb995a..c1e5faded9 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift @@ -230,6 +230,7 @@ extension FavoriteListViewController { } @objc func favoriteDataDidChange() { + OAFavoritesBridgeHelper.invalidateFavoriteFoldersCache() applySnapshot(animatingDifferences: true) } diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift index 251345492f..f3bd8b69ce 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Cells.swift @@ -52,6 +52,9 @@ extension FavoriteListViewController { 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 @@ -97,6 +100,20 @@ extension FavoriteListViewController { } } + 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] = [ diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift index 03e53aca4b..3b00f7ef5c 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Delegates.swift @@ -200,6 +200,7 @@ extension FavoriteListViewController: OAEditorDelegate { favoriteGroupAppearanceGroupName = nil favoriteGroupAppearanceEditor = nil + OAFavoritesBridgeHelper.invalidateFavoriteFoldersCache() applySnapshot(animatingDifferences: true) } @@ -234,6 +235,7 @@ extension FavoriteListViewController: OAEditorDelegate { extension FavoriteListViewController: OAEditPointViewControllerDelegate { func saveTapped() { + OAFavoritesBridgeHelper.invalidateFavoriteFoldersCache() applySnapshot() } } diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift index 5f1e6911dd..8a95b61aee 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController.swift @@ -185,7 +185,11 @@ final class FavoriteListViewController: UIViewController { let currentTime = Date.now.timeIntervalSince1970 guard forceUpdate || currentTime - lastDistanceDirectionUpdate >= 0.3 else { return } lastDistanceDirectionUpdate = currentTime - applySnapshot(animatingDifferences: false) + if currentSortMode.isDistanceOriented { + applySnapshot(animatingDifferences: false) + } else { + updateVisibleFavoriteCellsDistanceAndDirection() + } } func listCellBackgroundConfiguration() -> UIBackgroundConfiguration { diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteSortModeHelper.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteSortModeHelper.swift index 99f0543380..d49ee8a7d4 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteSortModeHelper.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteSortModeHelper.swift @@ -58,6 +58,10 @@ protocol FavoriteSortablePoint { 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 diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.h b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.h index b40772d08a..9471f73dd0 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.h +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.h @@ -28,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, readonly) BOOL isVisible; - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite; +- (void)updateDistanceAndDirection; @end diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.mm b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.mm index f7253a7546..e5e6f72f40 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.mm +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritePointBridgeItem.mm @@ -14,6 +14,9 @@ #include @implementation OAFavoritePointBridgeItem +{ + OAFavoriteItem *_favorite; +} - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite { @@ -34,11 +37,18 @@ - (instancetype)initWithFavorite:(OAFavoriteItem *)favorite _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; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.h b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.h index db2fcf6ed8..25997ad6b1 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.h +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.h @@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN @interface OAFavoritesBridgeHelper : NSObject ++ (void)invalidateFavoriteFoldersCache; + (NSArray *)favoriteFolders; + (NSArray *)favoritePointsForGroupName:(NSString *)groupName; + (NSString *)sharePoiURLStringForFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem; diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm index 3928cb931f..832b7b069d 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/OAFavoritesBridgeHelper.mm @@ -39,10 +39,20 @@ #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]; @@ -55,7 +65,8 @@ @implementation OAFavoritesBridgeHelper [folders addObject:[[OAFavoriteFolderBridgeItem alloc] initWithGroup:group index:index lastModifiedDate:lastModifiedDate fileSize:fileSize subtreePointsCount:subtreePointsCount]]; }]; - return [folders copy]; + favoriteFoldersCache = [folders copy]; + return favoriteFoldersCache; } + (NSArray *)favoritePointsForGroupName:(NSString *)groupName @@ -111,6 +122,7 @@ + (void)setFavoriteGroupVisible:(NSString *)groupName visible:(BOOL)visible } [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; } + (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned @@ -120,6 +132,7 @@ + (void)setFavoriteGroupPinned:(NSString *)groupName pinned:(BOOL)pinned return; [OAFavoritesHelper updateGroup:group pinned:pinned saveImmediately:YES]; + [self invalidateFavoriteFoldersCache]; } + (void)setFavoriteGroupsVisible:(NSArray *)groupNames visible:(BOOL)visible @@ -144,7 +157,10 @@ + (void)setFavoriteGroupsVisible:(NSArray *)groupNames visible:(BOOL } if (changed) + { [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; + } } + (void)setFavoriteGroupsPinned:(NSArray *)groupNames pinned:(BOOL)pinned @@ -166,7 +182,10 @@ + (void)setFavoriteGroupsPinned:(NSArray *)groupNames pinned:(BOOL)p } if (changed) + { [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; + } } + (BOOL)addFavoriteGroup:(NSString *)name @@ -186,6 +205,7 @@ + (BOOL)addFavoriteGroup:(NSString *)name iconName:iconName backgroundIconName:backgroundIconName]; [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; return YES; } @@ -279,6 +299,9 @@ + (void)moveFavoriteItems:(NSArray *)favoriteItems toGroupName:(NSString *)targe address:[favorite getAddress]]) [movedItemKeys addObject:itemKey]; } + + if (movedItemKeys.count > 0) + [self invalidateFavoriteFoldersCache]; } + (NSArray *)favoriteGroupNamesForMovingFavoriteItems:(NSArray *)favoriteItems @@ -371,7 +394,10 @@ + (void)changeFavoriteItems:(NSArray *)favoriteItems colorIndex:(NSInteger)color } if (changed) - [OAFavoritesHelper saveCurrentPointsIntoFile];; + { + [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; + } } + (OASGpxUtilitiesPointsGroup *)pointsGroupForGroupName:(NSString *)groupName @@ -477,7 +503,10 @@ + (BOOL)deleteFavoriteGroup:(NSString *)groupName if (groupsToDelete.count == 0) return NO; - return [OAFavoritesHelper deleteFavoriteGroups:groupsToDelete andFavoritesItems:nil]; + BOOL didDelete = [OAFavoritesHelper deleteFavoriteGroups:groupsToDelete andFavoritesItems:nil]; + if (didDelete) + [self invalidateFavoriteFoldersCache]; + return didDelete; } + (BOOL)deleteFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem @@ -487,6 +516,7 @@ + (BOOL)deleteFavoritePoint:(OAFavoritePointBridgeItem *)favoriteItem return NO; [OAFavoritesHelper deleteFavorites:@[favorite] saveImmediately:YES]; + [self invalidateFavoriteFoldersCache]; return YES; } @@ -565,6 +595,8 @@ + (BOOL)deleteFavoriteItems:(NSArray *)favoriteItems didDelete = YES; } + if (didDelete) + [self invalidateFavoriteFoldersCache]; return didDelete; } @@ -933,7 +965,10 @@ + (BOOL)renameFavoriteGroupTreeFromGroupName:(NSString *)sourceGroupName toGroup } if (changed) + { [OAFavoritesHelper saveCurrentPointsIntoFile]; + [self invalidateFavoriteFoldersCache]; + } return changed; } From a7b2c3a0b0d8794428473a4e3e95ceac102cdee6 Mon Sep 17 00:00:00 2001 From: Vladyslav Lysenko Date: Fri, 26 Jun 2026 18:18:36 +0300 Subject: [PATCH 41/41] favoriteDataDidChange not in main thread fixed --- .../FavoriteListViewController+Actions.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift index c1e5faded9..d5f126ae43 100644 --- a/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift +++ b/Sources/Controllers/MyPlaces/FavoriteListViewController/FavoriteListViewController+Actions.swift @@ -230,8 +230,10 @@ extension FavoriteListViewController { } @objc func favoriteDataDidChange() { - OAFavoritesBridgeHelper.invalidateFavoriteFoldersCache() - applySnapshot(animatingDifferences: true) + DispatchQueue.main.async { [weak self] in + OAFavoritesBridgeHelper.invalidateFavoriteFoldersCache() + self?.applySnapshot(animatingDifferences: true) + } } @objc func productPurchased() {