Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-22 13:58:55 +01:00
parent 4af19165ec
commit 68073add76
12458 changed files with 12350765 additions and 2 deletions

View file

@ -0,0 +1,129 @@
import CarPlay
final class ListTemplateBuilder {
enum ListTemplateType {
case history
case bookmarkLists
case bookmarks(category: BookmarkGroup)
case searchResults(results: [MWMCarPlaySearchResultObject])
}
enum BarButtonType {
case bookmarks
case search
}
// MARK: - CPListTemplate builder
class func buildListTemplate(for type: ListTemplateType) -> CPListTemplate {
var title = ""
var trailingNavigationBarButtons = [CPBarButton]()
switch type {
case .history:
title = L("pick_destination")
let bookmarksButton = buildBarButton(type: .bookmarks) { _ in
let listTemplate = ListTemplateBuilder.buildListTemplate(for: .bookmarkLists)
CarPlayService.shared.pushTemplate(listTemplate, animated: true)
}
let searchButton = buildBarButton(type: .search) { _ in
if CarPlayService.shared.isKeyboardLimited {
CarPlayService.shared.showKeyboardAlert()
} else {
let searchTemplate = SearchTemplateBuilder.buildSearchTemplate()
CarPlayService.shared.pushTemplate(searchTemplate, animated: true)
}
}
trailingNavigationBarButtons = [searchButton, bookmarksButton]
case .bookmarkLists:
title = L("bookmarks")
case .searchResults:
title = L("search_results")
case .bookmarks(let category):
title = category.title
}
let sections = buildSectionsForType(type)
let template = CPListTemplate(title: title, sections: sections)
template.trailingNavigationBarButtons = trailingNavigationBarButtons
return template
}
private class func buildSectionsForType(_ type: ListTemplateType) -> [CPListSection] {
switch type {
case .history:
return buildHistorySections()
case .bookmarks(let category):
return buildBookmarksSections(categoryId: category.categoryId)
case .bookmarkLists:
return buildBookmarkListsSections()
case .searchResults(let results):
return buildSearchResultsSections(results)
}
}
private class func buildHistorySections() -> [CPListSection] {
let searchQueries = FrameworkHelper.obtainLastSearchQueries()
let items = searchQueries.map({ (text) -> CPListItem in
let item = CPListItem(text: text, detailText: nil, image: UIImage(named: "recent"))
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.history,
metadata: nil)
return item
})
return [CPListSection(items: items)]
}
private class func buildBookmarkListsSections() -> [CPListSection] {
let bookmarkManager = BookmarksManager.shared()
let categories = bookmarkManager.sortedUserCategories()
let items: [CPListItem] = categories.compactMap({ category in
if category.bookmarksCount == 0 { return nil }
let placesString = category.placesCountTitle()
let item = CPListItem(text: category.title, detailText: placesString)
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.bookmarkLists,
metadata: CategoryInfo(category: category))
return item
})
return [CPListSection(items: items)]
}
private class func buildBookmarksSections(categoryId: MWMMarkGroupID) -> [CPListSection] {
let bookmarkManager = BookmarksManager.shared()
let bookmarks = bookmarkManager.bookmarks(forCategory: categoryId)
var items = bookmarks.map({ (bookmark) -> CPListItem in
let item = CPListItem(text: bookmark.prefferedName, detailText: bookmark.address)
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.bookmarks,
metadata: BookmarkInfo(categoryId: categoryId,
bookmarkId: bookmark.bookmarkId))
return item
})
let maxItemCount = CPListTemplate.maximumItemCount - 1
if items.count >= maxItemCount {
items = Array(items.prefix(maxItemCount))
let cropWarning = CPListItem(text: L("not_all_shown_bookmarks_carplay"), detailText: L("switch_to_phone_bookmarks_carplay"))
cropWarning.isEnabled = false
items.append(cropWarning)
}
return [CPListSection(items: items)]
}
private class func buildSearchResultsSections(_ results: [MWMCarPlaySearchResultObject]) -> [CPListSection] {
var items = [CPListItem]()
for object in results {
let item = CPListItem(text: object.title, detailText: object.address)
item.userInfo = ListItemInfo(type: CPConstants.ListItemType.searchResults,
metadata: SearchResultInfo(originalRow: object.originalRow))
items.append(item)
}
return [CPListSection(items: items)]
}
// MARK: - CPBarButton builder
private class func buildBarButton(type: BarButtonType, action: ((CPBarButton) -> Void)?) -> CPBarButton {
switch type {
case .bookmarks:
return CPBarButton(image: UIImage(systemName: "list.star")!, handler: action)
case .search:
return CPBarButton(image: UIImage(systemName: "keyboard.fill")!, handler: action)
}
}
}

