From 3c7681b7698bbdde7993e50076eebf3308609ebe Mon Sep 17 00:00:00 2001 From: Christoph Hagen Date: Tue, 29 Apr 2025 16:56:46 +0200 Subject: [PATCH] Add route block --- CHDataManagement.xcodeproj/project.pbxproj | 28 +++++ .../Generator/Blocks/ContentBlock.swift | 3 + .../Generator/Blocks/RouteBlock.swift | 82 +++++++++++++ .../Generator/KnownHeaderElement.swift | 9 ++ .../Model/Settings/PageSettings.swift | 11 ++ .../ContentElements/Images/PageImage.swift | 34 +++++- .../Routes/RouteLocalization.swift | 50 ++++++++ .../Routes/RouteViewComponents.swift | 7 ++ .../ContentElements/Routes/RouteViews.swift | 98 ++++++++++++++++ .../Generation/GenerationContentView.swift | 16 +-- .../Views/Pages/Commands/Insert+Route.swift | 111 ++++++++++++++++++ .../Pages/Commands/InsertableItemsView.swift | 1 + .../Pages/PageSettingsDetailView.swift | 5 + 13 files changed, 446 insertions(+), 9 deletions(-) create mode 100644 CHDataManagement/Generator/Blocks/RouteBlock.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/Routes/RouteLocalization.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/Routes/RouteViewComponents.swift create mode 100644 CHDataManagement/Page Elements/ContentElements/Routes/RouteViews.swift create mode 100644 CHDataManagement/Views/Pages/Commands/Insert+Route.swift diff --git a/CHDataManagement.xcodeproj/project.pbxproj b/CHDataManagement.xcodeproj/project.pbxproj index 2a0edcf..b08b7cd 100644 --- a/CHDataManagement.xcodeproj/project.pbxproj +++ b/CHDataManagement.xcodeproj/project.pbxproj @@ -197,6 +197,11 @@ E2DD04742C276F31003BFF1F /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2DD04732C276F31003BFF1F /* MainView.swift */; }; E2DD047A2C276F32003BFF1F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2DD04792C276F32003BFF1F /* Assets.xcassets */; }; E2E06DFB2CA4A65E0019C2AF /* Content.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E06DFA2CA4A6570019C2AF /* Content.swift */; }; + E2EC1FAB2DC0C99600C41784 /* RouteViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */; }; + E2EC1FAD2DC0D2FA00C41784 /* RouteLocalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */; }; + E2EC1FB02DC0D7DA00C41784 /* RouteBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */; }; + E2EC1FB22DC0D8BD00C41784 /* RouteViewComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FB12DC0D8BD00C41784 /* RouteViewComponents.swift */; }; + E2EC1FB42DC0FA8700C41784 /* Insert+Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */; }; E2FD1D0D2D2DBBA600B48627 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */; }; E2FD1D192D2DC4F500B48627 /* LoadingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */; }; E2FD1D1B2D2DC63800B48627 /* LinkPreviewDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */; }; @@ -470,6 +475,11 @@ E2DD047D2C276F32003BFF1F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; E2E06DFA2CA4A6570019C2AF /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = ""; }; E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Page+Mock.swift"; sourceTree = ""; }; + E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteViews.swift; sourceTree = ""; }; + E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteLocalization.swift; sourceTree = ""; }; + E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteBlock.swift; sourceTree = ""; }; + E2EC1FB12DC0D8BD00C41784 /* RouteViewComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteViewComponents.swift; sourceTree = ""; }; + E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Insert+Route.swift"; sourceTree = ""; }; E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = ""; }; E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingContext.swift; sourceTree = ""; }; E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewDetailView.swift; sourceTree = ""; }; @@ -712,6 +722,7 @@ E29D311E2D0320D90051B7F4 /* ContentElements */ = { isa = PBXGroup; children = ( + E2EC1FAE2DC0D30100C41784 /* Routes */, E2BF1BC52D6B16FA003089F1 /* HeadlineLink.swift */, E2B4821F2D67074C005C309D /* WallpaperSlider.swift */, E29D31C12D0DBED70051B7F4 /* AudioPlayer */, @@ -1063,6 +1074,16 @@ path = "Preview Content"; sourceTree = ""; }; + E2EC1FAE2DC0D30100C41784 /* Routes */ = { + isa = PBXGroup; + children = ( + E2EC1FB12DC0D8BD00C41784 /* RouteViewComponents.swift */, + E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */, + E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */, + ); + path = Routes; + sourceTree = ""; + }; E2FD1D262D2EBBA300B48627 /* Loading */ = { isa = PBXGroup; children = ( @@ -1083,6 +1104,7 @@ E2FD1D382D3BBECA00B48627 /* InsertableView.swift */, E2FD1D672D483CCA00B48627 /* Insert+Buttons.swift */, E2FD1D362D3BBCB500B48627 /* Insert+Image.swift */, + E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */, E2FD1D552D46CED500B48627 /* Insert+Labels.swift */, ); path = Commands; @@ -1125,6 +1147,7 @@ E2FE0F342D2B27E6002963B7 /* Blocks */ = { isa = PBXGroup; children = ( + E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */, E2B482212D676BEB005C309D /* PhoneScreensBlock.swift */, E2FE0F652D2C3B33002963B7 /* LabelsBlock.swift */, E2FE0F5C2D2BD006002963B7 /* Types */, @@ -1306,6 +1329,7 @@ E29D313B2D04464A0051B7F4 /* LocalizedTagDetailView.swift in Sources */, E2FE0F552D2BCFC4002963B7 /* ContentBlock.swift in Sources */, E2A37D212CEA94EC0000979F /* Sequence+Sorted.swift in Sources */, + E2EC1FB02DC0D7DA00C41784 /* RouteBlock.swift in Sources */, E29D31BA2D0DB5080051B7F4 /* LabelsCommand.swift in Sources */, E2FE0EFC2D266D22002963B7 /* NavigationSettings.swift in Sources */, E29D31692D0702700051B7F4 /* TextFileContentView.swift in Sources */, @@ -1395,6 +1419,7 @@ E20BCC9F2D53851400B8DBEB /* SelectableListItem.swift in Sources */, E20BCCAB2D53B86900B8DBEB /* GenerationResultsIssueView.swift in Sources */, E2B482092D5E7F4F005C309D /* WebsitePreviewSheet.swift in Sources */, + E2EC1FAD2DC0D2FA00C41784 /* RouteLocalization.swift in Sources */, E22990262D0F582B009F8D77 /* FilePropertyView.swift in Sources */, E2FD1D462D46428100B48627 /* PageIconView.swift in Sources */, E2A37D252CEBD7A10000979F /* PageListView.swift in Sources */, @@ -1425,6 +1450,7 @@ E25DA5752D018B6100AEF16D /* FileDetailView.swift in Sources */, E25DA50B2CFD988100AEF16D /* PageTagAssignmentView.swift in Sources */, E2FD1D682D483CCF00B48627 /* Insert+Buttons.swift in Sources */, + E2EC1FB22DC0D8BD00C41784 /* RouteViewComponents.swift in Sources */, E29D31A12D0C75CA0051B7F4 /* Content+Validation.swift in Sources */, E29D316D2D07A5050051B7F4 /* PageGenerationResults.swift in Sources */, E229903E2D0F8F02009F8D77 /* StringPropertyView.swift in Sources */, @@ -1519,6 +1545,7 @@ E25DA5712D01015400AEF16D /* GenerationContentView.swift in Sources */, E29D316F2D0822770051B7F4 /* SettingsListView.swift in Sources */, E2A21C0E2CB189DC0060935B /* Color+RGB.swift in Sources */, + E2EC1FAB2DC0C99600C41784 /* RouteViews.swift in Sources */, E25DA5852D01C92700AEF16D /* CommandType.swift in Sources */, E229903A2D0F7E48009F8D77 /* GenericPropertyView.swift in Sources */, E29D31AA2D0CEE3F0051B7F4 /* AudioPlayer.swift in Sources */, @@ -1533,6 +1560,7 @@ E2FD1D602D47EEEF00B48627 /* LocalizedPostContentView.swift in Sources */, E229903C2D0F8A7B009F8D77 /* OptionalTextFieldPropertyView.swift in Sources */, E2FD1D582D477A9400B48627 /* InsertableCommand.swift in Sources */, + E2EC1FB42DC0FA8700C41784 /* Insert+Route.swift in Sources */, E29D31222D0363FD0051B7F4 /* ContentButtons.swift in Sources */, E2FE0F222D2A84A0002963B7 /* VideoCommand.swift in Sources */, E2FE0F192D2723E3002963B7 /* ImageSet.swift in Sources */, diff --git a/CHDataManagement/Generator/Blocks/ContentBlock.swift b/CHDataManagement/Generator/Blocks/ContentBlock.swift index e473893..a9e0eb7 100644 --- a/CHDataManagement/Generator/Blocks/ContentBlock.swift +++ b/CHDataManagement/Generator/Blocks/ContentBlock.swift @@ -15,6 +15,8 @@ enum ContentBlock: String, CaseIterable { case screens + case route + var processor: BlockProcessor.Type { switch self { case .audio: return AudioBlock.self @@ -24,6 +26,7 @@ enum ContentBlock: String, CaseIterable { case .buttons: return ButtonsBlock.self case .labels: return LabelsBlock.self case .screens: return PhoneScreensBlock.self + case .route: return RouteBlock.self } } } diff --git a/CHDataManagement/Generator/Blocks/RouteBlock.swift b/CHDataManagement/Generator/Blocks/RouteBlock.swift new file mode 100644 index 0000000..2eafc1e --- /dev/null +++ b/CHDataManagement/Generator/Blocks/RouteBlock.swift @@ -0,0 +1,82 @@ + +struct RouteBlock: KeyedBlockProcessor { + + enum Key: String { + case chartTitle + case components + case mapTitle + case image + case caption + case file + } + + static let blockId: ContentBlock = .route + + let content: Content + + let results: PageGenerationResults + + let language: ContentLanguage + + init(content: Content, results: PageGenerationResults, language: ContentLanguage) { + self.content = content + self.results = results + self.language = language + } + + private var thumbnailWidth: Int { + content.settings.pages.contentWidth + } + + private var largeImageWidth: Int { + content.settings.pages.largeImageWidth + } + + func process(_ arguments: [Key : String], markdown: Substring) -> String { + let rawComponents = arguments[.components] ?? "all" + guard let imageId = arguments[.image], + let fileId = arguments[.file], + let components = RouteViewComponents(rawValue: rawComponents) else { + invalid(markdown) + return "" + } + + guard let image = content.image(imageId) else { + results.missing(file: imageId, source: "Route block") + return "" + } + guard let file = content.file(fileId) else { + results.missing(file: imageId, source: "Route block") + return "" + } + results.used(file: image) + results.require(file: file) + + let thumbnail = image.imageSet(width: thumbnailWidth, height: thumbnailWidth, language: language) + results.require(imageSet: thumbnail) + + let largeImage = image.imageSet(width: largeImageWidth, height: largeImageWidth, language: language) + results.require(imageSet: largeImage) + + results.require(header: .routeJs) + + let id = imageId.replacingOccurrences(of: ".", with: "-") + + let views = RouteViews( + localization: language == .english ? .english : .german, + chartTitle: arguments[.chartTitle], + chartId: "chart-" + id, + components: components, + mapTitle: arguments[.mapTitle], + mapId: "map-" + id, + filePath: file.absoluteUrl, + imageId: "image-" + id, + thumbnail: thumbnail, + largeImage: largeImage, + caption: arguments[.caption]) + + results.require(footer: views.script) + + return views.content + } +} diff --git a/CHDataManagement/Generator/KnownHeaderElement.swift b/CHDataManagement/Generator/KnownHeaderElement.swift index 84fb2d9..72d4fe0 100644 --- a/CHDataManagement/Generator/KnownHeaderElement.swift +++ b/CHDataManagement/Generator/KnownHeaderElement.swift @@ -19,6 +19,9 @@ enum KnownHeaderElement { case swiperJs + /// The Javascript to compute and animate route statistics + case routeJs + case style(String) func header(content: Content) -> HeaderElement? { @@ -55,6 +58,10 @@ enum KnownHeaderElement { if let swiperJs = content.settings.posts.swiperJsFile { return .js(file: swiperJs, defer: true) } + case .routeJs: + if let routeJs = content.settings.pages.routeJsFile { + return .js(file: routeJs, defer: false) + } case .style(let code): return .style(code) } @@ -96,6 +103,8 @@ extension KnownHeaderElement: CustomStringConvertible { return "swiper-css" case .swiperJs: return "swiper-js" + case .routeJs: + return "route-js" case .style(let style): return "style: " + style } diff --git a/CHDataManagement/Model/Settings/PageSettings.swift b/CHDataManagement/Model/Settings/PageSettings.swift index 2791527..f373335 100644 --- a/CHDataManagement/Model/Settings/PageSettings.swift +++ b/CHDataManagement/Model/Settings/PageSettings.swift @@ -29,6 +29,9 @@ final class PageSettings: ObservableObject { @Published var manifestFile: FileResource? + @Published + var routeJsFile: FileResource? + @Published var german: LocalizedPageSettings @@ -44,6 +47,7 @@ final class PageSettings: ObservableObject { imageCompareJsFile: FileResource? = nil, imageCompareCssFile: FileResource? = nil, manifestFile: FileResource? = nil, + routeJsFile: FileResource? = nil, german: LocalizedPageSettings, english: LocalizedPageSettings) { self.contentWidth = contentWidth @@ -55,6 +59,7 @@ final class PageSettings: ObservableObject { self.imageCompareJsFile = imageCompareJsFile self.imageCompareCssFile = imageCompareCssFile self.manifestFile = manifestFile + self.routeJsFile = routeJsFile self.german = german self.english = english } @@ -78,6 +83,9 @@ final class PageSettings: ObservableObject { if manifestFile == file { manifestFile = nil } + if routeJsFile == file { + routeJsFile = nil + } } } @@ -96,6 +104,7 @@ extension PageSettings { imageCompareJsFile: data.imageCompareJsFile.map(context.file), imageCompareCssFile: data.imageCompareCssFile.map(context.file), manifestFile: data.manifestFile.map(context.file), + routeJsFile: data.routeJsFile.map(context.file), german: .init(data: data.german), english: .init(data: data.english)) } @@ -110,6 +119,7 @@ extension PageSettings { imageCompareJsFile: imageCompareJsFile?.id, imageCompareCssFile: imageCompareCssFile?.id, manifestFile: manifestFile?.id, + routeJsFile: routeJsFile?.id, german: german.data, english: english.data) } @@ -124,6 +134,7 @@ extension PageSettings { let imageCompareJsFile: String? let imageCompareCssFile: String? let manifestFile: String? + let routeJsFile: String? let german: LocalizedPageSettings.Data let english: LocalizedPageSettings.Data } diff --git a/CHDataManagement/Page Elements/ContentElements/Images/PageImage.swift b/CHDataManagement/Page Elements/ContentElements/Images/PageImage.swift index 6280e9a..25014cf 100644 --- a/CHDataManagement/Page Elements/ContentElements/Images/PageImage.swift +++ b/CHDataManagement/Page Elements/ContentElements/Images/PageImage.swift @@ -19,9 +19,41 @@ struct PageImage: HtmlProducer { /// The optional caption text below the fullscreen image let caption: String? + /// The id of the image container + let id: String? + + /// Additional class names for the image container + let className: String? + + /// Additional content for the image container + let imageContent: String? + + init(imageId: String, thumbnail: ImageSet, largeImage: ImageSet, caption: String?, id: String? = nil, className: String? = nil, imageContent: String? = nil) { + self.imageId = imageId + self.thumbnail = thumbnail + self.largeImage = largeImage + self.caption = caption + self.id = id + self.className = className + self.imageContent = imageContent + } + + var idString: String { + guard let id else { return "" } + return " id='\(id)'" + } + + var classString: String { + guard let className else { return "" } + return " \(className)" + } + func populate(_ result: inout String) { - result += "
" + result += "" result += thumbnail.content + if let imageContent { + result += imageContent + } result += "
" result += "
" result += largeImage.content diff --git a/CHDataManagement/Page Elements/ContentElements/Routes/RouteLocalization.swift b/CHDataManagement/Page Elements/ContentElements/Routes/RouteLocalization.swift new file mode 100644 index 0000000..6b10aa2 --- /dev/null +++ b/CHDataManagement/Page Elements/ContentElements/Routes/RouteLocalization.swift @@ -0,0 +1,50 @@ + +struct RouteLocalization { + + let elevation: String + + let speed: String + + let pace: String + + let heartRate: String + + let fallback: String + + let hourUnit: String + + let duration: String + + let time: String + + let distance: String + + let loadFail: String +} + +extension RouteLocalization { + + static let german: RouteLocalization = .init( + elevation: "Höhe", + speed: "Geschw.", + pace: "Pace", + heartRate: "Herzfrequenz", + fallback: "Zur Anzeige der Statistiken wird JavaScript und Unterstützung für HTML5 Canvas benötigt.", + hourUnit: "Std", + duration: "Dauer", + time: "Zeit", + distance: "Distanz", + loadFail: "Die Statistiken konnten nicht geladen werden") + + static let english: RouteLocalization = .init( + elevation: "Elevation", + speed: "Speed", + pace: "Pace", + heartRate: "Heart Rate", + fallback: "Javascript and HTML5 Canvas Support are required to display statistics", + hourUnit: "h", + duration: "Duration", + time: "Time", + distance: "Distance", + loadFail: "The statistics could not be loaded") +} diff --git a/CHDataManagement/Page Elements/ContentElements/Routes/RouteViewComponents.swift b/CHDataManagement/Page Elements/ContentElements/Routes/RouteViewComponents.swift new file mode 100644 index 0000000..1bfc8eb --- /dev/null +++ b/CHDataManagement/Page Elements/ContentElements/Routes/RouteViewComponents.swift @@ -0,0 +1,7 @@ + +enum RouteViewComponents: String { + case onlyElevation = "elevation" + case all = "all" + case withoutHeartRate = "no-hr" +} + diff --git a/CHDataManagement/Page Elements/ContentElements/Routes/RouteViews.swift b/CHDataManagement/Page Elements/ContentElements/Routes/RouteViews.swift new file mode 100644 index 0000000..e3ed452 --- /dev/null +++ b/CHDataManagement/Page Elements/ContentElements/Routes/RouteViews.swift @@ -0,0 +1,98 @@ + +struct RouteViews: HtmlProducer { + + let localization: RouteLocalization + + /// The HTML id attribute used to enable fullscreen images + let map: PageImage + + let components: RouteViewComponents + + let mapId: String + + let mapTitle: String? + + let filePath: String + + let chartId: String + + let chartTitle: String? + + init(localization: RouteLocalization, + chartTitle: String?, + chartId: String, + components: RouteViewComponents, + mapTitle: String?, + mapId: String, + filePath: String, + imageId: String, + thumbnail: ImageSet, + largeImage: ImageSet, + caption: String? + ) { + self.localization = localization + self.components = components + self.mapId = mapId + self.filePath = filePath + self.map = PageImage( + imageId: imageId, + thumbnail: thumbnail, + largeImage: largeImage, + caption: caption, + id: mapId, + className: "map-container", + imageContent: "
") + self.chartTitle = chartTitle + self.chartId = chartId + self.mapTitle = mapTitle + } + + var pickerHiddenText: String { + guard components == .onlyElevation else { return "" } + return " style='display:none'" + } + + func populate(_ result: inout String) { + if let mapTitle { + result += "

\(mapTitle)

" + } + map.populate(&result) + + if let chartTitle { + result += "

\(chartTitle)

" + } + result += "
" + result += "
" + result += "" + result += "" + result += "" + if components == .all { + result += "" + } + result += "
" + result += "
" + result += "" + result += "
\(localization.fallback)
" + result += "
" + result += "
" + result += "
" + result += "
\(localization.loadFail)
" + result += "
" + result += "
" + result += "" + result += "" + result += "" + result += "
" + result += "
" + } + + var script: String { + """ + + """ + } +} diff --git a/CHDataManagement/Views/Generation/GenerationContentView.swift b/CHDataManagement/Views/Generation/GenerationContentView.swift index 112d996..1f1817d 100644 --- a/CHDataManagement/Views/Generation/GenerationContentView.swift +++ b/CHDataManagement/Views/Generation/GenerationContentView.swift @@ -42,6 +42,10 @@ struct GenerationContentView: View { } Text(content.generationStatus) .padding(.vertical, 5) + GenerationStringIssuesView( + text: "output files", + statusWhenNonEmpty: .nominal, + items: $content.results.outputFiles) GenerationResultsIssueView( text: "\(content.results.imagesToGenerate.count) images", status: .nominal, @@ -66,6 +70,10 @@ struct GenerationContentView: View { text: "empty pages", statusWhenNonEmpty: .warning, items: $content.results.emptyPages) { "\($0.pageId) (\($0.language))" } + GenerationStringIssuesView( + text: "additional output files", + statusWhenNonEmpty: .warning, + items: $content.results.unusedFilesInOutput) GenerationStringIssuesView( text: "inaccessible files", items: $content.results.inaccessibleFiles) { $0.id } @@ -93,14 +101,6 @@ struct GenerationContentView: View { GenerationStringIssuesView( text: "invalid blocks", items: $content.results.invalidBlocks) - GenerationStringIssuesView( - text: "output files", - statusWhenNonEmpty: .nominal, - items: $content.results.outputFiles) - GenerationStringIssuesView( - text: "additional output files", - statusWhenNonEmpty: .warning, - items: $content.results.unusedFilesInOutput) GenerationStringIssuesView( text: "warnings", statusWhenNonEmpty: .warning, diff --git a/CHDataManagement/Views/Pages/Commands/Insert+Route.swift b/CHDataManagement/Views/Pages/Commands/Insert+Route.swift new file mode 100644 index 0000000..55928e6 --- /dev/null +++ b/CHDataManagement/Views/Pages/Commands/Insert+Route.swift @@ -0,0 +1,111 @@ +import SwiftUI +import SFSafeSymbols + +struct InsertableRoute: View, InsertableCommandView { + + final class Model: ObservableObject, InsertableCommandModel { + + @Published + var caption: String? + + @Published + var chartTitle: String? + + @Published + var components: RouteViewComponents = .all + + @Published + var mapTitle: String? + + @Published + var selectedImage: FileResource? + + @Published + var dataFile: FileResource? + + var isReady: Bool { + selectedImage != nil && dataFile != nil + } + + init() { + + } + + var command: String? { + guard let selectedImage, + let dataFile else { + return nil + } + var result = ["```route"] + result.append("\(RouteBlock.Key.image.rawValue): \(selectedImage.id)") + result.append("\(RouteBlock.Key.file.rawValue): \(dataFile.id)") + result.append("\(RouteBlock.Key.components.rawValue): \(components.rawValue)") + if let caption { + result.append("\(RouteBlock.Key.caption.rawValue): \(caption)") + } + if let chartTitle { + result.append("\(RouteBlock.Key.chartTitle.rawValue): \(chartTitle)") + } + if let mapTitle { + result.append("\(RouteBlock.Key.mapTitle.rawValue): \(mapTitle)") + } + result.append("\n```") + return result.joined(separator: "\n") + } + } + + static let title = "Route" + + static let sheetTitle = "Insert route map and statistics" + + static let icon: SFSymbol = .map + + @ObservedObject + private var model: Model + + init(model: Model) { + self.model = model + } + + var body: some View { + VStack { + FilePropertyView( + title: "Map Image", + footer: "Select the image to insert as the map", + selectedFile: $model.selectedImage, + allowedType: .image) + FilePropertyView( + title: "Data File", + footer: "Select the file containing the statistics", + selectedFile: $model.dataFile, + allowedType: .text) + OptionalStringPropertyView( + title: "Map Title", + text: $model.mapTitle, + prompt: "Map title", + footer: "The title to show above the map image") + OptionalStringPropertyView( + title: "Map Image Caption", + text: $model.caption, + prompt: "Image Caption", + footer: "The caption to show below the fullscreen map") + OptionalStringPropertyView( + title: "Chart Title", + text: $model.chartTitle, + prompt: "Title", + footer: "The title to show above the statistics") + Picker(selection: $model.components) { + Text("All").tag(RouteViewComponents.all) + Text("Only elevation").tag(RouteViewComponents.withoutHeartRate) + Text("No heart rate").tag(RouteViewComponents.withoutHeartRate) + } label: { + Text("") + } + .pickerStyle(.segmented) + } + } +} + +#Preview { + InsertableRoute(model: .init()) +} diff --git a/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift b/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift index a16e36d..bb4a777 100644 --- a/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift +++ b/CHDataManagement/Views/Pages/Commands/InsertableItemsView.swift @@ -10,6 +10,7 @@ struct InsertableItemsView: View { InsertableView() InsertableView() InsertableView() + InsertableView() } } } diff --git a/CHDataManagement/Views/Settings/Pages/PageSettingsDetailView.swift b/CHDataManagement/Views/Settings/Pages/PageSettingsDetailView.swift index 66fe61e..2d3112e 100644 --- a/CHDataManagement/Views/Settings/Pages/PageSettingsDetailView.swift +++ b/CHDataManagement/Views/Settings/Pages/PageSettingsDetailView.swift @@ -62,6 +62,11 @@ struct PageSettingsDetailView: View { footer: "The manifest file with the properties of the website when used as a progressive web app", selectedFile: $pageSettings.manifestFile) + FilePropertyView( + title: "Route Statistics Javascript File", + footer: "The JavaScript file containing the logic to compute and animate statistics about workout routes", + selectedFile: $pageSettings.routeJsFile) + LocalizedPageSettingsView(settings: pageSettings.localized(in: language)) .id(language) }