Generate labels from workout
This commit is contained in:
@@ -183,6 +183,7 @@
|
|||||||
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; };
|
E2A9CB7E2C7BCF2A005C89CC /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2A9CB7D2C7BCF2A005C89CC /* Page.swift */; };
|
||||||
E2ADC02A2E5794AB00B4FF88 /* RouteOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2ADC0292E5794AB00B4FF88 /* RouteOverview.swift */; };
|
E2ADC02A2E5794AB00B4FF88 /* RouteOverview.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2ADC0292E5794AB00B4FF88 /* RouteOverview.swift */; };
|
||||||
E2ADC02C2E5795F300B4FF88 /* ElevationGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2ADC02B2E5795F000B4FF88 /* ElevationGraph.swift */; };
|
E2ADC02C2E5795F300B4FF88 /* ElevationGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2ADC02B2E5795F000B4FF88 /* ElevationGraph.swift */; };
|
||||||
|
E2ADC02E2E57CC6900B4FF88 /* Double+Rounded.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2ADC02D2E57CC6500B4FF88 /* Double+Rounded.swift */; };
|
||||||
E2B482002D5D1136005C309D /* Vapor in Frameworks */ = {isa = PBXBuildFile; productRef = E2B481FF2D5D1136005C309D /* Vapor */; };
|
E2B482002D5D1136005C309D /* Vapor in Frameworks */ = {isa = PBXBuildFile; productRef = E2B481FF2D5D1136005C309D /* Vapor */; };
|
||||||
E2B482032D5D1331005C309D /* WebServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482022D5D132D005C309D /* WebServer.swift */; };
|
E2B482032D5D1331005C309D /* WebServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482022D5D132D005C309D /* WebServer.swift */; };
|
||||||
E2B482052D5E7D4A005C309D /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482042D5E7D4A005C309D /* WebView.swift */; };
|
E2B482052D5E7D4A005C309D /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B482042D5E7D4A005C309D /* WebView.swift */; };
|
||||||
@@ -477,6 +478,7 @@
|
|||||||
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; };
|
E2A9CB7D2C7BCF2A005C89CC /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = "<group>"; };
|
||||||
E2ADC0292E5794AB00B4FF88 /* RouteOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteOverview.swift; sourceTree = "<group>"; };
|
E2ADC0292E5794AB00B4FF88 /* RouteOverview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteOverview.swift; sourceTree = "<group>"; };
|
||||||
E2ADC02B2E5795F000B4FF88 /* ElevationGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElevationGraph.swift; sourceTree = "<group>"; };
|
E2ADC02B2E5795F000B4FF88 /* ElevationGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElevationGraph.swift; sourceTree = "<group>"; };
|
||||||
|
E2ADC02D2E57CC6500B4FF88 /* Double+Rounded.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Rounded.swift"; sourceTree = "<group>"; };
|
||||||
E2B482022D5D132D005C309D /* WebServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebServer.swift; sourceTree = "<group>"; };
|
E2B482022D5D132D005C309D /* WebServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebServer.swift; sourceTree = "<group>"; };
|
||||||
E2B482042D5E7D4A005C309D /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
|
E2B482042D5E7D4A005C309D /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = "<group>"; };
|
||||||
E2B482082D5E7F4C005C309D /* WebsitePreviewSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsitePreviewSheet.swift; sourceTree = "<group>"; };
|
E2B482082D5E7F4C005C309D /* WebsitePreviewSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebsitePreviewSheet.swift; sourceTree = "<group>"; };
|
||||||
@@ -1074,6 +1076,7 @@
|
|||||||
E2B85F552C4BD0AD0047CD0C /* Extensions */ = {
|
E2B85F552C4BD0AD0047CD0C /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
E2ADC02D2E57CC6500B4FF88 /* Double+Rounded.swift */,
|
||||||
E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */,
|
E2FE0F1F2D29A709002963B7 /* Array+Remove.swift */,
|
||||||
E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */,
|
E2FE0EE72D16D4A3002963B7 /* ConvertThrowing.swift */,
|
||||||
E25DA5182CFF035200AEF16D /* Array+Split.swift */,
|
E25DA5182CFF035200AEF16D /* Array+Split.swift */,
|
||||||
@@ -1381,6 +1384,7 @@
|
|||||||
files = (
|
files = (
|
||||||
E2FD1D562D46CED900B48627 /* Insert+Labels.swift in Sources */,
|
E2FD1D562D46CED900B48627 /* Insert+Labels.swift in Sources */,
|
||||||
E29D31242D0366860051B7F4 /* TagList.swift in Sources */,
|
E29D31242D0366860051B7F4 /* TagList.swift in Sources */,
|
||||||
|
E2ADC02E2E57CC6900B4FF88 /* Double+Rounded.swift in Sources */,
|
||||||
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */,
|
E22990282D0F596C009F8D77 /* IntegerPropertyView.swift in Sources */,
|
||||||
E2FD1D1D2D2DE31800B48627 /* ItemType.swift in Sources */,
|
E2FD1D1D2D2DE31800B48627 /* ItemType.swift in Sources */,
|
||||||
E2FE0F482D2BC7D1002963B7 /* MarkdownProcessor.swift in Sources */,
|
E2FE0F482D2BC7D1002963B7 /* MarkdownProcessor.swift in Sources */,
|
||||||
|
|||||||
7
CHDataManagement/Extensions/Double+Rounded.swift
Normal file
7
CHDataManagement/Extensions/Double+Rounded.swift
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
extension Double {
|
||||||
|
|
||||||
|
func rounded(to interval: Double) -> Double {
|
||||||
|
(self / interval).rounded() * interval
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -298,6 +298,18 @@ final class FileResource: Item, LocalizedItem {
|
|||||||
return content.settings.general.url + version.outputPath
|
return content.settings.general.url + version.outputPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: Workout
|
||||||
|
|
||||||
|
var routeOverview: RouteOverview? {
|
||||||
|
guard type == .route else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let data = dataContent() else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return try? WorkoutData(data: data).overview
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: Video thumbnail
|
// MARK: Video thumbnail
|
||||||
|
|
||||||
func createVideoThumbnail() {
|
func createVideoThumbnail() {
|
||||||
|
|||||||
@@ -77,6 +77,45 @@ final class LocalizedPost: ChangeObservingItem {
|
|||||||
var hasVideos: Bool {
|
var hasVideos: Bool {
|
||||||
images.contains { $0.type.isVideo }
|
images.contains { $0.type.isVideo }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateLabels(from workout: RouteOverview, locale: Locale) {
|
||||||
|
insertOrReplace(label: .init(icon: .statisticsDistance, value: String(format: "%.1f km", locale: locale, workout.distance / 1000)))
|
||||||
|
insertOrReplace(label: .init(icon: .statisticsTime, value: workout.duration.duration(locale: locale)))
|
||||||
|
insertOrReplace(label: .init(icon: .statisticsElevationUp, value: workout.ascendedElevation.length(roundingToNearest: 50)))
|
||||||
|
insertOrReplace(label: .init(icon: .statisticsEnergy, value: workout.energy.energy(roundingToNearest: 50)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func insertOrReplace(label: ContentLabel) {
|
||||||
|
if let index = labels.firstIndex(where: { $0.icon == label.icon }) {
|
||||||
|
labels[index] = label
|
||||||
|
} else {
|
||||||
|
labels.append(label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension TimeInterval {
|
||||||
|
|
||||||
|
func duration(locale: Locale) -> String {
|
||||||
|
let totalMinutes = Int((self / 60).rounded(to: 5))
|
||||||
|
let hours = totalMinutes / 60
|
||||||
|
let minutes = totalMinutes % 60
|
||||||
|
|
||||||
|
let suffix = locale.identifier.hasPrefix("de") ? "Std" : "h"
|
||||||
|
|
||||||
|
return String(format: "%d:%02d ", hours, minutes) + suffix
|
||||||
|
}
|
||||||
|
|
||||||
|
func length(roundingToNearest interval: Double) -> String {
|
||||||
|
let rounded = Int(self.rounded(to: interval))
|
||||||
|
return "\(rounded) m"
|
||||||
|
}
|
||||||
|
|
||||||
|
func energy(roundingToNearest interval: Double) -> String {
|
||||||
|
let rounded = Int(self.rounded(to: interval))
|
||||||
|
return "\(rounded) kcal"
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Storage
|
// MARK: Storage
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ final class Post: Item, DateItem, LocalizedItem {
|
|||||||
@Published
|
@Published
|
||||||
var linkedPage: Page?
|
var linkedPage: Page?
|
||||||
|
|
||||||
|
/// The workout associated with the post
|
||||||
|
@Published
|
||||||
|
var associatedWorkout: FileResource?
|
||||||
|
|
||||||
init(content: Content,
|
init(content: Content,
|
||||||
id: String,
|
id: String,
|
||||||
isDraft: Bool,
|
isDraft: Bool,
|
||||||
@@ -47,7 +51,8 @@ final class Post: Item, DateItem, LocalizedItem {
|
|||||||
tags: [Tag],
|
tags: [Tag],
|
||||||
german: LocalizedPost,
|
german: LocalizedPost,
|
||||||
english: LocalizedPost,
|
english: LocalizedPost,
|
||||||
linkedPage: Page? = nil) {
|
linkedPage: Page? = nil,
|
||||||
|
associatedWorkout: FileResource? = nil) {
|
||||||
self.isDraft = isDraft
|
self.isDraft = isDraft
|
||||||
self.createdDate = createdDate
|
self.createdDate = createdDate
|
||||||
self.startDate = startDate
|
self.startDate = startDate
|
||||||
@@ -57,6 +62,7 @@ final class Post: Item, DateItem, LocalizedItem {
|
|||||||
self.german = german
|
self.german = german
|
||||||
self.english = english
|
self.english = english
|
||||||
self.linkedPage = linkedPage
|
self.linkedPage = linkedPage
|
||||||
|
self.associatedWorkout = associatedWorkout
|
||||||
super.init(content: content, id: id)
|
super.init(content: content, id: id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,6 +180,14 @@ final class Post: Item, DateItem, LocalizedItem {
|
|||||||
english: english,
|
english: english,
|
||||||
tags: tags)
|
tags: tags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func updateLabelsFromWorkout() {
|
||||||
|
guard let overview = associatedWorkout?.routeOverview else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
german.updateLabels(from: overview, locale: Locale(identifier: "de_DE"))
|
||||||
|
english.updateLabels(from: overview, locale: Locale(identifier: "en_US"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Post: StorageItem {
|
extension Post: StorageItem {
|
||||||
@@ -189,7 +203,8 @@ extension Post: StorageItem {
|
|||||||
tags: data.tags.compactMap(context.tag),
|
tags: data.tags.compactMap(context.tag),
|
||||||
german: .init(context: context, data: data.german),
|
german: .init(context: context, data: data.german),
|
||||||
english: .init(context: context, data: data.english),
|
english: .init(context: context, data: data.english),
|
||||||
linkedPage: data.linkedPageId.map(context.page))
|
linkedPage: data.linkedPageId.map(context.page),
|
||||||
|
associatedWorkout: data.associatedWorkoutId.map(context.file))
|
||||||
savedData = data
|
savedData = data
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +217,7 @@ extension Post: StorageItem {
|
|||||||
let german: LocalizedPost.Data
|
let german: LocalizedPost.Data
|
||||||
let english: LocalizedPost.Data
|
let english: LocalizedPost.Data
|
||||||
let linkedPageId: String?
|
let linkedPageId: String?
|
||||||
|
let associatedWorkoutId: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
var data: Data {
|
var data: Data {
|
||||||
@@ -213,7 +229,8 @@ extension Post: StorageItem {
|
|||||||
tags: tags.map { $0.identifier },
|
tags: tags.map { $0.identifier },
|
||||||
german: german.data,
|
german: german.data,
|
||||||
english: english.data,
|
english: english.data,
|
||||||
linkedPageId: linkedPage?.identifier)
|
linkedPageId: linkedPage?.identifier,
|
||||||
|
associatedWorkoutId: associatedWorkout?.identifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveToDisk(_ data: Data) -> Bool {
|
func saveToDisk(_ data: Data) -> Bool {
|
||||||
|
|||||||
@@ -28,7 +28,8 @@ struct PostContentView: View {
|
|||||||
TagDisplayView(tags: $post.tags)
|
TagDisplayView(tags: $post.tags)
|
||||||
}
|
}
|
||||||
PostLabelsView(
|
PostLabelsView(
|
||||||
post: localized,
|
post: post,
|
||||||
|
localized: localized,
|
||||||
other: other)
|
other: other)
|
||||||
PostTextView(post: localized)
|
PostTextView(post: localized)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,12 @@ struct PostDetailView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FilePropertyView(
|
||||||
|
title: "Associated workout",
|
||||||
|
footer: "The workout file to display with this post",
|
||||||
|
selectedFile: $post.associatedWorkout,
|
||||||
|
allowedType: .route)
|
||||||
|
|
||||||
LocalizedPostDetailView(
|
LocalizedPostDetailView(
|
||||||
post: post.localized(in: language),
|
post: post.localized(in: language),
|
||||||
transferImage: transferImage)
|
transferImage: transferImage)
|
||||||
|
|||||||
@@ -3,7 +3,10 @@ import SwiftUI
|
|||||||
struct PostLabelsView: View {
|
struct PostLabelsView: View {
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var post: LocalizedPost
|
var post: Post
|
||||||
|
|
||||||
|
@ObservedObject
|
||||||
|
var localized: LocalizedPost
|
||||||
|
|
||||||
@ObservedObject
|
@ObservedObject
|
||||||
var other: LocalizedPost
|
var other: LocalizedPost
|
||||||
@@ -17,11 +20,11 @@ struct PostLabelsView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView(.horizontal) {
|
ScrollView(.horizontal) {
|
||||||
HStack(spacing: 5) {
|
HStack(spacing: 5) {
|
||||||
if post.labels.isEmpty {
|
if localized.labels.isEmpty {
|
||||||
Text("Labels")
|
Text("Labels")
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
}
|
}
|
||||||
ForEach(post.labels) { label in
|
ForEach(localized.labels) { label in
|
||||||
HStack {
|
HStack {
|
||||||
PageIconView(icon: label.icon)
|
PageIconView(icon: label.icon)
|
||||||
.frame(maxWidth: 16, maxHeight: 16)
|
.frame(maxWidth: 16, maxHeight: 16)
|
||||||
@@ -45,16 +48,16 @@ struct PostLabelsView: View {
|
|||||||
}.buttonStyle(.plain)
|
}.buttonStyle(.plain)
|
||||||
if !other.labels.isEmpty {
|
if !other.labels.isEmpty {
|
||||||
Button("Transfer") {
|
Button("Transfer") {
|
||||||
post.labels = other.labels.map {
|
localized.labels = other.labels.map {
|
||||||
// Copy instead of reference
|
// Copy instead of reference
|
||||||
ContentLabel(icon: $0.icon, value: $0.value)
|
ContentLabel(icon: $0.icon, value: $0.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !post.labels.isEmpty {
|
if !localized.labels.isEmpty {
|
||||||
Button("Copy") {
|
Button("Copy") {
|
||||||
var command = "```labels"
|
var command = "```labels"
|
||||||
for label in post.labels {
|
for label in localized.labels {
|
||||||
command += "\n\(label.icon.rawValue): \(label.value)"
|
command += "\n\(label.icon.rawValue): \(label.value)"
|
||||||
}
|
}
|
||||||
command += "\n```"
|
command += "\n```"
|
||||||
@@ -63,23 +66,28 @@ struct PostLabelsView: View {
|
|||||||
pasteboard.setString(command, forType: .string)
|
pasteboard.setString(command, forType: .string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let workout = post.associatedWorkout {
|
||||||
|
Button("From workout") {
|
||||||
|
post.updateLabelsFromWorkout()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showLabelEditor) {
|
.sheet(isPresented: $showLabelEditor) {
|
||||||
LabelModificationView(labels: $post.labels)
|
LabelModificationView(labels: $localized.labels)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addLabel() {
|
func addLabel() {
|
||||||
post.labels.append(.init(icon: .clockFill, value: "Value"))
|
localized.labels.append(.init(icon: .clockFill, value: "Value"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func remove(_ label: ContentLabel) {
|
func remove(_ label: ContentLabel) {
|
||||||
guard let index = post.labels.firstIndex(of: label) else {
|
guard let index = localized.labels.firstIndex(of: label) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
post.labels.remove(at: index)
|
localized.labels.remove(at: index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user