mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Move miniplayer around to work with new layout on iPhone
This commit is contained in:
parent
89f21ad228
commit
663ea1d3f8
8 changed files with 235 additions and 249 deletions
|
|
@ -0,0 +1,44 @@
|
|||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
struct AudioToolbarButton: View {
|
||||
@State private var drawingHeight = true
|
||||
|
||||
var animation: Animation {
|
||||
.linear(duration: 0.5).repeatForever()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(Color.black)
|
||||
.frame(width: 21, height: 21)
|
||||
|
||||
// HStack(spacing: 1) {
|
||||
// bar(low: 0.4)
|
||||
// .animation(animation.speed(1.5), value: drawingHeight)
|
||||
// bar(low: 0.3)
|
||||
// .animation(animation.speed(1.2), value: drawingHeight)
|
||||
// bar(low: 0.5)
|
||||
// .animation(animation.speed(1.0), value: drawingHeight)
|
||||
// bar(low: 0.3)
|
||||
// .animation(animation.speed(1.7), value: drawingHeight)
|
||||
// // bar(low: 0.5)
|
||||
// // .animation(animation.speed(1.0), value: drawingHeight)
|
||||
// }
|
||||
// .frame(width: 20)
|
||||
// .onAppear {
|
||||
// drawingHeight.toggle()
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func bar(low: CGFloat = 0.0, high: CGFloat = 1.0) -> some View {
|
||||
RoundedRectangle(cornerRadius: 3)
|
||||
.fill(Color.black)
|
||||
.frame(width: 1)
|
||||
.frame(height: (drawingHeight ? high : low) * 15)
|
||||
.frame(height: 15, alignment: .bottom)
|
||||
}
|
||||
}
|
||||
|
|
@ -20,150 +20,6 @@
|
|||
self.presentingView = AnyView(presentingView)
|
||||
}
|
||||
|
||||
var playPauseButtonImage: String {
|
||||
switch audioController.state {
|
||||
case .playing:
|
||||
return "pause.circle"
|
||||
case .paused:
|
||||
return "play.circle"
|
||||
case .reachedEnd:
|
||||
return "gobackward"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var playPauseButtonItem: some View {
|
||||
if audioController.playbackError {
|
||||
return AnyView(Color.clear)
|
||||
}
|
||||
if let itemID = audioController.itemAudioProperties?.itemID, audioController.isLoadingItem(itemID: itemID) {
|
||||
return AnyView(ProgressView())
|
||||
} else {
|
||||
return AnyView(Button(
|
||||
action: {
|
||||
switch audioController.state {
|
||||
case .playing:
|
||||
audioController.pause()
|
||||
case .paused:
|
||||
audioController.unpause()
|
||||
case .reachedEnd:
|
||||
audioController.seek(to: 0.0)
|
||||
audioController.unpause()
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
label: {
|
||||
Image(systemName: playPauseButtonImage)
|
||||
.resizable(resizingMode: Image.ResizingMode.stretch)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.font(Font.title.weight(.light))
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
var stopButton: some View {
|
||||
Button(
|
||||
action: {
|
||||
audioController.stop()
|
||||
},
|
||||
label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.foregroundColor(Color(hex: "#3D3D3D"))
|
||||
|
||||
Image(systemName: "xmark")
|
||||
.resizable(resizingMode: Image.ResizingMode.stretch)
|
||||
.foregroundColor(Color(hex: "#D9D9D9"))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.font(Font.title.weight(.medium))
|
||||
.frame(width: 14, height: 14)
|
||||
}
|
||||
}
|
||||
)
|
||||
.background(Color.clear)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
func artwork(_ itemAudioProperties: LinkedItemAudioProperties, forDimensions dim: Double) -> some View {
|
||||
if let imageURL = itemAudioProperties.imageURL {
|
||||
return AnyView(AsyncImage(url: imageURL) { phase in
|
||||
if let image = phase.image {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: dim, height: dim)
|
||||
.cornerRadius(6)
|
||||
} else if phase.error != nil {
|
||||
defaultArtwork(forDimensions: dim)
|
||||
} else {
|
||||
Color.appButtonBackground
|
||||
.frame(width: dim, height: dim)
|
||||
.cornerRadius(6)
|
||||
}
|
||||
})
|
||||
}
|
||||
return AnyView(defaultArtwork(forDimensions: dim))
|
||||
}
|
||||
|
||||
func defaultArtwork(forDimensions dim: Double) -> some View {
|
||||
ZStack(alignment: .center) {
|
||||
Color.appButtonBackground
|
||||
.frame(width: dim, height: dim)
|
||||
.cornerRadius(6)
|
||||
|
||||
Image.headphones
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: dim / 2, height: dim / 2)
|
||||
}
|
||||
}
|
||||
|
||||
func playerContent(_ itemAudioProperties: LinkedItemAudioProperties) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(alignment: .center, spacing: 15) {
|
||||
if audioController.playbackError {
|
||||
Text("There was an error playing back your audio.").foregroundColor(Color.red).font(.footnote)
|
||||
Spacer(minLength: 0)
|
||||
} else {
|
||||
artwork(itemAudioProperties, forDimensions: 50)
|
||||
|
||||
Text(itemAudioProperties.title)
|
||||
.font(Font.system(size: 17, weight: .medium))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
.frame(maxHeight: 40, alignment: .leading)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
playPauseButtonItem
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(.themeAudioPlayerGray)
|
||||
}
|
||||
stopButton
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(.themeAudioPlayerGray)
|
||||
}
|
||||
.padding(16)
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.background(
|
||||
Color.systemBackground
|
||||
.shadow(color: .gray.opacity(0.33), radius: 8, x: 0, y: 4)
|
||||
.mask(Rectangle().padding(.top, -20))
|
||||
)
|
||||
.onTapGesture {
|
||||
withAnimation(.easeIn(duration: 0.08)) { expanded = true }
|
||||
}.fullScreenCover(isPresented: $expanded) {
|
||||
ExpandedPlayer()
|
||||
}
|
||||
.offset(y: expanded ? 110 : 0)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
ZStack(alignment: .center) {
|
||||
presentingView
|
||||
|
|
@ -174,8 +30,7 @@
|
|||
|
||||
VStack {
|
||||
Spacer(minLength: 0)
|
||||
playerContent(itemAudioProperties)
|
||||
.frame(maxHeight: expanded ? 0 : 110)
|
||||
MiniPlayerViewer(itemAudioProperties: itemAudioProperties)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
#if os(iOS)
|
||||
|
||||
import Foundation
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Utils
|
||||
import Views
|
||||
|
||||
public struct MiniPlayerViewer: View {
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
@Environment(\.colorScheme) private var colorScheme: ColorScheme
|
||||
|
||||
@State var expanded = true
|
||||
|
||||
let itemAudioProperties: LinkedItemAudioProperties
|
||||
|
||||
var playPauseButtonImage: String {
|
||||
switch audioController.state {
|
||||
case .playing:
|
||||
return "pause.circle"
|
||||
case .paused:
|
||||
return "play.circle"
|
||||
case .reachedEnd:
|
||||
return "gobackward"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var playPauseButtonItem: some View {
|
||||
if audioController.playbackError {
|
||||
return AnyView(Color.clear)
|
||||
}
|
||||
if let itemID = audioController.itemAudioProperties?.itemID, audioController.isLoadingItem(itemID: itemID) {
|
||||
return AnyView(ProgressView())
|
||||
} else {
|
||||
return AnyView(Button(
|
||||
action: {
|
||||
switch audioController.state {
|
||||
case .playing:
|
||||
audioController.pause()
|
||||
case .paused:
|
||||
audioController.unpause()
|
||||
case .reachedEnd:
|
||||
audioController.seek(to: 0.0)
|
||||
audioController.unpause()
|
||||
default:
|
||||
break
|
||||
}
|
||||
},
|
||||
label: {
|
||||
Image(systemName: playPauseButtonImage)
|
||||
.resizable(resizingMode: Image.ResizingMode.stretch)
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.font(Font.title.weight(.light))
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
var stopButton: some View {
|
||||
Button(
|
||||
action: {
|
||||
audioController.stop()
|
||||
},
|
||||
label: {
|
||||
ZStack {
|
||||
Circle()
|
||||
.foregroundColor(Color(hex: "#3D3D3D"))
|
||||
|
||||
Image(systemName: "xmark")
|
||||
.resizable(resizingMode: Image.ResizingMode.stretch)
|
||||
.foregroundColor(Color(hex: "#D9D9D9"))
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.font(Font.title.weight(.medium))
|
||||
.frame(width: 14, height: 14)
|
||||
}
|
||||
}
|
||||
)
|
||||
.background(Color.clear)
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
}
|
||||
|
||||
func artwork(_ itemAudioProperties: LinkedItemAudioProperties, forDimensions dim: Double) -> some View {
|
||||
if let imageURL = itemAudioProperties.imageURL {
|
||||
return AnyView(AsyncImage(url: imageURL) { phase in
|
||||
if let image = phase.image {
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: dim, height: dim)
|
||||
.cornerRadius(6)
|
||||
} else if phase.error != nil {
|
||||
defaultArtwork(forDimensions: dim)
|
||||
} else {
|
||||
Color.appButtonBackground
|
||||
.frame(width: dim, height: dim)
|
||||
.cornerRadius(6)
|
||||
}
|
||||
})
|
||||
}
|
||||
return AnyView(defaultArtwork(forDimensions: dim))
|
||||
}
|
||||
|
||||
func defaultArtwork(forDimensions dim: Double) -> some View {
|
||||
ZStack(alignment: .center) {
|
||||
Color.appButtonBackground
|
||||
.frame(width: dim, height: dim)
|
||||
.cornerRadius(6)
|
||||
|
||||
Image.headphones
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: dim / 2, height: dim / 2)
|
||||
}
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HStack(alignment: .center, spacing: 15) {
|
||||
if audioController.playbackError {
|
||||
Text("There was an error playing back your audio.").foregroundColor(Color.red).font(.footnote)
|
||||
Spacer(minLength: 0)
|
||||
} else {
|
||||
artwork(itemAudioProperties, forDimensions: 50)
|
||||
|
||||
Text(itemAudioProperties.title)
|
||||
.font(Font.system(size: 17, weight: .medium))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(2)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
.frame(maxHeight: 40, alignment: .leading)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
playPauseButtonItem
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(.themeAudioPlayerGray)
|
||||
}
|
||||
stopButton
|
||||
.frame(width: 40, height: 40)
|
||||
.foregroundColor(.themeAudioPlayerGray)
|
||||
}
|
||||
.padding(.vertical, 5)
|
||||
.padding(.horizontal, 15)
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
||||
.background(
|
||||
Color.themeTabBarColor
|
||||
)
|
||||
.frame(height: 60)
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
#if os(iOS)
|
||||
import SwiftUI
|
||||
|
||||
// Mostly from: https://kavsoft.dev/swiftui_3.0_marquee_text_animation with some customizations
|
||||
|
||||
struct Marquee: View {
|
||||
var text: String
|
||||
var font: UIFont
|
||||
|
||||
// Storing Text Size
|
||||
@State var storedSize: CGSize = .zero
|
||||
@State var offset: CGFloat = 0
|
||||
@State var animatedText: String = ""
|
||||
|
||||
var animationSpeed: Double = 0.03
|
||||
var delayTime: Double = 3.0
|
||||
|
||||
var body: some View {
|
||||
// Since it scrolls horizontal using ScrollView
|
||||
GeometryReader { proxy in
|
||||
|
||||
let size = proxy.size
|
||||
|
||||
let condition = textSize(text: text).width < (size.width - 50)
|
||||
|
||||
ScrollView(condition ? .init() : .horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .center) {
|
||||
Spacer(minLength: 0)
|
||||
Text(condition ? text : animatedText)
|
||||
.font(Font(font))
|
||||
.offset(x: condition ? 0 : offset)
|
||||
.padding(.horizontal, 15)
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||
}
|
||||
.frame(height: storedSize.height)
|
||||
.overlay(content: {
|
||||
// swiftlint:disable line_length
|
||||
HStack {
|
||||
let color: Color = .systemBackground
|
||||
|
||||
LinearGradient(colors: [color, color.opacity(0.7), color.opacity(0.5), color.opacity(0.3)], startPoint: .leading, endPoint: .trailing)
|
||||
.frame(width: 8)
|
||||
|
||||
Spacer()
|
||||
|
||||
LinearGradient(colors: [color, color.opacity(0.7), color.opacity(0.5), color.opacity(0.3)].reversed(), startPoint: .leading, endPoint: .trailing)
|
||||
.frame(width: 8)
|
||||
// swiftlint:enable line_length
|
||||
}
|
||||
})
|
||||
.disabled(true)
|
||||
.onAppear {
|
||||
startAnimation(text: text)
|
||||
}
|
||||
.onReceive(Timer.publish(every: (animationSpeed * storedSize.width) + delayTime,
|
||||
on: .main,
|
||||
in: .default).autoconnect()
|
||||
) { _ in
|
||||
offset = 0
|
||||
withAnimation(.linear(duration: animationSpeed * storedSize.width).delay(delayTime)) {
|
||||
offset = -storedSize.width
|
||||
}
|
||||
}
|
||||
.onChange(of: text) { newValue in
|
||||
animatedText = ""
|
||||
offset = 0
|
||||
startAnimation(text: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
func startAnimation(text: String) {
|
||||
// Double the text with some spacing so that we can create a continuous loop
|
||||
animatedText.append(text)
|
||||
(1 ... 15).forEach { _ in
|
||||
animatedText.append(" ")
|
||||
}
|
||||
storedSize = textSize(text: animatedText)
|
||||
animatedText.append(text)
|
||||
|
||||
let timing: Double = (animationSpeed * storedSize.width)
|
||||
withAnimation(.linear(duration: timing).delay(delayTime)) {
|
||||
offset = -storedSize.width
|
||||
}
|
||||
}
|
||||
|
||||
func textSize(text: String) -> CGSize {
|
||||
let attributes = [NSAttributedString.Key.font: font]
|
||||
|
||||
let size = (text as NSString).size(withAttributes: attributes)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
@ -17,8 +17,12 @@ import Views
|
|||
@MainActor
|
||||
struct LibraryTabView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
|
||||
@AppStorage(UserDefaultKey.lastSelectedTabItem.rawValue) var selectedTab = "inbox"
|
||||
|
||||
@State var showExpandedAudioPlayer = false
|
||||
|
||||
@MainActor
|
||||
public init() {
|
||||
UITabBar.appearance().isHidden = true
|
||||
|
|
@ -47,7 +51,7 @@ struct LibraryTabView: View {
|
|||
)
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack(spacing: 0) {
|
||||
TabView(selection: $selectedTab) {
|
||||
NavigationView {
|
||||
HomeFeedContainerView(viewModel: followingViewModel)
|
||||
|
|
@ -64,7 +68,21 @@ struct LibraryTabView: View {
|
|||
.navigationViewStyle(.stack)
|
||||
}.tag("profile")
|
||||
}
|
||||
if let audioProperties = audioController.itemAudioProperties {
|
||||
MiniPlayerViewer(itemAudioProperties: audioProperties)
|
||||
.onTapGesture {
|
||||
showExpandedAudioPlayer = true
|
||||
}
|
||||
.padding(0)
|
||||
Color(hex: "#3D3D3D")
|
||||
.frame(height: 1)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
CustomTabBar(selectedTab: $selectedTab)
|
||||
.padding(0)
|
||||
}
|
||||
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
|
||||
ExpandedPlayer()
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
.navigationBarHidden(true)
|
||||
|
|
|
|||
|
|
@ -51,9 +51,6 @@ struct InnerRootView: View {
|
|||
@ViewBuilder private var innerBody: some View {
|
||||
if authenticator.isLoggedIn {
|
||||
PrimaryContentView()
|
||||
#if os(iOS)
|
||||
.miniPlayer()
|
||||
#endif
|
||||
} else {
|
||||
WelcomeView()
|
||||
.accessibilityElement()
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ struct WebReaderContainerView: View {
|
|||
@State var readerSettingsChangedTransactionID: UUID?
|
||||
@State var annotationSaveTransactionID: UUID?
|
||||
@State var showNavBarActionID: UUID?
|
||||
@State var showExpandedAudioPlayer = false
|
||||
@State var shareActionID: UUID?
|
||||
@State var annotation = String()
|
||||
@State var showBottomBar = false
|
||||
|
|
@ -463,6 +464,9 @@ struct WebReaderContainerView: View {
|
|||
.fullScreenCover(item: $safariWebLink) {
|
||||
SafariView(url: $0.url)
|
||||
}
|
||||
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
|
||||
ExpandedPlayer()
|
||||
}
|
||||
#endif
|
||||
.alert(errorAlertMessage ?? LocalText.readerError, isPresented: $showErrorAlertMessage) {
|
||||
Button(LocalText.genericOk, role: .cancel, action: {
|
||||
|
|
@ -592,7 +596,17 @@ struct WebReaderContainerView: View {
|
|||
self.bottomBarOpacity = 0
|
||||
}
|
||||
}
|
||||
if let audioProperties = audioController.itemAudioProperties {
|
||||
MiniPlayerViewer(itemAudioProperties: audioProperties)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 40)
|
||||
.background(Color.themeTabBarColor)
|
||||
.onTapGesture {
|
||||
showExpandedAudioPlayer = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@ public extension DataService {
|
|||
result = try await URLSession.shared.data(for: request)
|
||||
} catch {
|
||||
print("ERROR DOWNLOADING PDF DATA: ", error)
|
||||
print("URL", url)
|
||||
}
|
||||
|
||||
guard let httpResponse = result?.1 as? HTTPURLResponse, 200 ..< 300 ~= httpResponse.statusCode else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue