Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4f9b5a9
Adds animations to the FloatingPlayerView for failing to dismiss and …
myhaksown Aug 11, 2026
2e14d61
Animations for failing to dismiss and dismissing the PlayerView. Som…
myhaksown Aug 11, 2026
dbe02c3
Fix for the FloatingPlayerView sitting on top of the tab bar on Max d…
myhaksown Aug 11, 2026
84574fd
Bottom button bar fixes. Makes airplay device name visible and pushe…
myhaksown Aug 11, 2026
ea80dcb
Swap the rectangle drag handle with a proper drag handle. Includes l…
myhaksown Aug 11, 2026
99c109a
Remaining logic for rounding the PlayerView corners (included in chan…
myhaksown Aug 11, 2026
a43e15c
Adjusts swipe distance for the PlayerView
myhaksown Aug 11, 2026
cad0445
Dynamic scaling and placement for various devices. Improves consiste…
myhaksown Aug 11, 2026
4931d8d
Larger target for the song position icon. Increases the size slightly.
myhaksown Aug 11, 2026
c858e4c
Adjust PlayerView height and offset values
myhaksown Aug 11, 2026
8fa3998
Merge pull request #1 from myhaksown/animations
myhaksown Aug 11, 2026
8d3de1a
Merge pull request #2 from myhaksown/ui-polish
myhaksown Aug 11, 2026
8ead25c
Redesigned LyricView to run on it's own with toggle, persistance when…
myhaksown Aug 12, 2026
ba18ca1
Remaining changes required for commit 8ead25c
myhaksown Aug 12, 2026
d3a9241
LyricView Redesign
myhaksown Aug 12, 2026
335cd10
When a song fails to play from the navidrome server the song will sho…
myhaksown Aug 12, 2026
1af2ffd
Merge remote-tracking branch 'refs/remotes/origin/gestures'
myhaksown Aug 12, 2026
816bbed
Completes the changes for lyric view. Tested to work on iOS 16 thru …
myhaksown Aug 13, 2026
116db5f
Fixes the PlayerView showing slightly on iPad Air 11 inch on iOS 26 w…
myhaksown Aug 13, 2026
60c41cb
Fixes a minor bug with the drag handler. When dragging down the Play…
myhaksown Aug 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 92 additions & 65 deletions flo/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import PulseUI
import SwiftUI
import Combine

struct ContentView: View {
@AppStorage(UserDefaultsKeys.enableDebug) private var enableDebug = false
Expand All @@ -24,8 +25,11 @@ struct ContentView: View {

@State private var floatingPlayerOffsetX: CGFloat = .zero
@State private var isSwipping = false

private var swipeThreshold: CGFloat = 150.0
@State private var keyboardHeight: CGFloat = 0

private var swipeThreshold: CGFloat {
UIScreen.main.bounds.width * 0.3
}

private var isPadSidebar: Bool {
guard UIDevice.current.userInterfaceIdiom == .pad else { return false }
Expand Down Expand Up @@ -114,40 +118,48 @@ struct ContentView: View {
}

@available(iOS 18.0, *)
private func sidebarTabContent<Content: View>(_ content: Content) -> some View {
content
.overlay(alignment: .bottom) {
if playerViewModel.hasNowPlaying() && !playerViewModel.shouldHidePlayer {
FloatingPlayerView(viewModel: playerViewModel)
.frame(maxWidth: 720)
.opacity(playerViewModel.hasNowPlaying() ? 1 : 0)
.offset(x: floatingPlayerOffsetX)
.onTapGesture {
self.isPlayerExpanded = true
}
.gesture(
DragGesture()
.onChanged { value in
if value.translation.width < .zero {
floatingPlayerOffsetX = value.translation.width
}

if abs(floatingPlayerOffsetX) > swipeThreshold, !isSwipping {
isSwipping = true
}
}
.onEnded { _ in
if abs(floatingPlayerOffsetX) > swipeThreshold, isSwipping {
playerViewModel.destroyPlayerAndQueue()
}

self.floatingPlayerOffsetX = .zero
self.isSwipping = false
private func sidebarTabContent<Content: View>(_ content: Content) -> some View {
content
.overlay(alignment: .bottom) {
if playerViewModel.hasNowPlaying() && !playerViewModel.shouldHidePlayer {
FloatingPlayerView(viewModel: playerViewModel)
.padding(.horizontal, 20)
.opacity(playerViewModel.hasNowPlaying() ? 1 : 0)
.offset(x: floatingPlayerOffsetX)
.onTapGesture {
self.isPlayerExpanded = true
}
.gesture(
DragGesture()
.onChanged { value in
if value.translation.width < .zero {
floatingPlayerOffsetX = value.translation.width
}

if abs(floatingPlayerOffsetX) > swipeThreshold, !isSwipping {
isSwipping = true
}
}
.onEnded { _ in
if abs(floatingPlayerOffsetX) > swipeThreshold, isSwipping {
withAnimation(.easeOut(duration: 0.25)) {
self.floatingPlayerOffsetX = -UIScreen.main.bounds.width
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
playerViewModel.destroyPlayerAndQueue()
self.floatingPlayerOffsetX = .zero
}
} else {
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)){
self.floatingPlayerOffsetX = .zero
}
}
self.isSwipping = false
}
)
}
)
}
}
}
}
}

@available(iOS 18.0, *)
private var sidebarTabView: some View {
Expand Down Expand Up @@ -273,9 +285,9 @@ struct ContentView: View {
GeometryReader { geometry in
let offScreenY: CGFloat = {
#if targetEnvironment(macCatalyst)
geometry.size.height
geometry.size.height + 20
#else
UIScreen.main.bounds.height
UIScreen.main.bounds.height + 20
#endif
}()

Expand All @@ -288,34 +300,28 @@ struct ContentView: View {
PlayerView(isExpanded: $isPlayerExpanded, viewModel: playerViewModel)
.ignoresSafeArea()
.offset(y: isPlayerExpanded ? 0 : offScreenY)
.animation(.spring(duration: 0.2), value: isPlayerExpanded)
.animation(.spring(response: 0.4, dampingFraction: 0.7), value: isPlayerExpanded)
}

if !isPadSidebar {
VStack {
Spacer()

if playerViewModel.hasNowPlaying() && !playerViewModel.shouldHidePlayer {
let isSmallScreen = UIScreen.main.bounds.width <= 390
let isPad = UIDevice.current.userInterfaceIdiom == .pad
let bottomPadding: CGFloat = isSmallScreen ? 32 : 0
let playerWidth: CGFloat? =
isPad
? 720
: (horizontalSizeClass == .regular ? 500 : nil)
let playerCenterOffsetX = floatingPlayerContentCenterOffsetX(
totalWidth: geometry.size.width
)
let playerBottomPadding: CGFloat = {
#if targetEnvironment(macCatalyst)
10
#else
isPad ? 0 : (40 + bottomPadding)
keyboardHeight > 0 ? -keyboardHeight + geometry.safeAreaInsets.bottom + 8: geometry.safeAreaInsets.bottom + 20
#endif
}()

FloatingPlayerView(viewModel: playerViewModel)
.frame(maxWidth: playerWidth ?? .infinity)
.padding(.horizontal, isPad ? 40 : 8)
.padding(.bottom, playerBottomPadding)
.opacity(playerViewModel.hasNowPlaying() ? 1 : 0)
.offset(
Expand All @@ -327,32 +333,53 @@ struct ContentView: View {
self.isPlayerExpanded = true
}
.gesture(
DragGesture()
.onChanged { value in
if value.translation.width < .zero {
floatingPlayerOffsetX = value.translation.width
}

if abs(floatingPlayerOffsetX) > swipeThreshold, !isSwipping {
isSwipping = true
}
}
.onEnded { _ in
if abs(floatingPlayerOffsetX) > swipeThreshold, isSwipping {
playerViewModel.destroyPlayerAndQueue()
}

self.floatingPlayerOffsetX = .zero
self.isSwipping = false
}
DragGesture()
.onChanged { value in
if value.translation.width < .zero {
floatingPlayerOffsetX = value.translation.width
}

if abs(floatingPlayerOffsetX) > swipeThreshold, !isSwipping {
isSwipping = true
}
}
.onEnded { _ in
if abs(floatingPlayerOffsetX) > swipeThreshold, isSwipping {
withAnimation(.easeOut(duration: 0.25)) {
self.floatingPlayerOffsetX = -UIScreen.main.bounds.width
}
DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) {
playerViewModel.destroyPlayerAndQueue()
self.floatingPlayerOffsetX = .zero
}

} else {
withAnimation(.spring(response: 0.4, dampingFraction: 0.7)){
self.floatingPlayerOffsetX = .zero

}
}
self.isSwipping = false
}
)
}
}
}
}
}
.onAppear {
PlaybackCoordinator.shared.attach(playerViewModel: playerViewModel)
PlaybackCoordinator.shared.attach(playerViewModel: playerViewModel)
}
.onReceive(NotificationCenter.default.publisher(for:
UIResponder.keyboardWillShowNotification)) { notification in
if let userInfo = notification.userInfo,
let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
keyboardHeight = keyboardFrame.height
}
}
.onReceive(NotificationCenter.default.publisher(for:
UIResponder.keyboardWillHideNotification)) { _ in
keyboardHeight = 0
}
}
}
Expand Down
Loading