diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/AudioToolbarButton.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/AudioToolbarButton.swift new file mode 100644 index 000000000..d7d961e87 --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/AudioToolbarButton.swift @@ -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) + } +} diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift index 861da9bdb..e86a00351 100644 --- a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayer.swift @@ -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) } } } diff --git a/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayerViewer.swift b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayerViewer.swift new file mode 100644 index 000000000..11451ee2e --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/AudioPlayer/MiniPlayerViewer.swift @@ -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 diff --git a/apple/OmnivoreKit/Sources/App/Views/Labels/MarqueTextView.swift b/apple/OmnivoreKit/Sources/App/Views/Labels/MarqueTextView.swift deleted file mode 100644 index cee36950b..000000000 --- a/apple/OmnivoreKit/Sources/App/Views/Labels/MarqueTextView.swift +++ /dev/null @@ -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 diff --git a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift index f3f5eb256..00529839a 100644 --- a/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/LibraryTabView.swift @@ -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) diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift index c48252a58..144c374e0 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootView.swift @@ -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() diff --git a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift index 3e96ab26d..5688277f6 100644 --- a/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift +++ b/apple/OmnivoreKit/Sources/App/Views/WebReader/WebReaderContainer.swift @@ -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) diff --git a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift index 3273ea68a..4e4209032 100644 --- a/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift +++ b/apple/OmnivoreKit/Sources/Services/DataService/Public/PDFLoading.swift @@ -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 {