diff --git a/LoopKit.xcodeproj/project.pbxproj b/LoopKit.xcodeproj/project.pbxproj index dcf0d7a99..db6b069da 100644 --- a/LoopKit.xcodeproj/project.pbxproj +++ b/LoopKit.xcodeproj/project.pbxproj @@ -298,6 +298,22 @@ 84AAB1E52C347EEA0054D304 /* Environment+InvestigationalDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AAB1E42C347EEA0054D304 /* Environment+InvestigationalDevice.swift */; }; 84CB9CDE2C0FD94B007210DD /* LoopCompletionFreshnessTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CB9CDD2C0FD94B007210DD /* LoopCompletionFreshnessTests.swift */; }; 84DF48892C33218100844FB1 /* MuteAllAppSoundsDurationSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48882C33217400844FB1 /* MuteAllAppSoundsDurationSheetView.swift */; }; + 84DF48E32F6C6B2100BEDB40 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48DF2F6C6B2100BEDB40 /* Metadata.swift */; }; + 84DF48E42F6C6B2100BEDB40 /* MediaContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48E02F6C6B2100BEDB40 /* MediaContent.swift */; }; + 84DF48E52F6C6B2100BEDB40 /* TranscriptParagraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48DC2F6C6B2100BEDB40 /* TranscriptParagraph.swift */; }; + 84DF48E62F6C6B2100BEDB40 /* ClosedCaptionFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48E22F6C6B2100BEDB40 /* ClosedCaptionFragment.swift */; }; + 84DF48E72F6C6B2100BEDB40 /* TranscriptExcerpt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48DD2F6C6B2100BEDB40 /* TranscriptExcerpt.swift */; }; + 84DF48E82F6C6B2100BEDB40 /* ClosedCaptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48E12F6C6B2100BEDB40 /* ClosedCaptions.swift */; }; + 84DF48E92F6C6B2100BEDB40 /* Transcript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48DE2F6C6B2100BEDB40 /* Transcript.swift */; }; + 84DF48EA2F6C6B2100BEDB40 /* Metadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48DF2F6C6B2100BEDB40 /* Metadata.swift */; }; + 84DF48EB2F6C6B2100BEDB40 /* MediaContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48E02F6C6B2100BEDB40 /* MediaContent.swift */; }; + 84DF48EC2F6C6B2100BEDB40 /* TranscriptParagraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48DC2F6C6B2100BEDB40 /* TranscriptParagraph.swift */; }; + 84DF48ED2F6C6B2100BEDB40 /* ClosedCaptionFragment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48E22F6C6B2100BEDB40 /* ClosedCaptionFragment.swift */; }; + 84DF48EE2F6C6B2100BEDB40 /* TranscriptExcerpt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48DD2F6C6B2100BEDB40 /* TranscriptExcerpt.swift */; }; + 84DF48EF2F6C6B2100BEDB40 /* ClosedCaptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48E12F6C6B2100BEDB40 /* ClosedCaptions.swift */; }; + 84DF48F02F6C6B2100BEDB40 /* Transcript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48DE2F6C6B2100BEDB40 /* Transcript.swift */; }; + 84DF48F22F6C6B9000BEDB40 /* TimeInterval+Timecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48F12F6C6B9000BEDB40 /* TimeInterval+Timecode.swift */; }; + 84DF48F32F6C6B9000BEDB40 /* TimeInterval+Timecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DF48F12F6C6B9000BEDB40 /* TimeInterval+Timecode.swift */; }; 84E8BBBE2CC9976E0078E6CF /* BulletedListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8BBBD2CC9976E0078E6CF /* BulletedListView.swift */; }; 84EE97812D71293E00D5E941 /* GlucoseHistoryLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EE97802D71293E00D5E941 /* GlucoseHistoryLayer.swift */; }; 84EE97B12D7A42DB00D5E941 /* ChartPointsScatterBorderedCirclesLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84EE97B02D7A42DB00D5E941 /* ChartPointsScatterBorderedCirclesLayer.swift */; }; @@ -1344,6 +1360,14 @@ 84AAB1E42C347EEA0054D304 /* Environment+InvestigationalDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Environment+InvestigationalDevice.swift"; sourceTree = ""; }; 84CB9CDD2C0FD94B007210DD /* LoopCompletionFreshnessTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoopCompletionFreshnessTests.swift; sourceTree = ""; }; 84DF48882C33217400844FB1 /* MuteAllAppSoundsDurationSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteAllAppSoundsDurationSheetView.swift; sourceTree = ""; }; + 84DF48DC2F6C6B2100BEDB40 /* TranscriptParagraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptParagraph.swift; sourceTree = ""; }; + 84DF48DD2F6C6B2100BEDB40 /* TranscriptExcerpt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TranscriptExcerpt.swift; sourceTree = ""; }; + 84DF48DE2F6C6B2100BEDB40 /* Transcript.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transcript.swift; sourceTree = ""; }; + 84DF48DF2F6C6B2100BEDB40 /* Metadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Metadata.swift; sourceTree = ""; }; + 84DF48E02F6C6B2100BEDB40 /* MediaContent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaContent.swift; sourceTree = ""; }; + 84DF48E12F6C6B2100BEDB40 /* ClosedCaptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedCaptions.swift; sourceTree = ""; }; + 84DF48E22F6C6B2100BEDB40 /* ClosedCaptionFragment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClosedCaptionFragment.swift; sourceTree = ""; }; + 84DF48F12F6C6B9000BEDB40 /* TimeInterval+Timecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimeInterval+Timecode.swift"; sourceTree = ""; }; 84E8BBBD2CC9976E0078E6CF /* BulletedListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletedListView.swift; sourceTree = ""; }; 84EE97802D71293E00D5E941 /* GlucoseHistoryLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseHistoryLayer.swift; sourceTree = ""; }; 84EE97B02D7A42DB00D5E941 /* ChartPointsScatterBorderedCirclesLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChartPointsScatterBorderedCirclesLayer.swift; sourceTree = ""; }; @@ -2330,6 +2354,7 @@ 437AFF22203BE382008C4892 /* Extensions */ = { isa = PBXGroup; children = ( + 84DF48F12F6C6B9000BEDB40 /* TimeInterval+Timecode.swift */, C187338329B9486200519CDF /* ClosedRange.swift */, C187337F29B9486100519CDF /* Collection.swift */, C187338029B9486100519CDF /* Comparable.swift */, @@ -2486,6 +2511,7 @@ 43D8FDCD1C728FDF0073BE78 /* LoopKit */ = { isa = PBXGroup; children = ( + 84DF48DB2F6C6B1200BEDB40 /* Media */, C15F9A572EB65D3B0082BDF4 /* TestingDate.swift */, 1DA649AA2445174400F61E75 /* Alert.swift */, A96E6C3627B35BC600F81A5B /* AnyCodableEquatable.swift */, @@ -2805,6 +2831,20 @@ path = Presets; sourceTree = ""; }; + 84DF48DB2F6C6B1200BEDB40 /* Media */ = { + isa = PBXGroup; + children = ( + 84DF48DC2F6C6B2100BEDB40 /* TranscriptParagraph.swift */, + 84DF48DD2F6C6B2100BEDB40 /* TranscriptExcerpt.swift */, + 84DF48DE2F6C6B2100BEDB40 /* Transcript.swift */, + 84DF48DF2F6C6B2100BEDB40 /* Metadata.swift */, + 84DF48E02F6C6B2100BEDB40 /* MediaContent.swift */, + 84DF48E12F6C6B2100BEDB40 /* ClosedCaptions.swift */, + 84DF48E22F6C6B2100BEDB40 /* ClosedCaptionFragment.swift */, + ); + path = Media; + sourceTree = ""; + }; 892A5D35222F03CB008961AB /* LoopTestingKit */ = { isa = PBXGroup; children = ( @@ -4164,6 +4204,13 @@ A9498D8223386C3300DAA9B9 /* Service.swift in Sources */, C187338F29B9486200519CDF /* NumberFormatter.swift in Sources */, 4322B785202FA2AF0002837D /* ReservoirValue.swift in Sources */, + 84DF48E32F6C6B2100BEDB40 /* Metadata.swift in Sources */, + 84DF48E42F6C6B2100BEDB40 /* MediaContent.swift in Sources */, + 84DF48E52F6C6B2100BEDB40 /* TranscriptParagraph.swift in Sources */, + 84DF48E62F6C6B2100BEDB40 /* ClosedCaptionFragment.swift in Sources */, + 84DF48E72F6C6B2100BEDB40 /* TranscriptExcerpt.swift in Sources */, + 84DF48E82F6C6B2100BEDB40 /* ClosedCaptions.swift in Sources */, + 84DF48E92F6C6B2100BEDB40 /* Transcript.swift in Sources */, B4A2ABEA2AA9F210007E3EC1 /* BolusActivationType.swift in Sources */, 891A3FD9224BEB4600378B27 /* EGPSchedule.swift in Sources */, 89AE2229228BC54C00BDFD85 /* TemporaryScheduleOverrideHistory.swift in Sources */, @@ -4177,6 +4224,7 @@ 4322B783202FA2AF0002837D /* Reservoir.swift in Sources */, 1DA649AB2445174400F61E75 /* Alert.swift in Sources */, 4322B782202FA2AF0002837D /* PumpEventType.swift in Sources */, + 84DF48F32F6C6B9000BEDB40 /* TimeInterval+Timecode.swift in Sources */, 89AE2228228BC54C00BDFD85 /* TemporaryPresetSettings.swift in Sources */, 43D8FDFA1C7290350073BE78 /* GlucoseRangeSchedule.swift in Sources */, 4322B77E202FA2AF0002837D /* PersistedPumpEvent.swift in Sources */, @@ -4497,6 +4545,13 @@ C1092410286A4ADB00FAD2B8 /* AutomaticDosingStrategy.swift in Sources */, C17F39CB23CD2D2F00FA1113 /* DeviceLogEntryType.swift in Sources */, A9E675AF22713F4700E25293 /* PumpEventType.swift in Sources */, + 84DF48EA2F6C6B2100BEDB40 /* Metadata.swift in Sources */, + 84DF48EB2F6C6B2100BEDB40 /* MediaContent.swift in Sources */, + 84DF48EC2F6C6B2100BEDB40 /* TranscriptParagraph.swift in Sources */, + 84DF48ED2F6C6B2100BEDB40 /* ClosedCaptionFragment.swift in Sources */, + 84DF48EE2F6C6B2100BEDB40 /* TranscriptExcerpt.swift in Sources */, + 84DF48EF2F6C6B2100BEDB40 /* ClosedCaptions.swift in Sources */, + 84DF48F02F6C6B2100BEDB40 /* Transcript.swift in Sources */, C1614F062AAFC36200F636E5 /* CgmEvent.swift in Sources */, C11A17482CB713C10019C517 /* Model.xcdatamodeld in Sources */, C1A174ED23DEAD6A0034DF11 /* DeviceLogEntry+CoreDataProperties.swift in Sources */, @@ -4547,6 +4602,7 @@ A9E675D422713F4700E25293 /* CachedGlucoseObject+CoreDataClass.swift in Sources */, A9498D7F23386C3300DAA9B9 /* GlucoseThreshold.swift in Sources */, A9E675D522713F4700E25293 /* WalshInsulinModel.swift in Sources */, + 84DF48F22F6C6B9000BEDB40 /* TimeInterval+Timecode.swift in Sources */, A9A53E2D2714E5BC0050C0B1 /* CodableDevice.swift in Sources */, 840A2DD42E399BD400D4E245 /* Modelv5ToModelv6.xcmappingmodel in Sources */, B40C43912707408400F5D86C /* DeliveryLimits.swift in Sources */, diff --git a/LoopKit/Extensions/TimeInterval+Timecode.swift b/LoopKit/Extensions/TimeInterval+Timecode.swift new file mode 100644 index 000000000..59fce2e1a --- /dev/null +++ b/LoopKit/Extensions/TimeInterval+Timecode.swift @@ -0,0 +1,63 @@ +// +// TimeInterval+Timecode.swift +// Loop +// +// Created by Cameron Ingham on 2/27/25. +// + +import Foundation + +public extension TimeInterval { + enum TimecodeStyle { + case caption + case transcript + + public var separator: String { + switch self { + case .caption: + return ":," + case .transcript: + return ":" + } + } + } + + init?(timecode: String, style: TimecodeStyle) { + self.init(timecode: timecode, separator: style.separator) + } + + init?(timecode: String, separator: String) { + let components = timecode.components(separatedBy: CharacterSet(charactersIn: separator)) + + guard components.count >= 3, + let hours = Int(components[0]), + let minutes = Int(components[1]), + let seconds = Int(components[2]) else { + return nil + } + + let totalSeconds = Double(hours * 3600 + minutes * 60 + seconds) + + var totalMilliseconds: Double = 0 + if components.count > 3, let milliseconds = Int(components[3]) { + totalMilliseconds = Double(milliseconds) / 1000.0 + } + + self = totalSeconds + totalMilliseconds + } + + func timecode(for style: TimecodeStyle) -> String { + let totalSeconds = Int(self) + let milliseconds = Int((self.truncatingRemainder(dividingBy: 1)) * 1000) + let seconds = totalSeconds % 60 + let minutes = (totalSeconds % 3600) / 60 + let hours = totalSeconds / 3600 + + switch style { + case .caption: + return String(format: "%02d:%02d:%02d,%03d", hours, minutes, seconds, milliseconds) + case .transcript: + return String(format: "%02d:%02d:%02d", hours, minutes, seconds) + } + } +} diff --git a/LoopKit/Media/ClosedCaptionFragment.swift b/LoopKit/Media/ClosedCaptionFragment.swift new file mode 100644 index 000000000..9064f7226 --- /dev/null +++ b/LoopKit/Media/ClosedCaptionFragment.swift @@ -0,0 +1,33 @@ +// +// ClosedCaptionFragment.swift +// Loop +// +// Created by Cameron Ingham on 2/27/25. +// + +import Foundation + +public struct ClosedCaptionFragment: Equatable, Hashable, RawRepresentable { + + public let sequenceNumber: Int + public let startTime: TimeInterval + public let endTime: TimeInterval + public let text: String + + public var rawValue: String { + """ + \(sequenceNumber) + \(String(describing: startTime.timecode)) --> \(String(describing: endTime.timecode)) + \(text) + """ + } + + public init?(rawValue: String) { + let rawFragments = rawValue.split(separator: "\n") + self.sequenceNumber = Int(rawFragments[0])! + let timecodes = rawFragments[1].split(separator: " --> ") + self.startTime = TimeInterval(timecode: String(timecodes[0]), style: .caption)! + self.endTime = TimeInterval(timecode: String(timecodes[1]), style: .caption)! + self.text = String(rawFragments[2]) + } +} diff --git a/LoopKit/Media/ClosedCaptions.swift b/LoopKit/Media/ClosedCaptions.swift new file mode 100644 index 000000000..0aef87069 --- /dev/null +++ b/LoopKit/Media/ClosedCaptions.swift @@ -0,0 +1,42 @@ +// +// ClosedCaptions.swift +// Loop +// +// Created by Cameron Ingham on 2/27/25. +// + +import Foundation + +public struct ClosedCaptions: Equatable, Hashable, RawRepresentable { + + public var fragments: [ClosedCaptionFragment] + + public var rawValue: String { + fragments.map(\.rawValue).joined(separator: "\n\n") + } + + public init(fragments: [ClosedCaptionFragment]) { + self.fragments = fragments + } + + public init(url: URL) { + guard let data = try? Data(contentsOf: url) else { + assertionFailure("Could not generate data from file at URL: \(url.absoluteString)") + self.init(fragments: []) + return + } + + let rawValue = String(data: data, encoding: .utf8)! + self.init(rawValue: rawValue) + } + + public init(rawValue: String) { + self.fragments = rawValue.split(separator: "\n\n").compactMap { + ClosedCaptionFragment(rawValue: String($0)) + } + } + + public func currentFragment(at timecode: TimeInterval) -> ClosedCaptionFragment? { + fragments.first(where: { timecode >= $0.startTime && timecode < $0.endTime }) + } +} diff --git a/LoopKit/Media/MediaContent.swift b/LoopKit/Media/MediaContent.swift new file mode 100644 index 000000000..ff8755862 --- /dev/null +++ b/LoopKit/Media/MediaContent.swift @@ -0,0 +1,49 @@ +// +// MediaContent.swift +// Loop +// +// Created by Cameron Ingham on 2/27/25. +// + +import AVFoundation +import Foundation + +public struct MediaContent: Equatable, Hashable, Identifiable { + + public struct StaticImage: Hashable { + public let name: String + public let bundle: Bundle + } + + public let fileName: String + public let metadata: Metadata + public let staticImage: StaticImage + public let animation: URL + public let audio: URL + public let transcript: Transcript? + public let closedCaptions: ClosedCaptions + + public let asset: AVAsset + + public init(_ name: String, bundle: Bundle?) { + self.fileName = name + self.metadata = Metadata(url: (bundle ?? Bundle.main).url(forResource: name, withExtension: "json")!)! + self.staticImage = StaticImage(name: name, bundle: bundle ?? Bundle.main) + self.animation = (bundle ?? Bundle.main).url(forResource: name, withExtension: "mp4")! + self.audio = (bundle ?? Bundle.main).url(forResource: name, withExtension: "mp3")! + self.transcript = Transcript(url: (bundle ?? Bundle.main).url(forResource: name, withExtension: "txt")!) + self.closedCaptions = ClosedCaptions(url: (bundle ?? Bundle.main).url(forResource: name, withExtension: "srt")!) + + self.asset = AVAsset(url: audio) + } + + public var duration: TimeInterval { + get async throws { + try await asset.load(.duration).seconds + } + } + + public var id: Int { + hashValue + } +} diff --git a/LoopKit/Media/Metadata.swift b/LoopKit/Media/Metadata.swift new file mode 100644 index 000000000..90bc382f9 --- /dev/null +++ b/LoopKit/Media/Metadata.swift @@ -0,0 +1,24 @@ +// +// Metadata.swift +// Loop +// +// Created by Cameron Ingham on 8/11/25. +// + +import Foundation + +public struct Metadata: Equatable, Hashable, Decodable { + public let title: String + public let author: String + + public init?(url: URL) { + do { + let metadata = try JSONDecoder().decode(Self.self, from: Data(contentsOf: url)) + self.title = metadata.title + self.author = metadata.author + } catch { + print(error.localizedDescription) + return nil + } + } +} diff --git a/LoopKit/Media/Transcript.swift b/LoopKit/Media/Transcript.swift new file mode 100644 index 000000000..716a74d7c --- /dev/null +++ b/LoopKit/Media/Transcript.swift @@ -0,0 +1,42 @@ +// +// Transcript.swift +// Loop +// +// Created by Cameron Ingham on 7/16/25. +// + +import Foundation + +public struct Transcript: Equatable, Hashable, RawRepresentable { + + public let paragraphs: [TranscriptParagraph] + + public var rawValue: String { + paragraphs.map(\.rawValue).joined(separator: "\n\n") + } + + public init(paragraphs: [TranscriptParagraph]) { + self.paragraphs = paragraphs + } + + public init(rawValue: String) { + self.paragraphs = rawValue.split(separator: "\n\n").compactMap { + TranscriptParagraph(rawValue: String($0)) + } + } + + public init(url: URL) { + guard let data = try? Data(contentsOf: url) else { + assertionFailure("Could not generate data from file at URL: \(url.absoluteString)") + self.init(paragraphs: []) + return + } + + let rawValue = String(data: data, encoding: .utf8)! + self.init(rawValue: rawValue) + } + + public func currentExcerpt(at timecode: TimeInterval) -> TranscriptExcerpt { + paragraphs.flatMap(\.excerpts).last(where: { $0.startTime <= timecode }) ?? TranscriptExcerpt(startTime: 0, text: "") + } +} diff --git a/LoopKit/Media/TranscriptExcerpt.swift b/LoopKit/Media/TranscriptExcerpt.swift new file mode 100644 index 000000000..d50220410 --- /dev/null +++ b/LoopKit/Media/TranscriptExcerpt.swift @@ -0,0 +1,29 @@ +// +// TranscriptExcerpt.swift +// Loop +// +// Created by Cameron Ingham on 7/16/25. +// + +import Foundation + +public struct TranscriptExcerpt: Equatable, Hashable, RawRepresentable { + + public let startTime: TimeInterval + public let text: String + + public var rawValue: String { + "[\(startTime.timecode(for: .transcript))] \(text)" + } + + public init(startTime: TimeInterval, text: String) { + self.startTime = startTime + self.text = text + } + + public init?(rawValue: String) { + let fragments = rawValue.dropFirst().split(separator: "] ") + self.startTime = TimeInterval(timecode: String(fragments[0]), style: .transcript)! + self.text = String(fragments[1]) + } +} diff --git a/LoopKit/Media/TranscriptParagraph.swift b/LoopKit/Media/TranscriptParagraph.swift new file mode 100644 index 000000000..480ea7043 --- /dev/null +++ b/LoopKit/Media/TranscriptParagraph.swift @@ -0,0 +1,37 @@ +// +// TranscriptParagraph.swift +// Loop +// +// Created by Cameron Ingham on 7/16/25. +// + +import Foundation + +public struct TranscriptParagraph: Equatable, Hashable, RawRepresentable { + + public let excerpts: [TranscriptExcerpt] + + public var rawValue: String { + excerpts.map(\.rawValue).joined(separator: " ") + } + + public init(excepts: [TranscriptExcerpt]) { + self.excerpts = excepts + } + + public init(rawValue: String) { + self.init( + excepts: rawValue + .split(separator: " [") + .map({ + let string = String($0) + if !string.hasPrefix("[") { + return "[\(string)" + } else { + return string + } + }) + .compactMap({ TranscriptExcerpt(rawValue: $0) }) + ) + } +} diff --git a/LoopKitUI/SupportUI.swift b/LoopKitUI/SupportUI.swift index cab4e96d5..07ad8e0cd 100644 --- a/LoopKitUI/SupportUI.swift +++ b/LoopKitUI/SupportUI.swift @@ -68,6 +68,10 @@ public struct DeviceWhitelist: Hashable { } } +public enum TrainingMediaDomain: Hashable { + case presets +} + @MainActor public protocol SupportUI: Pluggable { @@ -117,6 +121,9 @@ public protocol SupportUI: Pluggable { /// Use this to restore any values that were cached before a reset occurred func loopDidReset() + /// Provides playable media to Loop to be used in training new features + func trainingMedia(for domain: TrainingMediaDomain) -> [MediaContent] + /// Initializes the support with the previously-serialized state. /// /// - Parameters: diff --git a/MockKitUI/MockSupport.swift b/MockKitUI/MockSupport.swift index 2c1201450..43b7abf7d 100644 --- a/MockKitUI/MockSupport.swift +++ b/MockKitUI/MockSupport.swift @@ -65,6 +65,8 @@ public class MockSupport: SupportUI { public func loopWillReset() {} public func loopDidReset() {} + + public func trainingMedia(for domain: TrainingMediaDomain) -> [MediaContent] { [] } } extension MockSupport {