Compare commits
3 Commits
6ba75ab916
...
f3de1b72b6
Author | SHA1 | Date | |
---|---|---|---|
|
f3de1b72b6 | ||
|
3c7681b769 | ||
|
bbb1143600 |
@ -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 = "<group>"; };
|
||||
E2E06DFA2CA4A6570019C2AF /* Content.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Content.swift; sourceTree = "<group>"; };
|
||||
E2E06DFF2CA4A8EB0019C2AF /* Page+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Page+Mock.swift"; sourceTree = "<group>"; };
|
||||
E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteViews.swift; sourceTree = "<group>"; };
|
||||
E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteLocalization.swift; sourceTree = "<group>"; };
|
||||
E2EC1FAF2DC0D7D600C41784 /* RouteBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteBlock.swift; sourceTree = "<group>"; };
|
||||
E2EC1FB12DC0D8BD00C41784 /* RouteViewComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteViewComponents.swift; sourceTree = "<group>"; };
|
||||
E2EC1FB32DC0FA6D00C41784 /* Insert+Route.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Insert+Route.swift"; sourceTree = "<group>"; };
|
||||
E2FD1D0C2D2DBBA100B48627 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = "<group>"; };
|
||||
E2FD1D182D2DC4F500B48627 /* LoadingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingContext.swift; sourceTree = "<group>"; };
|
||||
E2FD1D1A2D2DC62C00B48627 /* LinkPreviewDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreviewDetailView.swift; sourceTree = "<group>"; };
|
||||
@ -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 = "<group>";
|
||||
};
|
||||
E2EC1FAE2DC0D30100C41784 /* Routes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E2EC1FB12DC0D8BD00C41784 /* RouteViewComponents.swift */,
|
||||
E2EC1FAC2DC0D2FA00C41784 /* RouteLocalization.swift */,
|
||||
E2EC1FAA2DC0C98C00C41784 /* RouteViews.swift */,
|
||||
);
|
||||
path = Routes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
82
CHDataManagement/Generator/Blocks/RouteBlock.swift
Normal file
82
CHDataManagement/Generator/Blocks/RouteBlock.swift
Normal file
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -25,6 +25,9 @@ final class GenerationResults: ObservableObject {
|
||||
@Published
|
||||
var requiredFiles: Set<FileResource> = []
|
||||
|
||||
@Published
|
||||
var externalFiles: Set<FileResource> = []
|
||||
|
||||
@Published
|
||||
var imagesToGenerate: Set<ImageVersion> = []
|
||||
|
||||
@ -113,6 +116,7 @@ final class GenerationResults: ObservableObject {
|
||||
self.missingPages = []
|
||||
self.externalLinks = []
|
||||
self.requiredFiles = []
|
||||
self.externalFiles = []
|
||||
self.imagesToGenerate = []
|
||||
self.invalidCommands = []
|
||||
self.invalidBlocks = []
|
||||
@ -143,6 +147,8 @@ final class GenerationResults: ObservableObject {
|
||||
update { self.externalLinks = externalLinks }
|
||||
let requiredFiles = cache.values.map { $0.requiredFiles }.union()
|
||||
update { self.requiredFiles = requiredFiles }
|
||||
let externalFiles = cache.values.map { $0.requiredFiles.filter { $0.isExternallyStored } }.union()
|
||||
update { self.externalFiles = externalFiles }
|
||||
let imagesToGenerate = cache.values.map { $0.imagesToGenerate }.union()
|
||||
update { self.imagesToGenerate = imagesToGenerate }
|
||||
let invalidCommands = cache.values.map { $0.invalidCommands.map { $0.markdown }}.union()
|
||||
@ -195,11 +201,19 @@ final class GenerationResults: ObservableObject {
|
||||
}
|
||||
|
||||
func require(file: FileResource) {
|
||||
update { self.requiredFiles.insert(file) }
|
||||
update {
|
||||
self.requiredFiles.insert(file)
|
||||
if file.isExternallyStored {
|
||||
self.externalFiles.insert(file)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func require<S>(files: S) where S: Sequence, S.Element == FileResource {
|
||||
update { self.requiredFiles.formUnion(files) }
|
||||
update {
|
||||
self.requiredFiles.formUnion(files)
|
||||
self.externalFiles.formUnion(files.filter { $0.isExternallyStored })
|
||||
}
|
||||
}
|
||||
|
||||
func generate(_ image: ImageVersion) {
|
||||
|
@ -108,7 +108,11 @@ final class FileResource: Item, LocalizedItem {
|
||||
}
|
||||
|
||||
func save(textContent: String) -> Bool {
|
||||
content.storage.save(fileContent: textContent, for: id)
|
||||
guard content.storage.save(fileContent: textContent, for: id) else {
|
||||
return false
|
||||
}
|
||||
modifiedDate = .now
|
||||
return true
|
||||
}
|
||||
|
||||
func dataContent() -> Foundation.Data? {
|
||||
|
@ -90,6 +90,8 @@ enum FileType: String {
|
||||
|
||||
case txt
|
||||
|
||||
case gpx
|
||||
|
||||
// MARK: Model
|
||||
|
||||
case stl
|
||||
@ -164,7 +166,7 @@ enum FileType: String {
|
||||
return .video
|
||||
case .mp3, .aac, .m4b, .m4a:
|
||||
return .audio
|
||||
case .json, .conf, .yaml, .txt:
|
||||
case .json, .conf, .yaml, .txt, .gpx:
|
||||
return .text
|
||||
case .html, .cpp, .swift, .sh, .js, .css:
|
||||
return .code
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 += "<div class='content-image' onclick=\"document.getElementById('\(imageId)').classList.add('active')\">"
|
||||
result += "<div\(idString) class='content-image\(classString)' onclick=\"document.getElementById('\(imageId)').classList.add('active')\">"
|
||||
result += thumbnail.content
|
||||
if let imageContent {
|
||||
result += imageContent
|
||||
}
|
||||
result += "</div>"
|
||||
result += "<div id='\(imageId)' class='fullscreen-image' onclick=\"document.getElementById('\(imageId)').classList.remove('active')\">"
|
||||
result += largeImage.content
|
||||
|
@ -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")
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
|
||||
enum RouteViewComponents: String {
|
||||
case onlyElevation = "elevation"
|
||||
case all = "all"
|
||||
case withoutHeartRate = "no-hr"
|
||||
}
|
||||
|
@ -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: "<div class='marker'></div>")
|
||||
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 += "<h2>\(mapTitle)</h2>"
|
||||
}
|
||||
map.populate(&result)
|
||||
|
||||
if let chartTitle {
|
||||
result += "<h2>\(chartTitle)</h2>"
|
||||
}
|
||||
result += "<div id='\(chartId)' class='charts'>"
|
||||
result += "<div class='picker y-picker'\(pickerHiddenText)>"
|
||||
result += "<button data-type='elevation' unit='m' class='active'>\(localization.elevation)</button>"
|
||||
result += "<button data-type='speed' unit='km/h'>\(localization.speed)</button>"
|
||||
result += "<button data-type='pace' unit='min/km'>\(localization.pace)</button>"
|
||||
if components == .all {
|
||||
result += "<button data-type='hr' unit='bpm'>\(localization.heartRate)</button>"
|
||||
}
|
||||
result += "</div>"
|
||||
result += "<div class='graph'>"
|
||||
result += "<canvas>"
|
||||
result += "<div class='fallback'>\(localization.fallback)</div>"
|
||||
result += "</canvas>"
|
||||
result += "<div class='line'></div>"
|
||||
result += "<div class='tooltip'></div>"
|
||||
result += "<div class='load-error'>\(localization.loadFail)</div>"
|
||||
result += "</div>"
|
||||
result += "<div class='picker x-picker'\(pickerHiddenText)>"
|
||||
result += "<button data-type='distance' unit='km' class='active'>\(localization.distance)</button>"
|
||||
result += "<button data-type='duration' unit='\(localization.hourUnit)'>\(localization.duration)</button>"
|
||||
result += "<button data-type='time' unit=''>\(localization.time)</button>"
|
||||
result += "</div>"
|
||||
result += "</div>"
|
||||
}
|
||||
|
||||
var script: String {
|
||||
"""
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
initializeGraphs(document, '\(mapId)', '\(chartId)', '\(filePath)');
|
||||
});
|
||||
</script>
|
||||
"""
|
||||
}
|
||||
}
|
@ -41,7 +41,7 @@ struct FileContentView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
case .text, .code:
|
||||
TextFileContentView(file: file)
|
||||
.id(file.id)
|
||||
.id(file.id + file.modifiedDate.description)
|
||||
case .video:
|
||||
VStack {
|
||||
if let image = file.imageToDisplay {
|
||||
|
@ -11,6 +11,9 @@ struct TextFileContentView: View {
|
||||
@State
|
||||
private var loadedFile: String?
|
||||
|
||||
@State
|
||||
private var loadedFileDate: Date = .now
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if fileContent != "" {
|
||||
@ -47,6 +50,7 @@ struct TextFileContentView: View {
|
||||
private func reload() {
|
||||
fileContent = file.textContent()
|
||||
loadedFile = file.id
|
||||
loadedFileDate = file.modifiedDate
|
||||
print("Loaded content of file \(file.id)")
|
||||
}
|
||||
|
||||
@ -57,6 +61,12 @@ struct TextFileContentView: View {
|
||||
}
|
||||
guard loadedFile == file.id else {
|
||||
print("[ERROR] Text File View: Not saving since file changed")
|
||||
reload()
|
||||
return
|
||||
}
|
||||
guard loadedFileDate == file.modifiedDate else {
|
||||
print("Text File View: Not saving changed file \(file.id)")
|
||||
reload()
|
||||
return
|
||||
}
|
||||
guard fileContent != "" else {
|
||||
@ -67,6 +77,7 @@ struct TextFileContentView: View {
|
||||
print("[ERROR] Text File View: Failed to save file \(file.id)")
|
||||
return
|
||||
}
|
||||
loadedFileDate = file.modifiedDate
|
||||
print("Text File View: Saved file \(file.id)")
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
@ -58,10 +62,18 @@ struct GenerationContentView: View {
|
||||
text: "required files",
|
||||
statusWhenNonEmpty: .nominal,
|
||||
items: $content.results.requiredFiles) { $0.id }
|
||||
GenerationStringIssuesView(
|
||||
text: "external files",
|
||||
statusWhenNonEmpty: .nominal,
|
||||
items: $content.results.externalFiles) { $0.id }
|
||||
GenerationStringIssuesView(
|
||||
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 }
|
||||
@ -89,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,
|
||||
|
111
CHDataManagement/Views/Pages/Commands/Insert+Route.swift
Normal file
111
CHDataManagement/Views/Pages/Commands/Insert+Route.swift
Normal file
@ -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())
|
||||
}
|
@ -10,6 +10,7 @@ struct InsertableItemsView: View {
|
||||
InsertableView<InsertableLabels>()
|
||||
InsertableView<InsertableButtons>()
|
||||
InsertableView<InsertableLink>()
|
||||
InsertableView<InsertableRoute>()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user