View file

@ -0,0 +1,205 @@
import CarPlay
final class MapTemplateBuilder {
enum MapButtonType {
case startPanning
case zoomIn
case zoomOut
}
enum BarButtonType {
case dismissPaning
case destination
case recenter
case settings
case mute
case unmute
case redirectRoute
case endRoute
}
private enum Constants {
static let carPlayGuidanceBackgroundColor = UIColor(46, 100, 51, 1.0)
}
// MARK: - CPMapTemplate builders
class func buildBaseTemplate(positionMode: MWMMyPositionMode) -> CPMapTemplate {
let mapTemplate = CPMapTemplate()
mapTemplate.hidesButtonsWithNavigationBar = false
configureBaseUI(mapTemplate: mapTemplate)
if positionMode == .pendingPosition {
mapTemplate.leadingNavigationBarButtons = []
} else if positionMode == .follow || positionMode == .followAndRotate {
setupDestinationButton(mapTemplate: mapTemplate)
} else {
setupRecenterButton(mapTemplate: mapTemplate)
}
return mapTemplate
}
class func buildNavigationTemplate() -> CPMapTemplate {
let mapTemplate = CPMapTemplate()
mapTemplate.hidesButtonsWithNavigationBar = false
configureNavigationUI(mapTemplate: mapTemplate)
return mapTemplate
}
class func buildTripPreviewTemplate(forTrips trips: [CPTrip]) -> CPMapTemplate {
let mapTemplate = CPMapTemplate()
mapTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.preview, trips: trips)
mapTemplate.mapButtons = []
mapTemplate.leadingNavigationBarButtons = []
let settingsButton = buildBarButton(type: .settings) { _ in
mapTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.previewSettings)
let gridTemplate = SettingsTemplateBuilder.buildGridTemplate()
CarPlayService.shared.pushTemplate(gridTemplate, animated: true)
}
mapTemplate.trailingNavigationBarButtons = [settingsButton]
return mapTemplate
}
// MARK: - MapTemplate UI configs
class func configureBaseUI(mapTemplate: CPMapTemplate) {
mapTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.main)
let panningButton = buildMapButton(type: .startPanning) { _ in
mapTemplate.showPanningInterface(animated: true)
}
let zoomInButton = buildMapButton(type: .zoomIn) { _ in
FrameworkHelper.zoomMap(.in)
}
let zoomOutButton = buildMapButton(type: .zoomOut) { _ in
FrameworkHelper.zoomMap(.out)
}
mapTemplate.mapButtons = [panningButton, zoomInButton, zoomOutButton]
let settingsButton = buildBarButton(type: .settings) { _ in
let gridTemplate = SettingsTemplateBuilder.buildGridTemplate()
CarPlayService.shared.pushTemplate(gridTemplate, animated: true)
}
mapTemplate.trailingNavigationBarButtons = [settingsButton]
}
class func configurePanUI(mapTemplate: CPMapTemplate) {
let zoomInButton = buildMapButton(type: .zoomIn) { _ in
FrameworkHelper.zoomMap(.in)
}
let zoomOutButton = buildMapButton(type: .zoomOut) { _ in
FrameworkHelper.zoomMap(.out)
}
mapTemplate.mapButtons = [zoomInButton, zoomOutButton]
let doneButton = buildBarButton(type: .dismissPaning) { _ in
mapTemplate.dismissPanningInterface(animated: true)
}
mapTemplate.leadingNavigationBarButtons = []
mapTemplate.trailingNavigationBarButtons = [doneButton]
}
class func configureNavigationUI(mapTemplate: CPMapTemplate) {
mapTemplate.userInfo = MapInfo(type: CPConstants.TemplateType.navigation)
let panningButton = buildMapButton(type: .startPanning) { _ in
mapTemplate.showPanningInterface(animated: true)
}
mapTemplate.mapButtons = [panningButton]
setupMuteAndRedirectButtons(template: mapTemplate)
let endButton = buildBarButton(type: .endRoute) { _ in
CarPlayService.shared.cancelCurrentTrip()
}
mapTemplate.trailingNavigationBarButtons = [endButton]
mapTemplate.guidanceBackgroundColor = Constants.carPlayGuidanceBackgroundColor
}
// MARK: - Conditional navigation buttons
class func setupDestinationButton(mapTemplate: CPMapTemplate) {
let destinationButton = buildBarButton(type: .destination) { _ in
let listTemplate = ListTemplateBuilder.buildListTemplate(for: .history)
CarPlayService.shared.pushTemplate(listTemplate, animated: true)
}
mapTemplate.leadingNavigationBarButtons = [destinationButton]
}
class func setupRecenterButton(mapTemplate: CPMapTemplate) {
let recenterButton = buildBarButton(type: .recenter) { _ in
FrameworkHelper.switchMyPositionMode()
}
mapTemplate.leadingNavigationBarButtons = [recenterButton]
}
private class func setupMuteAndRedirectButtons(template: CPMapTemplate) {
let redirectButton = buildBarButton(type: .redirectRoute) { _ in
let listTemplate = ListTemplateBuilder.buildListTemplate(for: .history)
CarPlayService.shared.pushTemplate(listTemplate, animated: true)
}
if MWMTextToSpeech.isTTSEnabled() {
let muteButton = buildBarButton(type: .mute) { _ in
MWMTextToSpeech.tts().active = false
setupUnmuteAndRedirectButtons(template: template)
}
template.leadingNavigationBarButtons = [muteButton, redirectButton]
} else {
template.leadingNavigationBarButtons = [redirectButton]
}
}
private class func setupUnmuteAndRedirectButtons(template: CPMapTemplate) {
let redirectButton = buildBarButton(type: .redirectRoute) { _ in
let listTemplate = ListTemplateBuilder.buildListTemplate(for: .history)
CarPlayService.shared.pushTemplate(listTemplate, animated: true)
}
if MWMTextToSpeech.isTTSEnabled() {
let unmuteButton = buildBarButton(type: .unmute) { _ in
MWMTextToSpeech.tts().active = true
setupMuteAndRedirectButtons(template: template)
}
template.leadingNavigationBarButtons = [unmuteButton, redirectButton]
} else {
template.leadingNavigationBarButtons = [redirectButton]
}
}
// MARK: - CPMapButton builder
private class func buildMapButton(type: MapButtonType, action: ((CPMapButton) -> Void)?) -> CPMapButton {
let button = CPMapButton(handler: action)
switch type {
case .startPanning:
button.image = UIImage(systemName: "arrow.up.and.down.and.arrow.left.and.right")
case .zoomIn:
button.image = UIImage(systemName: "plus")
case .zoomOut:
button.image = UIImage(systemName: "minus")
}
// Remove code below once Apple has fixed its issue with the button background
if #unavailable(iOS 26) {
switch type {
case .startPanning:
button.focusedImage = UIImage(systemName: "smallcircle.filled.circle.fill")
case .zoomIn:
button.focusedImage = UIImage(systemName: "plus.circle.fill")
case .zoomOut:
button.focusedImage = UIImage(systemName: "minus.circle.fill")
}
}
return button
}
// MARK: - CPBarButton builder
private class func buildBarButton(type: BarButtonType, action: ((CPBarButton) -> Void)?) -> CPBarButton {
switch type {
case .dismissPaning:
return CPBarButton(title: L("done"), handler: action)
case .destination:
return CPBarButton(title: L("pick_destination"), handler: action)
case .recenter:
return CPBarButton(title: L("follow_my_position"), handler: action)
case .settings:
return CPBarButton(image: UIImage(systemName: "gearshape.fill")!, handler: action)
case .mute:
return CPBarButton(image: UIImage(systemName: "speaker.wave.3")!, handler: action)
case .unmute:
return CPBarButton(image: UIImage(systemName: "speaker.slash")!, handler: action)
case .redirectRoute:
return CPBarButton(image: UIImage(named: "ic_carplay_redirect_route")!, handler: action)
case .endRoute:
return CPBarButton(title: L("navigation_stop_button").capitalized, handler: action)
}
}
}

