Add tappable description links and timestamps in iOS

This commit is contained in:
Arkadiusz Fal 2022-08-19 23:55:02 +02:00
parent eeda7a5c6e
commit 97fc8fa4b7
14 changed files with 308 additions and 70 deletions

View File

@ -7,4 +7,29 @@ extension String {
}
return replacingCharacters(in: range, with: replacement)
}
func replacingMatches(regex: String, replacementStringClosure: (String) -> String?) -> String {
guard let regex = try? NSRegularExpression(pattern: regex) else {
return self
}
let results = regex.matches(in: self, range: NSRange(startIndex..., in: self))
var outputText = self
results.reversed().forEach { match in
(1 ..< match.numberOfRanges).reversed().forEach { rangeIndex in
let matchingGroup: String = (self as NSString).substring(with: match.range(at: rangeIndex))
let rangeBounds = match.range(at: rangeIndex)
guard let range = Range(rangeBounds, in: self) else {
return
}
let replacement = replacementStringClosure(matchingGroup) ?? matchingGroup
outputText = outputText.replacingOccurrences(of: matchingGroup, with: replacement, range: range)
}
}
return outputText
}
}

View File

@ -43,9 +43,25 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier {
private var player: PlayerModel {
let player = PlayerModel()
player.currentItem = PlayerQueueItem(Video(videoID: "", title: "", author: "", length: 0, published: "2 days ago", views: 43434, channel: .init(id: "", name: ""), likes: 2332, dislikes: 30))
player.currentItem = PlayerQueueItem(
Video(
videoID: "",
title: "",
author: "",
length: 0,
published: "2 days ago",
views: 43434,
description: "The 14\" and 16\" MacBook Pros are incredible. I can finally retire the travel iMac.\nThat shirt! http://shop.MKBHD.com\nMacBook Pro skins: https://dbrand.com/macbooks\n\n0:00 Intro\n1:38 Top Notch Design\n2:27 Let's Talk Ports\n7:11 RIP Touchbar\n8:20 The new displays\n10:12 Living with the notch\n12:37 Performance\n19:39 Battery\n20:30 So should you get it?\n\nThe Verge Review: https://youtu.be/ftU1HzBKd5Y\nTyler Stalman Review: https://youtu.be/I10WMJV96ns\nDeveloper's tweet: https://twitter.com/softwarejameson/status/1455971162060697613?s=09&t=WbOkVKgDdcegIdyOdurSNQ&utm_source=pocket_mylist\n\nTech I'm using right now: https://www.amazon.com/shop/MKBHD\n\nIntro Track: http://youtube.com/20syl\nPlaylist of MKBHD Intro music: https://goo.gl/B3AWV5\n\nLaptop provided by Apple for review.\n\n~\nhttp://twitter.com/MKBHD\nhttp://instagram.com/MKBHD\nhttp://facebook.com/MKBHD",
channel: .init(id: "", name: "Channel Name"),
likes: 2332,
dislikes: 30,
keywords: ["Video", "Computer", "Long Long Keyword"]
)
)
#if os(iOS)
player.playerSize = .init(width: UIScreen.main.bounds.width, height: UIScreen.main.bounds.height)
#endif
player.queue = Video.allFixtures.map { PlayerQueueItem($0) }
player.videoBeingOpened = Video.fixture
return player
}

View File

@ -499,6 +499,22 @@ final class PipedAPI: Service, ObservableObject, VideosAPI {
range: nil
)
let linkRegex = #"(<a\s+(?:[^>]*?\s+)?href=\"[^"]*\">[^<]*<\/a>)"#
let hrefRegex = #"href=\"([^"]*)\">"#
guard let hrefRegex = try? NSRegularExpression(pattern: hrefRegex) else { return description }
description = description.replacingMatches(regex: linkRegex) { matchingGroup in
let results = hrefRegex.matches(in: matchingGroup, range: NSRange(matchingGroup.startIndex..., in: matchingGroup))
if let result = results.first {
if let swiftRange = Range(result.range(at: 1), in: matchingGroup) {
return String(matchingGroup[swiftRange])
}
}
return matchingGroup
}
description = description.replacingOccurrences(
of: "<[^>]+>",
with: "",

View File

@ -96,6 +96,7 @@ final class NavigationModel: ObservableObject {
}
navigation.hideKeyboard()
let presentingPlayer = player.presentingPlayer
player.hide()
navigation.presentingChannel = false
@ -110,11 +111,17 @@ final class NavigationModel: ObservableObject {
navigation.sidebarSectionChanged.toggle()
navigation.tabSelection = .recentlyOpened(recent.tag)
} else {
var delay = 0.0
#if os(iOS)
if presentingPlayer { delay = 1.0 }
#endif
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
withAnimation(.linear(duration: 0.3)) {
navigation.presentingChannel = true
}
}
}
}
static func openChannelPlaylist(
_ playlist: ChannelPlaylist,
@ -134,6 +141,8 @@ final class NavigationModel: ObservableObject {
#endif
navigation.hideKeyboard()
let presentingPlayer = player.presentingPlayer
player.hide()
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
recents.add(recent)
@ -142,12 +151,18 @@ final class NavigationModel: ObservableObject {
navigation.sidebarSectionChanged.toggle()
navigation.tabSelection = .recentlyOpened(recent.tag)
} else {
var delay = 0.0
#if os(iOS)
if presentingPlayer { delay = 1.0 }
#endif
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
withAnimation(.linear(duration: 0.3)) {
navigation.presentingPlaylist = true
}
}
}
}
}
static func openSearchQuery(
_ searchQuery: String?,
@ -156,18 +171,24 @@ final class NavigationModel: ObservableObject {
navigation: NavigationModel,
search: SearchModel
) {
player.hide()
navigation.presentingChannel = false
navigation.presentingPlaylist = false
navigation.tabSelection = .search
navigation.hideKeyboard()
let presentingPlayer = player.presentingPlayer
player.hide()
if let searchQuery = searchQuery {
let recent = RecentItem(from: searchQuery)
recents.add(recent)
DispatchQueue.main.async {
var delay = 0.0
#if os(iOS)
if presentingPlayer { delay = 1.0 }
#endif
DispatchQueue.main.asyncAfter(deadline: .now() + delay) {
search.queryText = searchQuery
search.changeQuery { query in query.query = searchQuery }
}

View File

@ -39,14 +39,14 @@ struct Video: Identifiable, Equatable, Hashable {
init(
id: String? = nil,
videoID: String,
title: String,
author: String,
length: TimeInterval,
published: String,
views: Int,
title: String = "",
author: String = "",
length: TimeInterval = .zero,
published: String = "",
views: Int = 0,
description: String? = nil,
genre: String? = nil,
channel: Channel,
channel: Channel = .init(id: "", name: ""),
thumbnails: [Thumbnail] = [],
indexID: String? = nil,
live: Bool = false,

View File

@ -140,7 +140,6 @@ extension Defaults.Keys {
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
static let visibleSections = Key<Set<VisibleSection>>("visibleSections", default: [.favorites, .subscriptions, .trending, .playlists])
static let videoDetailsPage = Key<VideoDetails.DetailsPage>("videoDetailsPage", default: .info)
#if os(iOS)
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)

View File

@ -101,6 +101,8 @@ struct OpenURLHandler {
Windows.main.open()
#endif
player.videoBeingOpened = Video(videoID: id)
player.playerAPI.video(id)
.load()
.onSuccess { response in

View File

@ -102,8 +102,6 @@ struct PlayerControls: View {
if model.presentingDetailsOverlay {
VideoDetailsOverlay()
.frame(maxWidth: detailsWidth, maxHeight: detailsHeight)
.modifier(ControlBackgroundModifier())
.clipShape(RoundedRectangle(cornerRadius: 4))
.transition(.opacity)
}

View File

@ -6,6 +6,8 @@ struct VideoDetailsOverlay: View {
var body: some View {
VideoDetails(sidebarQueue: false, fullScreen: fullScreenBinding)
.modifier(ControlBackgroundModifier())
.clipShape(RoundedRectangle(cornerRadius: 4))
}
var fullScreenBinding: Binding<Bool> {

View File

@ -0,0 +1,147 @@
#if os(iOS)
import ActiveLabel
#endif
import Defaults
import Foundation
import SwiftUI
struct VideoDescription: View {
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SearchModel> private var search
@Default(.showKeywords) private var showKeywords
var video: Video
var detailsSize: CGSize?
var description: String {
video.description ?? ""
}
var body: some View {
VStack {
#if os(iOS)
ActiveLabelDescriptionRepresentable(description: description, detailsSize: detailsSize)
#else
textDescription
#endif
keywords
}
}
@ViewBuilder var textDescription: some View {
#if !os(iOS)
Group {
if #available(macOS 12, *) {
Text(description)
#if !os(tvOS)
.textSelection(.enabled)
#endif
} else {
Text(description)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.font(.system(size: 14))
.lineSpacing(3)
#endif
}
@ViewBuilder var keywords: some View {
if showKeywords {
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
HStack {
ForEach(video.keywords, id: \.self) { keyword in
Button {
NavigationModel.openSearchQuery(keyword, player: player, recents: recents, navigation: navigation, search: search)
} label: {
HStack(alignment: .center, spacing: 0) {
Text("#")
.font(.system(size: 14).bold())
Text(keyword)
.frame(maxWidth: 500)
}
.font(.caption)
.foregroundColor(.white)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(Color("KeywordBackgroundColor"))
.mask(RoundedRectangle(cornerRadius: 3))
}
.buttonStyle(.plain)
}
}
}
}
}
var showScrollIndicators: Bool {
#if os(macOS)
false
#else
true
#endif
}
}
#if os(iOS)
struct ActiveLabelDescriptionRepresentable: UIViewRepresentable {
var description: String
var detailsSize: CGSize?
@State private var label = ActiveLabel()
@Environment(\.openURL) private var openURL
@EnvironmentObject<PlayerModel> private var player
func makeUIView(context _: Context) -> some UIView {
customizeLabel()
return label
}
func updateUIView(_: UIViewType, context _: Context) {
customizeLabel()
}
func customizeLabel() {
label.customize { label in
label.enabledTypes = [.url, .timestamp]
label.numberOfLines = 0
label.text = description
label.contentMode = .scaleAspectFill
label.font = .systemFont(ofSize: 14)
label.lineSpacing = 3
label.preferredMaxLayoutWidth = (detailsSize?.width ?? 330) - 30
label.URLColor = UIColor(Color.accentColor)
label.timestampColor = UIColor(Color.accentColor)
label.handleURLTap { url in
var urlToOpen = url
if var components = URLComponents(url: url, resolvingAgainstBaseURL: false) {
components.scheme = "yattee"
if let yatteeURL = components.url,
URLParser(url: urlToOpen).destination != nil
{
urlToOpen = yatteeURL
}
}
openURL(urlToOpen)
}
label.handleTimestampTap { timestamp in
player.backend.seek(to: timestamp.timeInterval)
}
}
}
}
#endif
struct VideoDescription_Previews: PreviewProvider {
static var previews: some View {
VideoDescription(video: .fixture)
.injectFixtureEnvironmentObjects()
}
}

View File

@ -33,6 +33,7 @@ struct VideoDetails: View {
@StateObject private var page: Page = .first()
@Environment(\.navigationStyle) private var navigationStyle
@Environment(\.verticalSizeClass) private var verticalSizeClass
@EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<CommentsModel> private var comments
@ -41,8 +42,6 @@ struct VideoDetails: View {
@EnvironmentObject<RecentsModel> private var recents
@EnvironmentObject<SubscriptionsModel> private var subscriptions
@Default(.videoDetailsPage) private var videoDetailsPage
@Default(.showKeywords) private var showKeywords
@Default(.playerDetailsPageButtonLabelStyle) private var playerDetailsPageButtonLabelStyle
var currentPage: DetailsPage {
@ -92,12 +91,10 @@ struct VideoDetails: View {
if pageIndex == DetailsPage.comments.index {
comments.load()
}
videoDetailsPage = DetailsPage.allCases.first { $0.index == pageIndex } ?? .info
}
}
.onAppear {
page.update(.new(index: videoDetailsPage.index))
page.update(.moveToFirst)
guard video != nil, accounts.app.supportsSubscriptions else {
subscribed = false
@ -114,11 +111,20 @@ struct VideoDetails: View {
}
}
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .topLeading)
.overlay(GeometryReader { proxy in
Color.clear
.onAppear {
detailsSize = proxy.size
}
.onChange(of: proxy.size) { newSize in
detailsSize = newSize
}
})
}
var publishedDateSection: some View {
Group {
if let video = player.currentVideo {
if let video = video {
HStack(spacing: 4) {
if let published = video.publishedDate {
Text(published)
@ -144,7 +150,6 @@ struct VideoDetails: View {
Button(action: {
page.update(.new(index: destination.index))
pageChangeAction?()
videoDetailsPage = destination
}) {
HStack {
Spacer()
@ -199,10 +204,12 @@ struct VideoDetails: View {
.contentShape(Rectangle())
}
@State private var detailsSize = CGSize.zero
var detailsPage: some View {
Group {
VStack(alignment: .leading, spacing: 0) {
if let video = player.currentVideo {
if let video = video {
VStack(spacing: 6) {
videoProperties
@ -218,47 +225,13 @@ struct VideoDetails: View {
}
}
.redacted(reason: .placeholder)
} else if let description = video.description {
Group {
if #available(iOS 15.0, macOS 12.0, tvOS 15.0, *) {
Text(description)
#if !os(tvOS)
.textSelection(.enabled)
#endif
} else {
Text(description)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.font(.system(size: 14))
.lineSpacing(3)
} else if video.description != nil, !video.description!.isEmpty {
VideoDescription(video: video, detailsSize: detailsSize)
.padding(.bottom, fullScreenLayout ? 10 : SafeArea.insets.bottom)
} else {
Text("No description")
.foregroundColor(.secondary)
}
if showKeywords {
ScrollView(.horizontal, showsIndicators: showScrollIndicators) {
HStack {
ForEach(video.keywords, id: \.self) { keyword in
HStack(alignment: .center, spacing: 0) {
Text("#")
.font(.system(size: 11).bold())
Text(keyword)
.frame(maxWidth: 500)
}
.font(.caption)
.foregroundColor(.white)
.padding(.vertical, 4)
.padding(.horizontal, 8)
.background(Color("KeywordBackgroundColor"))
.mask(RoundedRectangle(cornerRadius: 3))
}
}
.padding(.bottom, 10)
}
}
}
}
}
@ -266,6 +239,14 @@ struct VideoDetails: View {
}
}
var fullScreenLayout: Bool {
#if os(iOS)
return player.playingFullScreen || verticalSizeClass == .compact
#else
return player.playingFullScreen
#endif
}
@ViewBuilder var videoProperties: some View {
HStack(spacing: 2) {
publishedDateSection
@ -319,14 +300,6 @@ struct VideoDetails: View {
.frame(maxWidth: 100)
}
var showScrollIndicators: Bool {
#if os(macOS)
false
#else
true
#endif
}
}
struct VideoDetails_Previews: PreviewProvider {

View File

@ -303,6 +303,11 @@ struct VideoPlayerView: View {
playerSize: player.playerSize,
fullScreen: fullScreenDetails
))
.onDisappear {
if player.presentingPlayer {
player.setNeedsDrawing(true)
}
}
}
#endif
}

View File

@ -528,6 +528,7 @@
379775932689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
379775942689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
379775952689365600DD52A8 /* Array+Next.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379775922689365600DD52A8 /* Array+Next.swift */; };
3799AC0928B03CED001376F9 /* ActiveLabel in Frameworks */ = {isa = PBXBuildFile; productRef = 3799AC0828B03CED001376F9 /* ActiveLabel */; };
379B0253287A1CDF001015B5 /* OrientationTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379B0252287A1CDF001015B5 /* OrientationTracker.swift */; };
379F141F289ECE7F00DE48B5 /* QualitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */; };
379F1420289ECE7F00DE48B5 /* QualitySettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 379F141E289ECE7F00DE48B5 /* QualitySettings.swift */; };
@ -685,6 +686,9 @@
37CF8B8428535E4F00B71E37 /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 37CF8B8328535E4F00B71E37 /* SDWebImage */; };
37CF8B8628535E5A00B71E37 /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 37CF8B8528535E5A00B71E37 /* SDWebImage */; };
37CF8B8828535E6300B71E37 /* SDWebImage in Frameworks */ = {isa = PBXBuildFile; productRef = 37CF8B8728535E6300B71E37 /* SDWebImage */; };
37CFB48528AFE2510070024C /* VideoDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CFB48428AFE2510070024C /* VideoDescription.swift */; };
37CFB48628AFE2510070024C /* VideoDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CFB48428AFE2510070024C /* VideoDescription.swift */; };
37CFB48728AFE2510070024C /* VideoDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37CFB48428AFE2510070024C /* VideoDescription.swift */; };
37D4B0D92671614900C925CA /* Tests_iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0D82671614900C925CA /* Tests_iOS.swift */; };
37D4B0E32671614900C925CA /* Tests_macOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0E22671614900C925CA /* Tests_macOS.swift */; };
37D4B0E42671614900C925CA /* YatteeApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D4B0C22671614700C925CA /* YatteeApp.swift */; };
@ -1187,6 +1191,7 @@
37CC3F4F270D010D00608308 /* VideoBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoBanner.swift; sourceTree = "<group>"; };
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = "<group>"; };
37CEE4C02677B697005A1EFE /* Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = "<group>"; };
37CFB48428AFE2510070024C /* VideoDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDescription.swift; sourceTree = "<group>"; };
37D4B0C22671614700C925CA /* YatteeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YatteeApp.swift; sourceTree = "<group>"; };
37D4B0C32671614700C925CA /* AppTabNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTabNavigation.swift; sourceTree = "<group>"; };
37D4B0C42671614800C925CA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -1287,6 +1292,7 @@
37C2212B27ADA43700305B41 /* VideoToolbox.framework in Frameworks */,
3736A214286BB72300C9E5EE /* libswscale.xcframework in Frameworks */,
3736A216286BB72300C9E5EE /* libavfilter.xcframework in Frameworks */,
3799AC0928B03CED001376F9 /* ActiveLabel in Frameworks */,
3736A218286BB72300C9E5EE /* libharfbuzz.xcframework in Frameworks */,
3736A206286BB72300C9E5EE /* libfreetype.xcframework in Frameworks */,
37C2212927ADA41400305B41 /* CoreMedia.framework in Frameworks */,
@ -1539,6 +1545,7 @@
3795593527B08538007FF8F4 /* StreamControl.swift */,
37B81AFE26D2CA3700675966 /* VideoDetails.swift */,
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */,
37CFB48428AFE2510070024C /* VideoDescription.swift */,
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */,
37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */,
37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */,
@ -2270,6 +2277,7 @@
37A5DBC3285DFF5400CA4DD1 /* SwiftUIPager */,
372AA40F286D067B0000B1DC /* Repeat */,
37EE6DC428A305AD00BFD632 /* Reachability */,
3799AC0828B03CED001376F9 /* ActiveLabel */,
);
productName = "Yattee (iOS)";
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
@ -2480,6 +2488,7 @@
37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */,
372AA40E286D067B0000B1DC /* XCRemoteSwiftPackageReference "Repeat" */,
37EE6DC328A305AD00BFD632 /* XCRemoteSwiftPackageReference "Reachability" */,
3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */,
);
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
projectDirPath = "";
@ -2860,6 +2869,7 @@
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
3784B23B272894DA00B09468 /* ShareSheet.swift in Sources */,
379775932689365600DD52A8 /* Array+Next.swift in Sources */,
37CFB48528AFE2510070024C /* VideoDescription.swift in Sources */,
37B81AFC26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
37C7A1D5267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
37BA794F26DC3E0E002A0235 /* Int+Format.swift in Sources */,
@ -3124,6 +3134,7 @@
37C3A252272366440087A57A /* ChannelPlaylistView.swift in Sources */,
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
37CFB48628AFE2510070024C /* VideoDescription.swift in Sources */,
37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
377ABC41286E4AD5009C986F /* InstancesManifest.swift in Sources */,
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
@ -3236,6 +3247,7 @@
37319F0727103F94004ECCD0 /* PlayerQueue.swift in Sources */,
37E70925271CD43000D34DDE /* WelcomeScreen.swift in Sources */,
376BE50D27349108009AD608 /* BrowsingSettings.swift in Sources */,
37CFB48728AFE2510070024C /* VideoDescription.swift in Sources */,
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
378AE93E274EDFB4006A4EE1 /* Tint+Backport.swift in Sources */,
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */,
@ -4304,6 +4316,14 @@
minimumVersion = 1.5.0;
};
};
3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/yattee/ActiveLabel.swift.git";
requirement = {
branch = feature/timestamp;
kind = branch;
};
};
37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/fermoya/SwiftUIPager.git";
@ -4502,6 +4522,11 @@
package = 3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */;
productName = Siesta;
};
3799AC0828B03CED001376F9 /* ActiveLabel */ = {
isa = XCSwiftPackageProductDependency;
package = 3799AC0728B03CEC001376F9 /* XCRemoteSwiftPackageReference "ActiveLabel.swift" */;
productName = ActiveLabel;
};
37A5DBC3285DFF5400CA4DD1 /* SwiftUIPager */ = {
isa = XCSwiftPackageProductDependency;
package = 37A5DBC2285DFF5400CA4DD1 /* XCRemoteSwiftPackageReference "SwiftUIPager" */;

View File

@ -1,5 +1,14 @@
{
"pins" : [
{
"identity" : "activelabel.swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/yattee/ActiveLabel.swift.git",
"state" : {
"branch" : "feature/timestamp",
"revision" : "2a88b80e44f84aa614032466039307c65815b2f3"
}
},
{
"identity" : "alamofire",
"kind" : "remoteSourceControl",