From 269dbed35245fe1f9274baabd115e9f03913b5a4 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Thu, 25 Aug 2022 19:09:55 +0200 Subject: [PATCH] Animations improvements --- Model/NavigationModel.swift | 4 +- Model/Player/Backends/MPVBackend.swift | 2 +- Model/Player/PlayerModel.swift | 28 +++--- Shared/Constants.swift | 6 ++ Shared/Delay.swift | 7 ++ .../AnimationCompletionObserverModifier.swift | 51 +++++++++++ Shared/Navigation/AppTabNavigation.swift | 2 + Shared/Navigation/ContentView.swift | 3 +- Shared/Player/PlayerLayerView.swift | 2 +- Shared/Player/PlayerQueueView.swift | 31 ++++--- Shared/Player/VideoDescription.swift | 5 +- Shared/Player/VideoDetails.swift | 48 +++++----- Shared/Player/VideoPlayerView.swift | 89 ++++++++++--------- Shared/Settings/MultiselectRow.swift | 2 +- Shared/Views/ChannelPlaylistView.swift | 2 +- Shared/Views/ChannelVideosView.swift | 2 +- Yattee.xcodeproj/project.pbxproj | 24 +++++ 17 files changed, 205 insertions(+), 103 deletions(-) create mode 100644 Shared/Constants.swift create mode 100644 Shared/Delay.swift create mode 100644 Shared/Modifiers/AnimationCompletionObserverModifier.swift diff --git a/Model/NavigationModel.swift b/Model/NavigationModel.swift index f81f89cf..f11b5d4c 100644 --- a/Model/NavigationModel.swift +++ b/Model/NavigationModel.swift @@ -116,7 +116,7 @@ final class NavigationModel: ObservableObject { if presentingPlayer { delay = 1.0 } #endif DispatchQueue.main.asyncAfter(deadline: .now() + delay) { - withAnimation(.linear(duration: 0.3)) { + withAnimation(Constants.overlayAnimation) { navigation.presentingChannel = true } } @@ -156,7 +156,7 @@ final class NavigationModel: ObservableObject { if presentingPlayer { delay = 1.0 } #endif DispatchQueue.main.asyncAfter(deadline: .now() + delay) { - withAnimation(.linear(duration: 0.3)) { + withAnimation(Constants.overlayAnimation) { navigation.presentingPlaylist = true } } diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 3abf1e4f..6f5cb341 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -9,7 +9,7 @@ import SwiftUI final class MPVBackend: PlayerBackend { static var controlsUpdateInterval = 0.5 - static var networkStateUpdateInterval = 0.3 + static var networkStateUpdateInterval = 1.0 private var logger = Logger(label: "mpv-backend") diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 521fe410..5920d74a 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -225,15 +225,13 @@ final class PlayerModel: ObservableObject { } #endif - navigation.hideKeyboard() - - if !presentingPlayer { - DispatchQueue.main.async { [weak self] in - withAnimation(.linear(duration: 0.25)) { - self?.presentingPlayer = true - } + #if os(iOS) + Delay.by(0.5) { + self.navigation.hideKeyboard() } - } + #endif + + if !presentingPlayer { presentingPlayer = true } #if os(macOS) Windows.player.open() @@ -241,13 +239,17 @@ final class PlayerModel: ObservableObject { #endif } - func hide() { - withAnimation(.linear(duration: 0.25)) { + func hide(animate: Bool = true) { + if animate { + withAnimation(.easeOut(duration: 0.2)) { + presentingPlayer = false + } + } else { presentingPlayer = false } DispatchQueue.main.async { [weak self] in - self?.playingFullScreen = false + self?.exitFullScreen(showControls: false) } #if os(iOS) @@ -591,9 +593,7 @@ final class PlayerModel: ObservableObject { exitFullScreen() #if !os(macOS) - if closePlayerOnItemClose { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in self?.hide() } - } + if closePlayerOnItemClose { Delay.by(0.2) { self.hide() } } #endif } diff --git a/Shared/Constants.swift b/Shared/Constants.swift new file mode 100644 index 00000000..5649c088 --- /dev/null +++ b/Shared/Constants.swift @@ -0,0 +1,6 @@ +import Foundation +import SwiftUI + +struct Constants { + static let overlayAnimation = Animation.linear(duration: 0.2) +} diff --git a/Shared/Delay.swift b/Shared/Delay.swift new file mode 100644 index 00000000..f42663c4 --- /dev/null +++ b/Shared/Delay.swift @@ -0,0 +1,7 @@ +import Foundation + +struct Delay { + @discardableResult static func by(_ interval: TimeInterval, block: @escaping () -> Void) -> Timer { + Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in block() } + } +} diff --git a/Shared/Modifiers/AnimationCompletionObserverModifier.swift b/Shared/Modifiers/AnimationCompletionObserverModifier.swift new file mode 100644 index 00000000..fbd11993 --- /dev/null +++ b/Shared/Modifiers/AnimationCompletionObserverModifier.swift @@ -0,0 +1,51 @@ +import Foundation +import SwiftUI + +/// An animatable modifier that is used for observing animations for a given animatable value. +struct AnimationCompletionObserverModifier: AnimatableModifier where Value: VectorArithmetic { + /// While animating, SwiftUI changes the old input value to the new target value using this property. This value is set to the old value until the animation completes. + var animatableData: Value { + didSet { + notifyCompletionIfFinished() + } + } + + /// The target value for which we're observing. This value is directly set once the animation starts. During animation, `animatableData` will hold the oldValue and is only updated to the target value once the animation completes. + private var targetValue: Value + + /// The completion callback which is called once the animation completes. + private var completion: () -> Void + + init(observedValue: Value, completion: @escaping () -> Void) { + self.completion = completion + animatableData = observedValue + targetValue = observedValue + } + + /// Verifies whether the current animation is finished and calls the completion callback if true. + private func notifyCompletionIfFinished() { + guard animatableData == targetValue else { return } + + /// Dispatching is needed to take the next runloop for the completion callback. + /// This prevents errors like "Modifying state during view update, this will cause undefined behavior." + DispatchQueue.main.async { + self.completion() + } + } + + func body(content: Content) -> some View { + /// We're not really modifying the view so we can directly return the original input value. + return content + } +} + +extension View { + /// Calls the completion handler whenever an animation on the given value completes. + /// - Parameters: + /// - value: The value to observe for animations. + /// - completion: The completion callback to call once the animation completes. + /// - Returns: A modified `View` instance with the observer attached. + func onAnimationCompleted(for value: Value, completion: @escaping () -> Void) -> ModifiedContent> { + modifier(AnimationCompletionObserverModifier(observedValue: value, completion: completion)) + } +} diff --git a/Shared/Navigation/AppTabNavigation.swift b/Shared/Navigation/AppTabNavigation.swift index 63780aaf..e66e328f 100644 --- a/Shared/Navigation/AppTabNavigation.swift +++ b/Shared/Navigation/AppTabNavigation.swift @@ -157,6 +157,7 @@ struct AppTabNavigation: View { .environmentObject(subscriptions) .environmentObject(thumbnailsModel) .id("channelVideos") + .zIndex(player.presentingPlayer ? -1 : 2) .transition(.move(edge: .bottom)) } } @@ -171,6 +172,7 @@ struct AppTabNavigation: View { .environmentObject(subscriptions) .environmentObject(thumbnailsModel) .id("channelPlaylist") + .zIndex(player.presentingPlayer ? -1 : 1) .transition(.move(edge: .bottom)) } } diff --git a/Shared/Navigation/ContentView.swift b/Shared/Navigation/ContentView.swift index 5d1c4891..2945cb1d 100644 --- a/Shared/Navigation/ContentView.swift +++ b/Shared/Navigation/ContentView.swift @@ -133,7 +133,8 @@ struct ContentView: View { @ViewBuilder var videoPlayer: some View { if player.presentingPlayer { playerView - .transition(.move(edge: .bottom)) + .transition(.asymmetric(insertion: .identity, removal: .move(edge: .bottom))) + .zIndex(3) } else if player.activeBackend == .appleAVPlayer { #if os(iOS) playerView.offset(y: UIScreen.main.bounds.height) diff --git a/Shared/Player/PlayerLayerView.swift b/Shared/Player/PlayerLayerView.swift index a66b28f4..99bf5f4a 100644 --- a/Shared/Player/PlayerLayerView.swift +++ b/Shared/Player/PlayerLayerView.swift @@ -12,7 +12,7 @@ import Foundation wantsLayer = true }} - override init(frame frameRect: NSRect) { + override init(frame frameRect: CGRect) { super.init(frame: frameRect) } diff --git a/Shared/Player/PlayerQueueView.swift b/Shared/Player/PlayerQueueView.swift index 34ec0303..ee20ad88 100644 --- a/Shared/Player/PlayerQueueView.swift +++ b/Shared/Player/PlayerQueueView.swift @@ -5,6 +5,7 @@ import SwiftUI struct PlayerQueueView: View { var sidebarQueue: Bool @Binding var fullScreen: Bool + @State private var relatedVisible = false @FetchRequest(sortDescriptors: [.init(key: "watchedAt", ascending: false)]) var watches: FetchedResults @@ -24,7 +25,7 @@ struct PlayerQueueView: View { autoplaying } playingNext - if sidebarQueue { + if sidebarQueue, relatedVisible { related } if saveHistory, showHistoryInPlayer { @@ -37,7 +38,15 @@ struct PlayerQueueView: View { .listRowInsets(EdgeInsets()) #endif } + .onChange(of: player.currentItem) { _ in + relatedVisible = false + Delay.by(2) { + withAnimation(.easeIn(duration: 0.25)) { + self.relatedVisible = true + } + } + } #if os(macOS) .listStyle(.inset) #elseif os(iOS) @@ -131,18 +140,18 @@ struct PlayerQueueView: View { } } - private var related: some View { - Group { - if !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty { - Section(header: Text("Related")) { - ForEach(player.currentVideo!.related) { video in - PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: $fullScreen) - .contextMenu { - VideoContextMenuView(video: video) - } - } + @ViewBuilder private var related: some View { + if let related = player.currentVideo?.related, !related.isEmpty { + Section(header: Text("Related")) { + ForEach(related) { video in + PlayerQueueRow(item: PlayerQueueItem(video), fullScreen: $fullScreen) + .contextMenu { + VideoContextMenuView(video: video) + } + .id(video.videoID) } } + .transaction { t in t.disablesAnimations = true } } } diff --git a/Shared/Player/VideoDescription.swift b/Shared/Player/VideoDescription.swift index 70ee48e9..4c2f793f 100644 --- a/Shared/Player/VideoDescription.swift +++ b/Shared/Player/VideoDescription.swift @@ -124,13 +124,14 @@ struct VideoDescription: View { components.scheme = "yattee" if let yatteeURL = components.url { let parser = URLParser(url: urlToOpen) - if parser.destination == .video, + let destination = parser.destination + if destination == .video, parser.videoID == player.currentVideo?.videoID, let time = parser.time { player.backend.seek(to: Double(time)) return - } else { + } else if destination != nil { urlToOpen = yatteeURL } } diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift index 944aec2a..0f328e4b 100644 --- a/Shared/Player/VideoDetails.swift +++ b/Shared/Player/VideoDetails.swift @@ -89,11 +89,13 @@ struct VideoDetails: View { VStack {} } } + .onPageWillChange { pageIndex in if pageIndex == DetailsPage.comments.index { comments.load() } } + .frame(maxWidth: detailsSize.width) } .onAppear { page.update(.moveToFirst) @@ -209,38 +211,36 @@ struct VideoDetails: View { @State private var detailsSize = CGSize.zero var detailsPage: some View { - Group { - VStack(alignment: .leading, spacing: 0) { - if let video = video { - VStack(spacing: 6) { - videoProperties + VStack(alignment: .leading, spacing: 0) { + if let video = video { + VStack(spacing: 6) { + videoProperties - Divider() - } - .padding(.bottom, 6) + Divider() + } + .padding(.bottom, 6) - VStack(alignment: .leading, spacing: 10) { - if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) { - VStack(alignment: .leading, spacing: 0) { - ForEach(1 ... Int.random(in: 2 ... 5), id: \.self) { _ in - Text(String(repeating: Video.fixture.description ?? "", count: Int.random(in: 1 ... 4))) - } + VStack(alignment: .leading, spacing: 10) { + if !player.videoBeingOpened.isNil && (video.description.isNil || video.description!.isEmpty) { + VStack(alignment: .leading, spacing: 0) { + ForEach(1 ... Int.random(in: 2 ... 5), id: \.self) { _ in + Text(String(repeating: Video.fixture.description ?? "", count: Int.random(in: 1 ... 4))) } - .redacted(reason: .placeholder) - } else if video.description != nil, !video.description!.isEmpty { - VideoDescription(video: video, detailsSize: detailsSize) - #if os(iOS) - .padding(.bottom, fullScreenLayout ? 10 : SafeArea.insets.bottom) - #endif - } else { - Text("No description") - .foregroundColor(.secondary) } + .redacted(reason: .placeholder) + } else if video.description != nil, !video.description!.isEmpty { + VideoDescription(video: video, detailsSize: detailsSize) + #if os(iOS) + .padding(.bottom, fullScreenLayout ? 10 : SafeArea.insets.bottom) + #endif + } else { + Text("No description") + .foregroundColor(.secondary) } } } - .padding(.horizontal) } + .padding(.horizontal) } var fullScreenLayout: Bool { diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index d8c2d5e4..92b45335 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -8,7 +8,7 @@ import SwiftUI struct VideoPlayerView: View { #if os(iOS) - static let hiddenOffset = YatteeApp.isForPreviews ? 0 : max(UIScreen.main.bounds.height, UIScreen.main.bounds.width) + 100 + static let hiddenOffset = UIScreen.main.bounds.height static let defaultSidebarQueueValue = UIScreen.main.bounds.width > 900 && Defaults[.playerSidebar] == .whenFits #else static let defaultSidebarQueueValue = Defaults[.playerSidebar] != .never @@ -45,7 +45,7 @@ struct VideoPlayerView: View { #if os(iOS) @GestureState private var dragGestureState = false @GestureState private var dragGestureOffset = CGSize.zero - @State private var viewDragOffset = 0.0 + @State private var viewDragOffset = Self.hiddenOffset @State private var orientationObserver: Any? #endif @@ -102,6 +102,7 @@ struct VideoPlayerView: View { } } } + .animation(nil, value: player.playerSize) .onAppear { if player.musicMode { player.backend.startControlsUpdates() @@ -145,25 +146,20 @@ struct VideoPlayerView: View { playerSize = geometry.size } } - #if os(iOS) - .frame(width: playerWidth.isNil ? nil : Double(playerWidth!), height: playerHeight.isNil ? nil : Double(playerHeight!)) - .ignoresSafeArea(.all, edges: playerEdgesIgnoringSafeArea) - #endif .onChange(of: geometry.size) { size in self.playerSize = size } .onChange(of: fullScreenDetails) { value in player.backend.setNeedsDrawing(!value) } + #if os(iOS) + .frame(width: playerWidth.isNil ? nil : Double(playerWidth!), height: playerHeight.isNil ? nil : Double(playerHeight!)) + .ignoresSafeArea(.all, edges: playerEdgesIgnoringSafeArea) .onAppear { - #if os(iOS) - viewDragOffset = 0.0 - configureOrientationUpdatesBasedOnAccelerometer() + viewDragOffset = 0 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak player] in - player?.onPresentPlayer?() - player?.onPresentPlayer = nil - } + Delay.by(0.2) { + configureOrientationUpdatesBasedOnAccelerometer() if let orientationMask = player.lockedOrientation { Orientation.lockOrientation( @@ -171,27 +167,37 @@ struct VideoPlayerView: View { andRotateTo: orientationMask == .landscapeLeft ? .landscapeLeft : orientationMask == .landscapeRight ? .landscapeRight : .portrait ) } - #endif + } } .onDisappear { - #if os(iOS) - if Defaults[.lockPortraitWhenBrowsing] { - Orientation.lockOrientation(.portrait, andRotateTo: .portrait) - } else { - Orientation.lockOrientation(.allButUpsideDown) - } - stopOrientationUpdates() - playerControls.hideOverlays() + if Defaults[.lockPortraitWhenBrowsing] { + Orientation.lockOrientation(.portrait, andRotateTo: .portrait) + } else { + Orientation.lockOrientation(.allButUpsideDown) + } + stopOrientationUpdates() + playerControls.hideOverlays() - player.lockedOrientation = nil - #endif + player.lockedOrientation = nil } + .onAnimationCompleted(for: viewDragOffset) { + guard !dragGestureState else { return } + if viewDragOffset == 0 { + player.onPresentPlayer?() + player.onPresentPlayer = nil + } else if viewDragOffset == Self.hiddenOffset { + player.hide(animate: false) + } + } + + #endif } + .compositingGroup() #if os(iOS) - .offset(y: playerOffset) - .animation(.linear(duration: 0.2), value: playerOffset) - .backport - .persistentSystemOverlays(!fullScreenLayout) + .offset(y: playerOffset) + .animation(dragGestureState ? .interactiveSpring(response: 0.05) : .easeOut(duration: 0.2), value: playerOffset) + .backport + .persistentSystemOverlays(!fullScreenLayout) #endif #endif } @@ -276,11 +282,6 @@ struct VideoPlayerView: View { } #if os(iOS) .gesture(playerControls.presentingOverlays ? nil : playerDragGesture) - .onChange(of: dragGestureState) { _ in - if !dragGestureState { - onPlayerDragGestureEnded() - } - } #elseif os(macOS) .onAppear(perform: { NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { @@ -302,7 +303,6 @@ struct VideoPlayerView: View { VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) #if os(iOS) .ignoresSafeArea(.all, edges: .bottom) - .transition(.move(edge: .bottom)) #endif .background(colorScheme == .dark ? Color.black : Color.white) .modifier(VideoDetailsPaddingModifier( @@ -412,7 +412,9 @@ struct VideoPlayerView: View { #if os(iOS) Button { - player.hide() + withAnimation(.spring(response: 0.3, dampingFraction: 0, blendDuration: 0)) { + viewDragOffset = Self.hiddenOffset + } } label: { Image(systemName: "xmark") .font(.system(size: 40)) @@ -474,16 +476,11 @@ struct VideoPlayerView: View { !playerControls.presentingControlsOverlay else { return } if viewDragOffset > 100 { - player.hide() - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { - player.backend.setNeedsDrawing(false) - player.exitFullScreen() + withAnimation(Constants.overlayAnimation) { + viewDragOffset = Self.hiddenOffset } - - viewDragOffset = Self.hiddenOffset } else { - withAnimation(.linear(duration: 0.2)) { + withAnimation(Constants.overlayAnimation) { viewDragOffset = 0 } player.backend.setNeedsDrawing(true) @@ -502,6 +499,8 @@ struct VideoPlayerView: View { !player.playingFullScreen, !player.playingInPictureInPicture { + guard player.presentingPlayer else { return } + DispatchQueue.main.async { playerControls.presentingControls = false player.enterFullScreen(showControls: false) @@ -532,7 +531,9 @@ struct VideoPlayerView: View { lastOrientation = orientation DispatchQueue.main.async { - guard Defaults[.enterFullscreenInLandscape] else { + guard Defaults[.enterFullscreenInLandscape], + player.presentingPlayer + else { return } diff --git a/Shared/Settings/MultiselectRow.swift b/Shared/Settings/MultiselectRow.swift index 2b6784cc..5126e3b8 100644 --- a/Shared/Settings/MultiselectRow.swift +++ b/Shared/Settings/MultiselectRow.swift @@ -45,6 +45,6 @@ struct MultiselectRow: View { struct MultiselectRow_Previews: PreviewProvider { static var previews: some View { - MultiselectRow(title: "Title", selected: false, action: { _ in }) + MultiselectRow(title: "Title", selected: false) { _ in } } } diff --git a/Shared/Views/ChannelPlaylistView.swift b/Shared/Views/ChannelPlaylistView.swift index 2b0463ae..9e871ff7 100644 --- a/Shared/Views/ChannelPlaylistView.swift +++ b/Shared/Views/ChannelPlaylistView.swift @@ -88,7 +88,7 @@ struct ChannelPlaylistView: View { ToolbarItem(placement: .navigation) { if navigationStyle == .tab { Button("Done") { - withAnimation { + withAnimation(Constants.overlayAnimation) { navigation.presentingPlaylist = false } } diff --git a/Shared/Views/ChannelVideosView.swift b/Shared/Views/ChannelVideosView.swift index eace2a57..46bfd6fb 100644 --- a/Shared/Views/ChannelVideosView.swift +++ b/Shared/Views/ChannelVideosView.swift @@ -84,7 +84,7 @@ struct ChannelVideosView: View { ToolbarItem(placement: .navigation) { if navigationStyle == .tab { Button("Done") { - withAnimation { + withAnimation(Constants.overlayAnimation) { navigation.presentingChannel = false } } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index e802e728..994bcfdd 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -318,6 +318,9 @@ 3752069D285E910600CA655F /* ChapterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChapterView.swift */; }; 3752069E285E910600CA655F /* ChapterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChapterView.swift */; }; 3752069F285E910600CA655F /* ChapterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChapterView.swift */; }; + 3754B01528B7F84D009717C8 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3754B01428B7F84D009717C8 /* Constants.swift */; }; + 3754B01628B7F84D009717C8 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3754B01428B7F84D009717C8 /* Constants.swift */; }; + 3754B01728B7F84D009717C8 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3754B01428B7F84D009717C8 /* Constants.swift */; }; 3756C2A62861131100E4B059 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A52861131100E4B059 /* NetworkState.swift */; }; 3756C2A72861131100E4B059 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A52861131100E4B059 /* NetworkState.swift */; }; 3756C2A82861131100E4B059 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A52861131100E4B059 /* NetworkState.swift */; }; @@ -691,6 +694,12 @@ 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 */; }; + 37D2E0D028B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2E0CF28B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift */; }; + 37D2E0D128B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2E0CF28B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift */; }; + 37D2E0D228B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2E0CF28B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift */; }; + 37D2E0D428B67EFC00F64D52 /* Delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2E0D328B67EFC00F64D52 /* Delay.swift */; }; + 37D2E0D528B67EFC00F64D52 /* Delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2E0D328B67EFC00F64D52 /* Delay.swift */; }; + 37D2E0D628B67EFC00F64D52 /* Delay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37D2E0D328B67EFC00F64D52 /* Delay.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 */; }; @@ -1057,6 +1066,7 @@ 3751BA8227E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnYouTubeDislikeAPI.swift; sourceTree = ""; }; 37520698285E8DD300CA655F /* Chapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chapter.swift; sourceTree = ""; }; 3752069C285E910600CA655F /* ChapterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterView.swift; sourceTree = ""; }; + 3754B01428B7F84D009717C8 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 3756C2A52861131100E4B059 /* NetworkState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkState.swift; sourceTree = ""; }; 3756C2A92861151C00E4B059 /* NetworkStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStateModel.swift; sourceTree = ""; }; 37579D5C27864F5F00FD0B98 /* Help.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Help.swift; sourceTree = ""; }; @@ -1194,6 +1204,8 @@ 37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleAssetStream.swift; sourceTree = ""; }; 37CEE4C02677B697005A1EFE /* Stream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Stream.swift; sourceTree = ""; }; 37CFB48428AFE2510070024C /* VideoDescription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoDescription.swift; sourceTree = ""; }; + 37D2E0CF28B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimationCompletionObserverModifier.swift; sourceTree = ""; }; + 37D2E0D328B67EFC00F64D52 /* Delay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Delay.swift; sourceTree = ""; }; 37D4B0C22671614700C925CA /* YatteeApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YatteeApp.swift; sourceTree = ""; }; 37D4B0C32671614700C925CA /* AppTabNavigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTabNavigation.swift; sourceTree = ""; }; 37D4B0C42671614800C925CA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -1796,6 +1808,7 @@ isa = PBXGroup; children = ( 37F64FE326FE70A60081B69E /* RedrawOnModifier.swift */, + 37D2E0CF28B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift */, ); path = Modifiers; sourceTree = ""; @@ -1965,8 +1978,10 @@ 371AAE2526CEBF0B00901972 /* Trending */, 371AAE2726CEBF4700901972 /* Videos */, 371AAE2826CEC7D900901972 /* Views */, + 3754B01428B7F84D009717C8 /* Constants.swift */, 375168D52700FAFF008F96A6 /* Debounce.swift */, 372915E52687E3B900F5A35B /* Defaults.swift */, + 37D2E0D328B67EFC00F64D52 /* Delay.swift */, 3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */, 3729037D2739E47400EA99F6 /* MenuCommands.swift */, 37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */, @@ -2740,6 +2755,7 @@ 372CFD15285F2E2A00B0B54B /* ControlsBar.swift in Sources */, 37BD07C82698B71C003EBB87 /* AppTabNavigation.swift in Sources */, 37599F38272B4D740087F250 /* FavoriteButton.swift in Sources */, + 3754B01528B7F84D009717C8 /* Constants.swift in Sources */, 378FFBC428660172009E3FBE /* URLParser.swift in Sources */, 3784B23D2728B85300B09468 /* ShareButton.swift in Sources */, 37EAD86B267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */, @@ -2750,6 +2766,7 @@ 37A5DBC8285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */, 377ABC40286E4AD5009C986F /* InstancesManifest.swift in Sources */, 37BD07B52698AA4D003EBB87 /* ContentView.swift in Sources */, + 37D2E0D428B67EFC00F64D52 /* Delay.swift in Sources */, 37130A5B277657090033018A /* Yattee.xcdatamodeld in Sources */, 37152EEA26EFEB95004FB96D /* LazyView.swift in Sources */, 3761ABFD26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, @@ -2760,6 +2777,7 @@ 37C7A1DA267CACF50010EAD6 /* TrendingCountry.swift in Sources */, 37E80F40287B472300561799 /* ScrollContentBackground+Backport.swift in Sources */, 375EC959289EEB8200751258 /* QualityProfileForm.swift in Sources */, + 37D2E0D028B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */, 3727B74A27872A920021C15E /* VisualEffectBlur-iOS.swift in Sources */, 37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */, 37130A5F277657300033018A /* PersistenceController.swift in Sources */, @@ -2991,6 +3009,7 @@ 37DD9DBB2785D60300539416 /* FramePreferenceKey.swift in Sources */, 375DFB5926F9DA010013F468 /* InstancesModel.swift in Sources */, 3705B183267B4E4900704544 /* TrendingCategory.swift in Sources */, + 37D2E0D128B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */, 37FB285F272225E800A57617 /* ContentItemView.swift in Sources */, 37FD43DC270470B70073EE42 /* InstancesSettings.swift in Sources */, 3756C2A72861131100E4B059 /* NetworkState.swift in Sources */, @@ -3125,6 +3144,7 @@ 37484C2A26FC83FF00287258 /* AccountForm.swift in Sources */, 37BE0BD026A0E2D50092E2DB /* VideoPlayerView.swift in Sources */, 373CFAEC26975CBF003CB2C6 /* PlaylistFormView.swift in Sources */, + 37D2E0D528B67EFC00F64D52 /* Delay.swift in Sources */, 37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */, 370F4FAB27CC164D001B35DC /* PlayerControlsModel.swift in Sources */, 37E8B0ED27B326C00024006F /* TimelineView.swift in Sources */, @@ -3135,6 +3155,7 @@ 3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */, 3782B95E2755858100990149 /* NSTextField+FocusRingType.swift in Sources */, 37C3A252272366440087A57A /* ChannelPlaylistView.swift in Sources */, + 3754B01628B7F84D009717C8 /* Constants.swift in Sources */, 373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */, 37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */, 37CFB48628AFE2510070024C /* VideoDescription.swift in Sources */, @@ -3234,6 +3255,7 @@ 3748187026A769D60084E870 /* DetailBadge.swift in Sources */, 3741A32C27E7EFFD00D266D1 /* PlayerControls.swift in Sources */, 371B7E632759706A00D21217 /* CommentsView.swift in Sources */, + 37D2E0D628B67EFC00F64D52 /* Delay.swift in Sources */, 379F1421289ECE7F00DE48B5 /* QualitySettings.swift in Sources */, 37A9965C26D6F8CA006E3224 /* HorizontalCells.swift in Sources */, 3782B95727557E6E00990149 /* SearchSuggestions.swift in Sources */, @@ -3372,6 +3394,7 @@ 37D4B19926717E1500C925CA /* Video.swift in Sources */, 378E50FD26FE8B9F00F49626 /* Instance.swift in Sources */, 37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */, + 3754B01728B7F84D009717C8 /* Constants.swift in Sources */, 37BC50AE2778BCBA00510953 /* HistoryModel.swift in Sources */, 37D526E02720AC4400ED2F5E /* VideosAPI.swift in Sources */, 37599F36272B44000087F250 /* FavoritesModel.swift in Sources */, @@ -3383,6 +3406,7 @@ 3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */, 37C3A24F272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */, 37FB28432721B22200A57617 /* ContentItem.swift in Sources */, + 37D2E0D228B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */, 37AAF2A226741C97007FC770 /* SubscriptionsView.swift in Sources */, 37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */, 372915E82687E3B900F5A35B /* Defaults.swift in Sources */,