View file

@ -0,0 +1,9 @@
import CarPlay
final class SearchTemplateBuilder {
// MARK: - CPSearchTemplate builder
class func buildSearchTemplate() -> CPSearchTemplate {
let template = CPSearchTemplate()
return template
}
}

View file

@ -0,0 +1,157 @@
import CarPlay
final class SettingsTemplateBuilder {
// MARK: - CPGridTemplate builder
class func buildGridTemplate() -> CPGridTemplate {
let actions = SettingsTemplateBuilder.buildGridButtons()
let gridTemplate = CPGridTemplate(title: L("settings"),
gridButtons: actions)
return gridTemplate
}
private class func buildGridButtons() -> [CPGridButton] {
let options = RoutingOptions()
return [createTollButton(options: options),
createUnpavedButton(options: options),
createPavedButton(options: options),
createMotorwayButton(options: options),
createFerryButton(options: options),
createStepsButton(options: options),
createSpeedcamButton()]
}
// MARK: - CPGridButton builders
private class func createTollButton(options: RoutingOptions) -> CPGridButton {
var tollIconName = "tolls.circle"
if options.avoidToll { tollIconName += ".slash" }
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
var image = UIImage(named: tollIconName, in: nil, with: configuration)!
if #unavailable(iOS 26) {
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
}
let tollButton = CPGridButton(titleVariants: [L("avoid_tolls")], image: image) { _ in
options.avoidToll = !options.avoidToll
options.save()
CarPlayService.shared.updateRouteAfterChangingSettings()
CarPlayService.shared.popTemplate(animated: true)
}
return tollButton
}
private class func createUnpavedButton(options: RoutingOptions) -> CPGridButton {
var unpavedIconName = "unpaved.circle"
if options.avoidDirty && !options.avoidPaved { unpavedIconName += ".slash" }
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
var image = UIImage(named: unpavedIconName, in: nil, with: configuration)!
if #unavailable(iOS 26) {
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
}
let unpavedButton = CPGridButton(titleVariants: [L("avoid_unpaved")], image: image) { _ in
options.avoidDirty = !options.avoidDirty
if options.avoidDirty {
options.avoidPaved = false
}
options.save()
CarPlayService.shared.updateRouteAfterChangingSettings()
CarPlayService.shared.popTemplate(animated: true)
}
unpavedButton.isEnabled = !options.avoidPaved
return unpavedButton
}
private class func createPavedButton(options: RoutingOptions) -> CPGridButton {
var pavedIconName = "paved.circle"
if options.avoidPaved && !options.avoidDirty { pavedIconName += ".slash" }
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
var image = UIImage(named: pavedIconName, in: nil, with: configuration)!
if #unavailable(iOS 26) {
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
}
let pavedButton = CPGridButton(titleVariants: [L("avoid_paved")], image: image) { _ in
options.avoidPaved = !options.avoidPaved
if options.avoidPaved {
options.avoidDirty = false
}
options.save()
CarPlayService.shared.updateRouteAfterChangingSettings()
CarPlayService.shared.popTemplate(animated: true)
}
pavedButton.isEnabled = !options.avoidDirty
return pavedButton
}
private class func createMotorwayButton(options: RoutingOptions) -> CPGridButton {
var motorwayIconName = "motorways.circle"
if options.avoidMotorway { motorwayIconName += ".slash" }
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
var image = UIImage(named: motorwayIconName, in: nil, with: configuration)!
if #unavailable(iOS 26) {
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
}
let motorwayButton = CPGridButton(titleVariants: [L("avoid_motorways")], image: image) { _ in
options.avoidMotorway = !options.avoidMotorway
options.save()
CarPlayService.shared.updateRouteAfterChangingSettings()
CarPlayService.shared.popTemplate(animated: true)
}
return motorwayButton
}
private class func createFerryButton(options: RoutingOptions) -> CPGridButton {
var ferryIconName = "ferries.circle"
if options.avoidFerry { ferryIconName += ".slash" }
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
var image = UIImage(named: ferryIconName, in: nil, with: configuration)!
if #unavailable(iOS 26) {
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
}
let ferryButton = CPGridButton(titleVariants: [L("avoid_ferry")], image: image) { _ in
options.avoidFerry = !options.avoidFerry
options.save()
CarPlayService.shared.updateRouteAfterChangingSettings()
CarPlayService.shared.popTemplate(animated: true)
}
return ferryButton
}
private class func createStepsButton(options: RoutingOptions) -> CPGridButton {
var stepsIconName = "steps.circle"
if options.avoidSteps { stepsIconName += ".slash" }
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
var image = UIImage(named: stepsIconName, in: nil, with: configuration)!
if #unavailable(iOS 26) {
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
}
let stepsButton = CPGridButton(titleVariants: [L("avoid_steps")], image: image) { _ in
options.avoidSteps = !options.avoidSteps
options.save()
CarPlayService.shared.updateRouteAfterChangingSettings()
CarPlayService.shared.popTemplate(animated: true)
}
return stepsButton
}
private class func createSpeedcamButton() -> CPGridButton {
var speedcamIconName = "speedcamera"
let isSpeedCamActivated = CarPlayService.shared.isSpeedCamActivated
if !isSpeedCamActivated { speedcamIconName += ".slash" }
let configuration = UIImage.SymbolConfiguration(textStyle: .title1)
var image = UIImage(named: speedcamIconName, in: nil, with: configuration)!
if #unavailable(iOS 26) {
image = image.withTintColor(.white, renderingMode: .alwaysTemplate)
image = UIImage(data: image.pngData()!)!.withRenderingMode(.alwaysTemplate)
}
let speedButton = CPGridButton(titleVariants: [L("speedcams_alert_title_carplay_1"), L("speedcams_alert_title_carplay_2")], image: image) { _ in
CarPlayService.shared.isSpeedCamActivated = !isSpeedCamActivated
CarPlayService.shared.popTemplate(animated: true)
}
return speedButton
}
}