diff --git a/FloatplaneAPIClient/Package.swift b/FloatplaneAPIClient/Package.swift
index 9a1b6d4..e7c7945 100644
--- a/FloatplaneAPIClient/Package.swift
+++ b/FloatplaneAPIClient/Package.swift
@@ -7,6 +7,7 @@ let package = Package(
platforms: [
.macOS(.v10_15),
.tvOS(.v13),
+ .iOS(.v13)
],
products: [
// Products define the executables and libraries produced by a package, and make them visible to other packages.
diff --git a/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/Shared/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 0000000..eb87897
--- /dev/null
+++ b/Shared/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json b/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..c136eaf
--- /dev/null
+++ b/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,148 @@
+{
+ "images" : [
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "2x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "iphone",
+ "scale" : "3x",
+ "size" : "60x60"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "20x20"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "29x29"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "40x40"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "1x",
+ "size" : "76x76"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "76x76"
+ },
+ {
+ "idiom" : "ipad",
+ "scale" : "2x",
+ "size" : "83.5x83.5"
+ },
+ {
+ "idiom" : "ios-marketing",
+ "scale" : "1x",
+ "size" : "1024x1024"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "16x16"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "32x32"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "128x128"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "256x256"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "1x",
+ "size" : "512x512"
+ },
+ {
+ "idiom" : "mac",
+ "scale" : "2x",
+ "size" : "512x512"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Shared/Assets.xcassets/Contents.json b/Shared/Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/Shared/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/Shared/ContentView.swift b/Shared/ContentView.swift
new file mode 100644
index 0000000..9103843
--- /dev/null
+++ b/Shared/ContentView.swift
@@ -0,0 +1,88 @@
+//
+// ContentView.swift
+// Shared
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import SwiftUI
+import FloatplaneAPIClient
+
+struct ContentView: View {
+ @ObservedObject var viewModel: AuthViewModel
+
+ @State var showErrorMoreDetails = false
+
+ @State var hasInitiallyLoaded = false
+
+ @State var width = UIScreen.main.bounds.width
+
+ @Environment(\.colorScheme) var colorScheme
+
+ enum Notifications {
+ static let loggedOut = Notification.Name("com.jamesnl.Wasserflug-tvOSApp.loggedOut")
+ }
+
+ var body: some View {
+ VStack {
+ ZStack {
+ if viewModel.isLoadingAuthStatus {
+ Image("wasserflug-logo")
+ .resizable()
+ .foregroundColor(colorScheme == .dark ? .white : .black)
+ .scaledToFit()
+ .frame(maxWidth: 250)
+ ProgressView()
+ } else if !viewModel.isLoggedIn {
+ LoginView(viewModel: viewModel)
+ } else {
+ RootTabView()
+ }
+ GeometryReader { proxy in
+ Color.clear.onChange(of: proxy.size.width) { newValue in
+ DispatchQueue.main.async {
+ self.width = newValue
+ }
+ }.onAppear {
+ DispatchQueue.main.async {
+ self.width = proxy.size.width
+ }
+ }
+ }
+ .hidden()
+ }
+ }
+ .environmentObject(viewModel.userInfo)
+ .environment(\.screenWidth, self.width)
+ .onAppear(perform: {
+ if !hasInitiallyLoaded {
+ hasInitiallyLoaded = true
+ viewModel.determineAuthenticationStatus()
+ }
+ })
+ .onReceive(NotificationCenter.default.publisher(for: Notifications.loggedOut, object: nil), perform: { _ in
+ viewModel.determineAuthenticationStatus()
+ })
+ .alert("Application Error", isPresented: $viewModel.showAuthenticationErrorAlert, presenting: viewModel.authenticationCheckError, actions: { _ in
+ Button("OK", action: {})
+ Button("More Information", action: {
+ showErrorMoreDetails = true
+ })
+ }, message: { error in
+ Text("""
+Logging in was successful, but an error was encountered while loading your user profile. Please submit a bug report with the app developer, *NOT* with Floatplane staff.
+
+\(error.localizedDescription)
+""")
+ })
+ .alert("Application Error", isPresented: $showErrorMoreDetails, presenting: viewModel.authenticationCheckError, actions: { _ in }, message: { error in
+ Text("\(String(describing: error))")
+ })
+ }
+}
+
+struct ContentView_Previews: PreviewProvider {
+ static var previews: some View {
+ ContentView(viewModel: AuthViewModel(fpApiService: MockFPAPIService()))
+ }
+}
diff --git a/Shared/Launch Screen.storyboard b/Shared/Launch Screen.storyboard
new file mode 100644
index 0000000..46e7b9e
--- /dev/null
+++ b/Shared/Launch Screen.storyboard
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Shared/SupplementalViews/BlogPostSelectionView.swift b/Shared/SupplementalViews/BlogPostSelectionView.swift
new file mode 100644
index 0000000..1a6a6eb
--- /dev/null
+++ b/Shared/SupplementalViews/BlogPostSelectionView.swift
@@ -0,0 +1,161 @@
+//
+// BlogPostSelectionView.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import SwiftUI
+import FloatplaneAPIClient
+import CachedAsyncImage
+
+struct BlogPostSelectionView: View {
+ enum ViewOrigin: Equatable {
+ case home(UserModel?)
+ case creator
+ }
+
+ let blogPost: BlogPostModelV3
+ let viewOrigin: ViewOrigin
+ @FetchRequest var watchProgresses: FetchedResults
+
+ @Environment(\.fpApiService) var fpApiService
+ @Environment(\.managedObjectContext) private var viewContext
+
+ @State var clickedOnVideo = false;
+
+ var progress: CGFloat {
+ if let watchProgress = watchProgresses.first(where: { $0.videoId == blogPost.videoAttachments?.first }) {
+ let progress = watchProgress.progress
+ return progress >= 0.95 ? 1.0 : progress
+ } else {
+ return 0.0
+ }
+ }
+
+ private let relativeTimeConverter: RelativeDateTimeFormatter = {
+ let formatter = RelativeDateTimeFormatter()
+ formatter.unitsStyle = .full
+ return formatter
+ }()
+
+ var body: some View {
+ let meta = blogPost.metadata
+
+ VStack {
+ NavigationLink("Link to Video", isActive: $clickedOnVideo) {
+ BlogPostView(viewModel: BlogPostViewModel(fpApiService: fpApiService, id: blogPost.id), watchProgresses: FetchRequest(entity: WatchProgress.entity(), sortDescriptors: [], predicate: NSPredicate(format: "blogPostId = %@ and videoId = %@", blogPost.id, blogPost.videoAttachments?.first ?? "")), shouldAutoPlay: true)
+ }
+ .hidden()
+ CachedAsyncImage(url: blogPost.thumbnail.pathUrlOrNil, content: { image in
+ ZStack(alignment: .bottomLeading) {
+ image
+ .resizable()
+ .scaledToFit()
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ VStack {
+ HStack {
+ Spacer()
+ let duration: TimeInterval = meta.hasVideo ? meta.videoDuration : meta.hasAudio ? meta.audioDuration : 0.0
+ if duration != 0 {
+ HStack {
+ Text("\(TimeInterval(duration).floatplaneTimestamp)")
+ .colorInvert()
+ }
+ .padding([.leading, .trailing], 5)
+ .background(FPColors.darkBlue)
+ .cornerRadius(10.0, corners: [.bottomLeft])
+ }
+ }
+ Spacer()
+ }
+ GeometryReader { geometry in
+ Rectangle()
+ .fill(FPColors.blue)
+ .frame(width: geometry.size.width * progress)
+ }
+ .frame(height: 4)
+ }
+ .cornerRadius(10.0)
+ }, placeholder: {
+ ZStack {
+ ProgressView()
+ VStack {
+ HStack {
+ Spacer()
+ let duration: TimeInterval = meta.hasVideo ? meta.videoDuration : meta.hasAudio ? meta.audioDuration : 0.0
+ if duration != 0 {
+ HStack {
+ Text("\(TimeInterval(duration).floatplaneTimestamp)")
+ }
+ .padding([.leading, .trailing], 5)
+ }
+ }
+ Spacer()
+ }
+ Rectangle()
+ .fill(.clear)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .aspectRatio(blogPost.thumbnail?.aspectRatio ?? 1.0, contentMode: .fit)
+ }
+ })
+ HStack {
+ let profileImageSize: CGFloat = 35
+ if case let .home(creatorOwner) = viewOrigin,
+ let profileImagePath = creatorOwner?.profileImage.path,
+ let profileImageUrl = URL(string: profileImagePath) {
+ CachedAsyncImage(url: profileImageUrl, content: { image in
+ image
+ .resizable()
+ .scaledToFit()
+ .frame(width: profileImageSize, height: profileImageSize)
+ .cornerRadius(profileImageSize / 2)
+ }, placeholder: {
+ ProgressView()
+ .frame(width: profileImageSize, height: profileImageSize)
+ })
+ .padding([.all], 5)
+ }
+ VStack {
+ Text(verbatim: blogPost.title)
+ .font(.body)
+ .lineLimit(2)
+ }
+ Spacer()
+ }
+ HStack {
+ Text("\(meta.hasVideo ? "Video" : meta.hasAudio ? "Audio" : meta.hasGallery ? "Gallery" : "Picture")")
+ .font(.caption2)
+ .padding([.all], 7)
+ .foregroundColor(.white)
+ .background(.gray)
+ .cornerRadius(10)
+
+ if case .home(_) = viewOrigin {
+ Text(verbatim: blogPost.creator.title)
+ .font(.system(size: 18, weight: .light))
+ }
+ Spacer()
+ Text("\(relativeTimeConverter.localizedString(for: blogPost.releaseDate, relativeTo: Date()))")
+ .lineLimit(1)
+ }
+ }.onTapGesture {
+ self.clickedOnVideo.toggle()
+ }
+ }
+}
+
+struct BlogPostSelectionView_Previews: PreviewProvider {
+ static var previews: some View {
+ ScrollView {
+ BlogPostSelectionView(
+ blogPost: MockData.blogPosts.blogPosts.first!,
+ viewOrigin: .home(MockData.creatorOwners.users.first!.user),
+ watchProgresses: FetchRequest(entity: WatchProgress.entity(), sortDescriptors: [], predicate: NSPredicate(format: "blogPostId = %@", MockData.blogPosts.blogPosts.first!.id), animation: .default)
+ )
+ .environment(\.fpApiService, MockFPAPIService())
+ .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
+ }
+ .padding(.all)
+ }
+}
diff --git a/Shared/SupplementalViews/VideoPlayerView.swift b/Shared/SupplementalViews/VideoPlayerView.swift
new file mode 100644
index 0000000..19da23d
--- /dev/null
+++ b/Shared/SupplementalViews/VideoPlayerView.swift
@@ -0,0 +1,165 @@
+//
+// VideoPlayerView.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 26/06/2022.
+//
+
+import AVKit
+import FloatplaneAPIClient
+import SwiftUI
+import Logging
+
+struct VideoPlayerView: View {
+ @ObservedObject var viewModel: VideoViewModel
+
+ @Environment(\.screenWidth) var videoWidth: CGFloat
+
+ let orientationChanged = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
+ .makeConnectable()
+ .autoconnect()
+
+ let content: CdnDeliveryV2Response
+ let beginningWatchTime: Double
+
+ // Get the aspect ratio of the video content
+ var aspectRatio: CGFloat {
+ let height = content.resource.data.qualityLevels?.first?.height ?? 9
+ let width = content.resource.data.qualityLevels?.first?.width ?? 16
+ let aspectRatio = CGFloat(CGFloat(height) / CGFloat(width))
+ return aspectRatio
+ }
+
+ var body: some View {
+ VideoPlayerViewWrapped(viewModel: viewModel, content: content, beginningWatchTime: beginningWatchTime)
+ .frame(width: self.videoWidth, height: aspectRatio * self.videoWidth)
+ }
+}
+
+private var playerItemContext = 0
+
+struct VideoPlayerViewWrapped: UIViewControllerRepresentable {
+ @AppStorage("DesiredQuality") var desiredQuality: String = ""
+ @ObservedObject var viewModel: VideoViewModel
+ @Environment(\.managedObjectContext) var managedObjectContext
+
+ let content: CdnDeliveryV2Response
+ let beginningWatchTime: Double
+
+ let logger: Logger = {
+ var logger = Wasserflug.logger
+ logger[metadataKey: "class"] = "\(Self.Type.self)"
+ return logger
+ }()
+
+ func makeUIViewController(context: Context) -> ExtendedAVPlayerViewController {
+ let vc = ExtendedAVPlayerViewController()
+ vc.delegate = context.coordinator
+ let playerItem = viewModel.createAVPlayerItem(desiredQuality: desiredQuality)
+ if beginningWatchTime > 0.0, beginningWatchTime < 1.0 {
+ let totalSeconds = viewModel.videoAttachment.duration
+ let percentageOfTotal = beginningWatchTime
+ let seekToSeconds = totalSeconds * percentageOfTotal
+ let newCMTime = CMTime(seconds: seekToSeconds, preferredTimescale: 1)
+ playerItem.seek(to: newCMTime, completionHandler: nil)
+ }
+ vc.player = AVPlayer(playerItem: playerItem)
+ vc.player?.addObserver(context.coordinator, forKeyPath: #keyPath(AVPlayer.rate), context: nil)
+ vc.player?.play() // Needs to play when it first appears.
+ return vc
+ }
+
+ func updateUIViewController(_ uiViewController: ExtendedAVPlayerViewController, context: Context) {}
+
+ func makeCoordinator() -> Coordinator {
+ return Coordinator(self)
+ }
+
+ class Coordinator: NSObject, AVPlayerViewControllerDelegate {
+ let parent: VideoPlayerViewWrapped
+
+ init(_ parent: VideoPlayerViewWrapped) {
+ self.parent = parent
+ }
+
+ func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
+ let extendedPlayerViewController = playerViewController as! ExtendedAVPlayerViewController
+ extendedPlayerViewController.isStatusBarHidden = false
+ }
+
+ func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
+ let extendedPlayerViewController = playerViewController as! ExtendedAVPlayerViewController
+ extendedPlayerViewController.isStatusBarHidden = true
+ }
+
+ override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
+ guard let player = object as? AVPlayer else {
+ return;
+ }
+ if keyPath != #keyPath(AVPlayer.rate) {
+ return;
+ }
+ let fetchRequest = WatchProgress.fetchRequest()
+ let totalDurationSeconds = parent.viewModel.videoAttachment.duration
+ let blogPostId = parent.viewModel.contentPost.id
+ let videoId = parent.viewModel.videoAttachment.id
+ let progress: Double
+ if let lastPlayerItem = player.currentItem {
+ progress = Double(lastPlayerItem.currentTime().seconds) / totalDurationSeconds
+ } else {
+ progress = 0.0
+ }
+
+ fetchRequest.predicate = NSPredicate(format: "blogPostId = %@ and videoId = %@", blogPostId, videoId)
+
+ parent.logger.notice("Attempting to record watch progress for a video.", metadata: [
+ "blogPostId": "\(blogPostId)",
+ "videoId": "\(videoId)",
+ "totalDurationSeconds": "\(totalDurationSeconds)",
+ "progress": "\(progress)",
+ ])
+
+ if let fetchResult = (try? parent.managedObjectContext.fetch(fetchRequest))?.first {
+ parent.logger.info("Did find previous watchProgress. Will mutate with new progress.", metadata: [
+ "previous": "\(String(reflecting: fetchResult))",
+ ])
+ fetchResult.progress = progress
+ } else {
+ parent.logger.info("No previous watchProgress found. Will create new WatchProgress entity.")
+ let newWatchProgress = WatchProgress(context: parent.managedObjectContext)
+ newWatchProgress.blogPostId = blogPostId
+ newWatchProgress.videoId = videoId
+ newWatchProgress.progress = progress
+ }
+
+ do {
+ try parent.managedObjectContext.save()
+ parent.logger.info("Successfully saved watch progress for \(blogPostId) \(videoId)")
+ } catch {
+ parent.logger.error("Error saving watch progress for for \(blogPostId) \(videoId): \(String(reflecting: error))")
+ // Otherwise, this has minimal impact on the user of this application.
+ // No need to further handle the error.
+ }
+
+ if player.rate > 0.0 {
+ // Playback started
+ } else {
+ // Playback stopped
+ }
+ }
+ }
+}
+
+class ExtendedAVPlayerViewController: AVPlayerViewController {
+ var isStatusBarHidden: Bool = false
+
+ override var prefersStatusBarHidden: Bool {
+ return isStatusBarHidden
+ }
+}
+
+// struct VideoPlayerView_Previews: PreviewProvider {
+// static var previews: some View {
+// VideoPlayerView()
+// }
+// }
diff --git a/Shared/SupplementalViews/VisualEffectView.swift b/Shared/SupplementalViews/VisualEffectView.swift
new file mode 100644
index 0000000..4f254a9
--- /dev/null
+++ b/Shared/SupplementalViews/VisualEffectView.swift
@@ -0,0 +1,14 @@
+//
+// VisualEffectView.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import SwiftUI
+
+struct VisualEffectView: UIViewRepresentable {
+ var effect: UIVisualEffect?
+ func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { UIVisualEffectView() }
+ func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { uiView.effect = effect }
+}
diff --git a/Shared/Utilities/FPColors.swift b/Shared/Utilities/FPColors.swift
new file mode 100644
index 0000000..767ba62
--- /dev/null
+++ b/Shared/Utilities/FPColors.swift
@@ -0,0 +1,14 @@
+//
+// FPColors.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import Foundation
+import SwiftUI
+
+enum FPColors {
+ static let blue = Color(.sRGB, red: 0, green: 175.0/256.0, blue: 236.0/256.0, opacity: 1.0)
+ static let darkBlue = Color(.sRGB, red: 0, green: 175.0/256.0, blue: 236.0/256.0, opacity: 1.0)
+}
diff --git a/Shared/Views/BlogPostView.swift b/Shared/Views/BlogPostView.swift
new file mode 100644
index 0000000..7866f0b
--- /dev/null
+++ b/Shared/Views/BlogPostView.swift
@@ -0,0 +1,164 @@
+//
+// BlogPostView.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import SwiftUI
+import CachedAsyncImage
+
+struct BlogPostView: View {
+ @StateObject var viewModel: BlogPostViewModel
+
+ @Environment(\.fpApiService) var fpApiService
+ @Environment(\.colorScheme) var colorScheme
+
+ @EnvironmentObject var userInfo: UserInfo
+
+ @FetchRequest var watchProgresses: FetchedResults
+
+ var progress: CGFloat {
+ if let watchProgress = watchProgresses.first {
+ let progress = watchProgress.progress
+ return progress >= 0.95 ? 1.0 : progress
+ } else {
+ return 0.0
+ }
+ }
+
+ var formatter: DateFormatter {
+ let formatter = DateFormatter()
+ formatter.dateStyle = .long
+ formatter.timeStyle = .medium
+ return formatter
+ }
+
+ // Ignore, just make it compatible with the tvOS version
+ var shouldAutoPlay: Bool
+
+ var body: some View {
+ NavigationView {
+ ZStack {
+ VStack {
+ switch viewModel.state {
+ case .idle:
+ Spacer()
+ ProgressView().onAppear(perform: {
+ viewModel.load(colorScheme: colorScheme)
+ })
+ Spacer()
+ case .loading:
+ Spacer()
+ ProgressView()
+ Spacer()
+ case let .failed(error):
+ ErrorView(error: error)
+ case let .loaded(content):
+ ScrollView {
+ if let videoAttachments = content.videoAttachments, let firstVideo = videoAttachments.first {
+ VideoView(viewModel: VideoViewModel(fpApiService: fpApiService, videoAttachment: firstVideo, contentPost: content, description: viewModel.textAttributedString), beginningWatchTime: progress)
+ } else {
+ CachedAsyncImage(url: content.thumbnail.pathUrlOrNil) { image in
+ ZStack {
+ image
+ .resizable()
+ .scaledToFit()
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .blur(radius: 1.5)
+ VStack {
+ Spacer()
+ Image(systemName: "play.slash")
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 42, height: 42)
+ .background(Circle().inset(by: -10).foregroundColor(.red))
+ Spacer()
+ }
+ }
+ } placeholder: {
+ VStack {
+ Spacer()
+ ProgressView()
+ Spacer()
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ }
+ HStack {
+ Text(content.title)
+ .font(.headline)
+ Spacer()
+ }
+ .padding(.horizontal)
+ HStack {
+ let profileImageSize: CGFloat = 35
+ if let user = userInfo.creatorOwners[content.creator.owner], let path = user.profileImage.path {
+ CachedAsyncImage(url: URL(string: path), content: { image in
+ image
+ .resizable()
+ .scaledToFit()
+ .frame(width: profileImageSize, height: profileImageSize)
+ .cornerRadius(profileImageSize / 2)
+ }, placeholder: {
+ ProgressView()
+ .frame(width: profileImageSize, height: profileImageSize)
+ })
+ .padding([.all], 5)
+ }
+ VStack {
+ HStack {
+ Text(verbatim: content.creator.title)
+ .font(.body)
+ .lineLimit(1)
+ Spacer()
+ }
+ HStack {
+ Text(content.releaseDate, formatter: self.formatter)
+ .font(.caption2)
+ Spacer()
+ }
+ }
+ HStack(alignment: .center) {
+ Button {
+ viewModel.like()
+ } label: {
+ let additional = viewModel.isLiked && viewModel.latestUserInteraction != nil ? 1 : 0
+ Label("\(content.likes + additional)", systemImage: "hand.thumbsup")
+ }
+ .foregroundColor(viewModel.isLiked ? .green : colorScheme == .light ? .gray : .white)
+ Button {
+ viewModel.dislike()
+ } label: {
+ let additional = viewModel.isDisliked && viewModel.latestUserInteraction != nil ? 1 : 0
+ Label("\(content.likes + additional)", systemImage: "hand.thumbsdown")
+ }
+ .foregroundColor(viewModel.isDisliked ? .red : colorScheme == .light ? .gray : .white)
+ }
+ Spacer()
+ }
+ .padding(.horizontal)
+ }
+ }
+ }
+
+ // Blur navigation bar to recreate the translucent effect
+ VStack {
+ VisualEffectView(effect: UIBlurEffect(style: colorScheme == .light ? .light : .dark))
+ .ignoresSafeArea(edges: .top)
+ .frame(height: 0)
+
+ Spacer()
+ }
+ }
+ }
+ .navigationViewStyle(.stack)
+ }
+}
+
+struct BlogPostView_Previews: PreviewProvider {
+ static var previews: some View {
+ BlogPostView(viewModel: BlogPostViewModel(fpApiService: MockFPAPIService(), id: ""), watchProgresses: FetchRequest(entity: WatchProgress.entity(), sortDescriptors: [], predicate: NSPredicate(format: "blogPostId = %@", MockData.blogPosts.blogPosts.first!.id), animation: .default), shouldAutoPlay: false)
+ .environmentObject(MockData.userInfo)
+ }
+}
diff --git a/Shared/Views/CreatorContentView.swift b/Shared/Views/CreatorContentView.swift
new file mode 100644
index 0000000..30cf02e
--- /dev/null
+++ b/Shared/Views/CreatorContentView.swift
@@ -0,0 +1,229 @@
+//
+// CreatorContentView.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import SwiftUI
+import FloatplaneAPIClient
+import CachedAsyncImage
+
+struct CreatorContentView: View {
+ @EnvironmentObject var userInfo: UserInfo
+ @Environment(\.fpApiService) var fpApiService
+ @Environment(\.colorScheme) var colorScheme
+
+ @StateObject var viewModel: CreatorContentViewModel
+ @StateObject var livestreamViewModel: LivestreamViewModel
+
+ @State var isShowingSearch = false
+ @State var isShowingLive = false
+
+ var gridColumns: [GridItem] = [
+ GridItem(.adaptive(minimum: 370))
+ ]
+
+ var body: some View {
+ VStack {
+ switch viewModel.state {
+ case .idle:
+ ProgressView().onAppear(perform: {
+ viewModel.load()
+ livestreamViewModel.load()
+ livestreamViewModel.startLoadingLiveStatus()
+ })
+ case .loading:
+ ProgressView()
+ case let .failed(error):
+ ErrorView(error: error)
+ case let .loaded(content):
+ ScrollView {
+ GeometryReader { geometry in
+ ZStack {
+ CachedAsyncImage(url: viewModel.coverImagePath, content: { image in
+ if geometry.frame(in: .global).minY <= 0 {
+ image
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: geometry.size.width, height: geometry.size.height)
+ //.offset(y: geometry.frame(in: .global).minY/9)
+ .clipped()
+ } else {
+ image
+ .resizable()
+ .aspectRatio(contentMode: .fill)
+ .frame(width: geometry.size.width, height: geometry.size.height + geometry.frame(in: .global).minY)
+ .clipped()
+ .offset(y: -geometry.frame(in: .global).minY)
+ }
+ }, placeholder: {
+ ProgressView()
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .aspectRatio(viewModel.creator.cover?.aspectRatio ?? 1.0, contentMode: .fit)
+ })
+ }
+ }
+ .frame(height: viewModel.coverImageWidth != nil && viewModel.coverImageHeight != nil ? ( UIScreen.main.bounds.size.width / viewModel.coverImageWidth!) * viewModel.coverImageHeight! : 150)
+
+ // Logo and text
+
+ let offset: CGFloat = 70 / 2
+
+ HStack {
+ let logoSize: CGFloat = 70
+
+ // isLive circle stroke width
+ let border: CGFloat = 3
+
+ // isLive circle gradient
+
+ let gradient = Gradient(colors: [Color(red: 0.07, green: 0.76, blue: 0.91), Color(red: 0.77, green: 0.44, blue: 0.93), Color(red: 0.96, green: 0.31, blue: 0.35)])
+
+ let linearGradient = LinearGradient(gradient: gradient, startPoint: .topLeading, endPoint: .bottomTrailing)
+
+ CachedAsyncImage(url: viewModel.creatorProfileImagePath, content: { image in
+ image
+ .resizable()
+ .frame(width: logoSize, height: logoSize)
+ .clipShape(Circle())
+
+ }, placeholder: {
+ ProgressView()
+ .frame(width: logoSize, height: logoSize)
+ })
+ // isLive circle
+ .if(self.livestreamViewModel.isLive) { view in
+ view.overlay(
+ ZStack {
+ Circle()
+ .inset(by: -((border / 2) + 2))
+ .stroke(linearGradient, lineWidth: border)
+ .frame(width: logoSize, height: logoSize)
+ .animation(.easeInOut(duration: 1.5).repeatForever(autoreverses: true), value: true)
+ HStack {
+ Spacer()
+ VStack {
+ Spacer()
+ Image(systemName: "livephoto.play")
+ .foregroundStyle(linearGradient)
+ .offset(x: 5, y: 5)
+ }
+ }
+ }
+ )
+ }
+ .sheet(isPresented: $isShowingLive, onDismiss: {
+ self.isShowingLive = false
+ }, content: {
+ LivestreamPlayerView(viewModel: self.livestreamViewModel)
+ .edgesIgnoringSafeArea(.all)
+ })
+ .offset(y: -offset)
+ .onTapGesture {
+ if self.livestreamViewModel.isLive {
+ self.isShowingLive = true
+ }
+ }
+
+ VStack {
+ Text(viewModel.creatorAboutHeader)
+ .fontWeight(.bold)
+ .font(.system(.headline))
+ .lineLimit(1)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ Spacer()
+ }
+ Spacer()
+ }
+ .padding(.horizontal)
+
+ HStack {
+ Text(viewModel.creatorAboutBody)
+ .lineLimit(2)
+ .frame(maxWidth: .infinity, alignment: .leading)
+ }
+ .padding(.horizontal)
+ .offset(y: -offset)
+
+
+
+ // Videos
+
+ LazyVGrid(columns: gridColumns, alignment: .center, spacing: 15) {
+ ForEach(content) { blogPost in
+ BlogPostSelectionView(
+ blogPost: blogPost,
+ viewOrigin: .creator,
+ watchProgresses: FetchRequest(entity: WatchProgress.entity(), sortDescriptors: [], predicate: NSPredicate(format: "blogPostId = %@", blogPost.id), animation: .default)
+ )
+ .onAppear(perform: {
+ viewModel.itemDidAppear(blogPost)
+ })
+ }
+ }
+ .padding(.horizontal)
+ .offset(y: -offset)
+ }
+ .onDisappear {
+ viewModel.creatorContentDidDisappear()
+ }.onAppear {
+ viewModel.creatorContentDidAppearAgain()
+ livestreamViewModel.loadLiveStatus()
+ }
+ }
+ }
+ }
+
+}
+
+struct CreatorContentView_Previews: PreviewProvider {
+ @State static var selection = RootTabView.Selection.creator(MockData.creators[0].id)
+ static var previews: some View {
+ Group {
+ TabView(selection: $selection) {
+ Text("test").tabItem {
+ Text("Home")
+ }
+ CreatorContentView(viewModel: CreatorContentViewModel(
+ fpApiService: MockFPAPIService(),
+ creator: MockData.creators[0],
+ creatorOwner: MockData.creatorOwners.users[0].user
+ ), livestreamViewModel: LivestreamViewModel(
+ fpApiService: MockFPAPIService(),
+ creator: MockData.creators[0])
+ )
+ .tag(RootTabView.Selection.creator(MockData.creators[0].id))
+ .tabItem {
+ Text(MockData.creators[0].title)
+ }
+ .environmentObject(MockData.userInfo)
+ Text("test").tabItem {
+ Text("Settings")
+ }
+ }
+ TabView(selection: $selection) {
+ Text("test").tabItem {
+ Text("Home")
+ }
+ CreatorContentView(viewModel: CreatorContentViewModel(
+ fpApiService: MockFPAPIService(),
+ creator: MockData.creators[0],
+ creatorOwner: MockData.creatorOwners.users[0].user
+ ), livestreamViewModel: LivestreamViewModel(
+ fpApiService: MockFPAPIService(),
+ creator: MockData.creators[0], mockIsLive: true)
+ )
+ .tag(RootTabView.Selection.creator(MockData.creators[0].id))
+ .tabItem {
+ Text(MockData.creators[0].title)
+ }
+ .environmentObject(MockData.userInfo)
+ Text("test").tabItem {
+ Text("Settings")
+ }
+ }
+ }
+ }
+}
+
diff --git a/Shared/Views/CreatorSearchView.swift b/Shared/Views/CreatorSearchView.swift
new file mode 100644
index 0000000..9862f7d
--- /dev/null
+++ b/Shared/Views/CreatorSearchView.swift
@@ -0,0 +1,56 @@
+//
+// CreatorSearchView.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 23.06.22.
+//
+
+import SwiftUI
+
+struct CreatorSearchView: View {
+
+ @StateObject var viewModel: CreatorContentViewModel
+
+ let creatorName: String
+
+ private var gridColumns: [GridItem] {
+ return Array(repeating: GridItem(.flexible(minimum: 0, maximum: .infinity), alignment: .top), count: UIDevice.current.userInterfaceIdiom == .pad ? 2 : 1)
+ }
+
+ var body: some View {
+ NavigationView {
+ ScrollView {
+ switch viewModel.state {
+ case .idle:
+ EmptyView()
+ case .loading:
+ ProgressView()
+ case let .failed(error):
+ ErrorView(error: error)
+ case let .loaded(content):
+ LazyVGrid(columns: gridColumns, spacing: 60) {
+ ForEach(content) { blogPost in
+ BlogPostSelectionView(
+ blogPost: blogPost,
+ viewOrigin: .creator,
+ watchProgresses: FetchRequest(entity: WatchProgress.entity(), sortDescriptors: [], predicate: NSPredicate(format: "blogPostId = %@", blogPost.id), animation: .default)
+ )
+ .onAppear(perform: {
+ viewModel.itemDidAppear(blogPost)
+ })
+ }
+ }
+ .padding(40)
+ }
+ }
+ .searchable(text: $viewModel.searchText, prompt: "Search for Video")
+ .navigationTitle(self.creatorName)
+ }
+ }
+}
+
+struct CreatorSearchView_Previews: PreviewProvider {
+ static var previews: some View {
+ CreatorSearchView(viewModel: CreatorContentViewModel(fpApiService: MockFPAPIService(), creator: MockData.creators.first!, creatorOwner: MockData.creatorOwners.users[0].user), creatorName: "Linus Tech Tips")
+ }
+}
diff --git a/Shared/Views/HomeView.swift b/Shared/Views/HomeView.swift
new file mode 100644
index 0000000..3e59dbe
--- /dev/null
+++ b/Shared/Views/HomeView.swift
@@ -0,0 +1,61 @@
+//
+// HomeView.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import SwiftUI
+
+struct HomeView: View {
+ @StateObject var viewModel: HomeViewModel
+
+ @EnvironmentObject var userInfo: UserInfo
+
+ var gridColumns: [GridItem] = [
+ GridItem(.adaptive(minimum: 370))
+ ]
+
+ var body: some View {
+ NavigationView {
+ switch viewModel.state {
+ case .idle:
+ Color.clear.onAppear(perform: {
+ viewModel.load()
+ })
+ case .loading:
+ ProgressView()
+ case let .failed(error):
+ ErrorView(error: error)
+ case let .loaded(response):
+ ScrollView {
+ LazyVGrid(columns: self.gridColumns, alignment: .center, spacing: 15) {
+ ForEach(response.blogPosts) { blogPost in
+ BlogPostSelectionView(
+ blogPost: blogPost,
+ viewOrigin: .home(userInfo.creatorOwners[blogPost.creator.owner.id]),
+ watchProgresses: FetchRequest(entity: WatchProgress.entity(), sortDescriptors: [], predicate: NSPredicate(format: "blogPostId = %@", blogPost.id), animation: .default)
+ )
+ .onAppear(perform: {
+ viewModel.itemDidAppear(blogPost)
+ })
+ }
+ }
+ .padding()
+ }.onDisappear {
+ viewModel.homeDidDisappear()
+ }.onAppear {
+ viewModel.homeDidAppearAgain()
+ }
+ }
+ }
+ .navigationViewStyle(.stack)
+ }
+}
+
+struct HomeView_Previews: PreviewProvider {
+ static var previews: some View {
+ HomeView(viewModel: HomeViewModel(userInfo: MockData.userInfo, fpApiService: MockFPAPIService()))
+ .environmentObject(MockData.userInfo)
+ }
+}
diff --git a/Shared/Views/LoginView.swift b/Shared/Views/LoginView.swift
new file mode 100644
index 0000000..2eb1d75
--- /dev/null
+++ b/Shared/Views/LoginView.swift
@@ -0,0 +1,142 @@
+//
+// LoginView.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import SwiftUI
+
+struct LoginView: View {
+ @ObservedObject var viewModel: AuthViewModel
+
+ @State var username: String = ""
+ @State var password: String = ""
+ @State var secondFactorCode: String = ""
+
+ @Environment(\.colorScheme) var colorScheme
+
+ private enum Field: Hashable {
+ case usernameField
+ case passwordField
+ case secondFactorField
+ case loginButton
+ }
+ @FocusState private var focusedField: Field?
+
+ var body: some View {
+ NavigationView {
+ List {
+ // Logo
+ HStack {
+ Spacer()
+ Image("wasserflug-logo")
+ .resizable()
+ .foregroundColor(colorScheme == .dark ? .white : .black)
+ .scaledToFit()
+ .frame(maxWidth: 250)
+ Spacer()
+ }
+ .listRowBackground(EmptyView())
+ .listRowSeparator(.hidden)
+
+ // Login Form
+ Section(header: Text("Credentials")) {
+
+ TextField("Username", text: $username)
+ .textInputAutocapitalization(.never)
+ .textContentType(.username)
+ .focused($focusedField, equals: .usernameField)
+ .disabled(viewModel.isAttemptingLogin || viewModel.needsSecondFactor || viewModel.isAttemptingSecondFactor)
+
+ SecureField("Password", text: $password)
+ .textContentType(.password)
+ .focused($focusedField, equals: .passwordField)
+ .onSubmit {
+ withAnimation {
+ self.login()
+ }
+ }
+ .submitLabel(SubmitLabel.send)
+ .disabled(viewModel.isAttemptingLogin || viewModel.needsSecondFactor || viewModel.isAttemptingSecondFactor)
+
+ if viewModel.needsSecondFactor {
+ TextField("2FA Code", text: $secondFactorCode)
+ .textContentType(.oneTimeCode)
+ .disabled(viewModel.isAttemptingSecondFactor)
+ }
+
+ Button(action: {
+ withAnimation {
+ self.login()
+ }
+ }, label: {
+ HStack {
+ if viewModel.isAttemptingLogin || viewModel.isAttemptingSecondFactor {
+ ProgressView()
+ }
+ if viewModel.needsSecondFactor {
+ Text("Submit 2FA ✈️")
+ } else {
+ Text("Login ✈️")
+ }
+ }
+ })
+ .focused($focusedField, equals: .loginButton)
+ .disabled(viewModel.isAttemptingLogin || viewModel.isAttemptingSecondFactor)
+
+ }
+ }
+ .listStyle(.insetGrouped)
+ .navigationTitle("Login")
+ }
+ .alert("Login", isPresented: $viewModel.showIncorrectLoginAlert, actions: { }, message: {
+ if let error = viewModel.loginError {
+ Text("""
+There was an error while attempting to log in. Please submit a bug report with the app developer, *NOT* with Floatplane staff.
+
+\(error.localizedDescription)
+""")
+ } else {
+ Text("""
+Username or password is incorrect.
+If you have forgotten your password, please reset it via https://www.floatplane.com/reset-password
+""")
+ }
+ })
+ .navigationViewStyle(.stack)
+ }
+
+ func login() {
+ if username.isEmpty {
+ focusedField = .usernameField
+ } else if password.isEmpty {
+ focusedField = .passwordField
+ } else {
+ if viewModel.needsSecondFactor, secondFactorCode.isEmpty {
+ focusedField = .secondFactorField
+ } else if viewModel.needsSecondFactor {
+ viewModel.attemptSecondFactor(secondFactorCode: secondFactorCode) {
+ viewModel.determineAuthenticationStatus()
+ print("Second Factor correct")
+ }
+ } else {
+ viewModel.attemptLogin(username: username, password: password) {
+ viewModel.determineAuthenticationStatus()
+ print("Logged in")
+ }
+ }
+ print("Test")
+ }
+ }
+}
+
+struct LoginView_Previews: PreviewProvider {
+
+ static var previews: some View {
+ Group {
+ LoginView(viewModel: AuthViewModel(fpApiService: MockFPAPIService()))
+ .previewInterfaceOrientation(.portrait)
+ }
+ }
+}
diff --git a/Shared/Views/SettingsView.swift b/Shared/Views/SettingsView.swift
new file mode 100644
index 0000000..d9e298e
--- /dev/null
+++ b/Shared/Views/SettingsView.swift
@@ -0,0 +1,59 @@
+//
+// SettingsView.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import SwiftUI
+import CachedAsyncImage
+import FloatplaneAPIClient
+
+struct SettingsView: View {
+ @EnvironmentObject var userInfo: UserInfo
+
+ var body: some View {
+ NavigationView {
+ List {
+ if let userSelf = userInfo.userSelf, let imageUrl = URL(string: userSelf.profileImage.path) {
+ HStack {
+ Spacer()
+ HStack(spacing: 20) {
+ let pfpSize: CGFloat = 150
+ CachedAsyncImage(url: imageUrl, content: { image in
+ image
+ .resizable()
+ .frame(width: pfpSize, height: pfpSize)
+ }, placeholder: {
+ ProgressView()
+ .frame(width: pfpSize, height: pfpSize)
+ })
+ }
+ Spacer()
+ }
+ .listRowBackground(EmptyView())
+ Section {
+ Text(verbatim: userSelf.username)
+ Button("Logout") {
+ FloatplaneAPIClientAPI.removeAuthenticationCookies()
+ NotificationCenter.default.post(name: ContentView.Notifications.loggedOut, object: nil)
+ }
+ } header: {
+ Text("User")
+ }
+ .listStyle(.insetGrouped)
+
+ }
+ }
+ .navigationTitle("Settings")
+ }
+ .navigationViewStyle(.stack)
+ }
+}
+
+struct SettingsView_Previews: PreviewProvider {
+ static var previews: some View {
+ SettingsView()
+ .environmentObject(MockData.userInfo)
+ }
+}
diff --git a/Shared/Views/VideoView.swift b/Shared/Views/VideoView.swift
new file mode 100644
index 0000000..612171e
--- /dev/null
+++ b/Shared/Views/VideoView.swift
@@ -0,0 +1,60 @@
+//
+// VideoView.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 28/06/2022.
+//
+
+import SwiftUI
+import AVKit
+import FloatplaneAPIClient
+import CachedAsyncImage
+
+struct VideoView: View {
+
+ @StateObject var viewModel: VideoViewModel
+ let beginningWatchTime: Double
+
+ var body: some View {
+ switch viewModel.state {
+ case .idle:
+ Spacer()
+ ProgressView().onAppear(perform: {
+ viewModel.load()
+ })
+ Spacer()
+ case .loading:
+ CachedAsyncImage(url: self.viewModel.contentPost.thumbnail.pathUrlOrNil) { image in
+ ZStack {
+ image
+ .resizable()
+ .scaledToFit()
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .blur(radius: 2)
+ VStack {
+ Spacer()
+ ProgressView()
+ Spacer()
+ }
+ }
+ } placeholder: {
+ VStack {
+ Spacer()
+ ProgressView()
+ Spacer()
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ case let .failed(error):
+ ErrorView(error: error)
+ case let .loaded(content):
+ VideoPlayerView(viewModel: viewModel, content: content, beginningWatchTime: beginningWatchTime)
+ }
+ }
+}
+
+struct VideoView_Previews: PreviewProvider {
+ static var previews: some View {
+ VideoView(viewModel: VideoViewModel(fpApiService: MockFPAPIService(), videoAttachment: MockData.getBlogPost.videoAttachments!.first!, contentPost: MockData.getBlogPost, description: "Test description"), beginningWatchTime: 0.0)
+ }
+}
diff --git a/Shared/Wasserflug.swift b/Shared/Wasserflug.swift
new file mode 100644
index 0000000..2a0a196
--- /dev/null
+++ b/Shared/Wasserflug.swift
@@ -0,0 +1,82 @@
+//
+// Wasserflug.swift
+// Wasserflug-tvOS
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import Foundation
+import Vapor
+import FloatplaneAPIClient
+
+struct Wasserflug {
+ static var logger: Logging.Logger {
+ var logger = Logging.Logger(label: Bundle.main.bundleIdentifier!)
+ #if DEBUG
+ // For debugging, log at a lower level to get more information.
+ logger.logLevel = .debug
+ #else
+ // For release mode, only log important items.
+ logger.logLevel = .notice
+ #endif
+ return logger
+ }
+ static var networkLogger: Logging.Logger {
+ var logger = Logging.Logger(label: Bundle.main.bundleIdentifier!)
+ logger.logLevel = .info
+ logger[metadataKey: "category"] = "network"
+ return logger
+ }
+
+ let vaporApp = Vapor.Application(.production, .createNew)
+ let fpApiService: FPAPIService = DefaultFPAPIService()
+ let authViewModel: AuthViewModel
+ let persistenceController = PersistenceController.shared
+
+ init() {
+ // Set custom user agent for network requests. This is particularly
+ // required in order to pass the login phase with bypassing the captcha.
+ Wasserflug.setHttp(header: "User-Agent", value: "Wasserflug tvOS App, CFNetwork")
+
+ // Attempt to use any previous authentication cookies, so the user does
+ // not need to login on every app start.
+ FloatplaneAPIClientAPI.loadAuthenticationCookiesFromStorage()
+
+ // Use FP's date format for JSON encoding/decoding.
+ let fpDateFormatter = DateFormatter()
+ fpDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
+ Configuration.contentConfiguration.use(encoder: JSONEncoder.custom(dates: .formatted(fpDateFormatter)), for: .json)
+ Configuration.contentConfiguration.use(decoder: JSONDecoder.custom(dates: .formatted(fpDateFormatter)), for: .json)
+
+ // Bootstrap core logging.
+ LoggingSystem.bootstrap({ (label) -> LogHandler in
+ var loggingLogger = OSLoggingLogger(label: label, category: "Wasserflug")
+ loggingLogger.logLevel = .debug
+ return MultiplexLogHandler([
+ loggingLogger,
+ ])
+ })
+
+ // Bootstrap API/Network logging.
+ Configuration.apiClient = vaporApp.client
+ .logging(to: Wasserflug.networkLogger)
+ Configuration.apiWrapper = { clientRequest in
+ Wasserflug.networkLogger.info("Sending \(clientRequest.method) request to \(clientRequest.url)")
+ }
+
+ // Create and store in @State the main view model.
+ authViewModel = AuthViewModel(fpApiService: fpApiService)
+ }
+
+ private static func setHttp(header: String, value: String) {
+ FloatplaneAPIClientAPI.customHeaders.replaceOrAdd(name: header, value: value)
+ if var headers = URLSession.shared.configuration.httpAdditionalHeaders {
+ headers[header] = value
+ URLSession.shared.configuration.httpAdditionalHeaders = headers
+ } else {
+ URLSession.shared.configuration.httpAdditionalHeaders = [
+ header: value,
+ ]
+ }
+ }
+}
diff --git a/Shared/WasserflugApp.swift b/Shared/WasserflugApp.swift
new file mode 100644
index 0000000..a446c71
--- /dev/null
+++ b/Shared/WasserflugApp.swift
@@ -0,0 +1,21 @@
+//
+// WasserflugApp.swift
+// Shared
+//
+// Created by Nils Bergmann on 22.06.22.
+//
+
+import SwiftUI
+
+@main
+struct WasserflugApp: App {
+ let wasserflug = Wasserflug();
+
+ var body: some Scene {
+ WindowGroup {
+ ContentView(viewModel: wasserflug.authViewModel)
+ .environment(\.fpApiService, wasserflug.fpApiService)
+ .environment(\.managedObjectContext, wasserflug.persistenceController.container.viewContext)
+ }
+ }
+}
diff --git a/Wasserflug--iOS--Info.plist b/Wasserflug--iOS--Info.plist
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/Wasserflug--iOS--Info.plist
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/Wasserflug-tvOS.xcodeproj/project.pbxproj b/Wasserflug-tvOS.xcodeproj/project.pbxproj
index 67ba292..5cae36f 100644
--- a/Wasserflug-tvOS.xcodeproj/project.pbxproj
+++ b/Wasserflug-tvOS.xcodeproj/project.pbxproj
@@ -27,7 +27,6 @@
E753379F27CB06BE0010E3D2 /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378227CB06BE0010E3D2 /* VideoPlayerView.swift */; };
E75337A027CB06BE0010E3D2 /* PlayButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378327CB06BE0010E3D2 /* PlayButton.swift */; };
E75337A127CB06BE0010E3D2 /* PlayMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378427CB06BE0010E3D2 /* PlayMediaView.swift */; };
- E75337A227CB06BE0010E3D2 /* BlogPostSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378527CB06BE0010E3D2 /* BlogPostSelectionView.swift */; };
E75337A327CB06BE0010E3D2 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378627CB06BE0010E3D2 /* ErrorView.swift */; };
E75337A427CB06BE0010E3D2 /* BlogPostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378827CB06BE0010E3D2 /* BlogPostViewModel.swift */; };
E75337A527CB06BE0010E3D2 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378927CB06BE0010E3D2 /* HomeViewModel.swift */; };
@@ -41,7 +40,6 @@
E75337AD27CB06BE0010E3D2 /* CreatorSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379227CB06BE0010E3D2 /* CreatorSearchView.swift */; };
E75337AE27CB06BE0010E3D2 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379327CB06BE0010E3D2 /* SettingsView.swift */; };
E75337AF27CB06BE0010E3D2 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379427CB06BE0010E3D2 /* LoginView.swift */; };
- E75337B027CB06BE0010E3D2 /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379527CB06BE0010E3D2 /* VideoView.swift */; };
E75337B127CB06BE0010E3D2 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379627CB06BE0010E3D2 /* HomeView.swift */; };
E75337B227CB06BE0010E3D2 /* PictureView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379727CB06BE0010E3D2 /* PictureView.swift */; };
E75337B327CB06BE0010E3D2 /* BlogPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379827CB06BE0010E3D2 /* BlogPostView.swift */; };
@@ -53,6 +51,91 @@
E75337BC27CB07900010E3D2 /* FloatplaneAPIClient in Frameworks */ = {isa = PBXBuildFile; productRef = E75337BB27CB07900010E3D2 /* FloatplaneAPIClient */; };
E75337BE27CB08990010E3D2 /* EnvironmentKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = E75337BD27CB08990010E3D2 /* EnvironmentKeys.swift */; };
E7A447E92815E1C0009BCE2E /* SwiftUI+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A447E82815E1C0009BCE2E /* SwiftUI+Extensions.swift */; };
+ FB09BD342868B74A00B52FED /* VideoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378A27CB06BE0010E3D2 /* VideoViewModel.swift */; };
+ FB09BD352868B74A00B52FED /* VideoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378A27CB06BE0010E3D2 /* VideoViewModel.swift */; };
+ FB09BD3C2868C5EC00B52FED /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB09BD3B2868C5EC00B52FED /* VideoPlayerView.swift */; };
+ FB09BD3D2868C5EC00B52FED /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB09BD3B2868C5EC00B52FED /* VideoPlayerView.swift */; };
+ FB18ED6F286880F100F53433 /* BlogPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB549286376FB006B4023 /* BlogPostView.swift */; };
+ FB457F8128649753008A82AF /* CreatorSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB457F8028649753008A82AF /* CreatorSearchView.swift */; };
+ FB457F8228649753008A82AF /* CreatorSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FB457F8028649753008A82AF /* CreatorSearchView.swift */; };
+ FB457F832864AB0D008A82AF /* LivestreamPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379A27CB06BE0010E3D2 /* LivestreamPlayerView.swift */; };
+ FB457F842864AB0D008A82AF /* LivestreamPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379A27CB06BE0010E3D2 /* LivestreamPlayerView.swift */; };
+ FBDDB4E5286339B2006B4023 /* WasserflugApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB4D6286339B0006B4023 /* WasserflugApp.swift */; };
+ FBDDB4E6286339B2006B4023 /* WasserflugApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB4D6286339B0006B4023 /* WasserflugApp.swift */; };
+ FBDDB4E7286339B2006B4023 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB4D7286339B0006B4023 /* ContentView.swift */; };
+ FBDDB4E8286339B2006B4023 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB4D7286339B0006B4023 /* ContentView.swift */; };
+ FBDDB4E9286339B2006B4023 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FBDDB4D8286339B2006B4023 /* Assets.xcassets */; };
+ FBDDB4EA286339B2006B4023 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FBDDB4D8286339B2006B4023 /* Assets.xcassets */; };
+ FBDDB4F2286339CD006B4023 /* FloatplaneAPIClient in Frameworks */ = {isa = PBXBuildFile; productRef = FBDDB4F1286339CD006B4023 /* FloatplaneAPIClient */; };
+ FBDDB4F4286339D4006B4023 /* FloatplaneAPIClient in Frameworks */ = {isa = PBXBuildFile; productRef = FBDDB4F3286339D4006B4023 /* FloatplaneAPIClient */; };
+ FBDDB4F728633A09006B4023 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB4F628633A09006B4023 /* LoginView.swift */; };
+ FBDDB4F828633A09006B4023 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB4F628633A09006B4023 /* LoginView.swift */; };
+ FBDDB4FD28633A57006B4023 /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378D27CB06BE0010E3D2 /* AuthViewModel.swift */; };
+ FBDDB4FE28633A57006B4023 /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378D27CB06BE0010E3D2 /* AuthViewModel.swift */; };
+ FBDDB4FF28633A89006B4023 /* FPAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753377427CB066E0010E3D2 /* FPAPIService.swift */; };
+ FBDDB50028633A89006B4023 /* FPAPIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753377427CB066E0010E3D2 /* FPAPIService.swift */; };
+ FBDDB50228633A93006B4023 /* Vapor in Frameworks */ = {isa = PBXBuildFile; productRef = FBDDB50128633A93006B4023 /* Vapor */; };
+ FBDDB50428633A99006B4023 /* Vapor in Frameworks */ = {isa = PBXBuildFile; productRef = FBDDB50328633A99006B4023 /* Vapor */; };
+ FBDDB50528633C04006B4023 /* BaseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378B27CB06BE0010E3D2 /* BaseViewModel.swift */; };
+ FBDDB50628633C05006B4023 /* BaseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378B27CB06BE0010E3D2 /* BaseViewModel.swift */; };
+ FBDDB50728633C45006B4023 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378627CB06BE0010E3D2 /* ErrorView.swift */; };
+ FBDDB50928633C47006B4023 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378627CB06BE0010E3D2 /* ErrorView.swift */; };
+ FBDDB50E28633E7C006B4023 /* OSLoggingLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753377627CB066E0010E3D2 /* OSLoggingLogger.swift */; };
+ FBDDB51028633E7E006B4023 /* OSLoggingLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753377627CB066E0010E3D2 /* OSLoggingLogger.swift */; };
+ FBDDB51228633F36006B4023 /* Wasserflug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB51128633F36006B4023 /* Wasserflug.swift */; };
+ FBDDB51328633F36006B4023 /* Wasserflug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB51128633F36006B4023 /* Wasserflug.swift */; };
+ FBDDB51428633F36006B4023 /* Wasserflug.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB51128633F36006B4023 /* Wasserflug.swift */; };
+ FBDDB51528633FCB006B4023 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = E71FDB7827E66D7E00A9C88B /* Persistence.swift */; };
+ FBDDB51628633FCB006B4023 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = E71FDB7827E66D7E00A9C88B /* Persistence.swift */; };
+ FBDDB51728633FEB006B4023 /* Wasserflug.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E71FDB7A27E66DB100A9C88B /* Wasserflug.xcdatamodeld */; };
+ FBDDB51828633FEC006B4023 /* Wasserflug.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = E71FDB7A27E66DB100A9C88B /* Wasserflug.xcdatamodeld */; };
+ FBDDB5192863400A006B4023 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753377027CB066E0010E3D2 /* Authentication.swift */; };
+ FBDDB51B2863400C006B4023 /* Authentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753377027CB066E0010E3D2 /* Authentication.swift */; };
+ FBDDB51C2863423F006B4023 /* FloatplaneAPIClient+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753377127CB066E0010E3D2 /* FloatplaneAPIClient+Extensions.swift */; };
+ FBDDB51E28634240006B4023 /* FloatplaneAPIClient+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753377127CB066E0010E3D2 /* FloatplaneAPIClient+Extensions.swift */; };
+ FBDDB52028634577006B4023 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E753374027CAFAFF0010E3D2 /* Assets.xcassets */; };
+ FBDDB52128634577006B4023 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E753374027CAFAFF0010E3D2 /* Assets.xcassets */; };
+ FBDDB52228635446006B4023 /* EnvironmentKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = E75337BD27CB08990010E3D2 /* EnvironmentKeys.swift */; };
+ FBDDB52328635447006B4023 /* EnvironmentKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = E75337BD27CB08990010E3D2 /* EnvironmentKeys.swift */; };
+ FBDDB52428635CD0006B4023 /* RootTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379D27CB06BE0010E3D2 /* RootTabView.swift */; };
+ FBDDB52528635CD1006B4023 /* RootTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379D27CB06BE0010E3D2 /* RootTabView.swift */; };
+ FBDDB52728635CEE006B4023 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB52628635CEE006B4023 /* HomeView.swift */; };
+ FBDDB52828635CEE006B4023 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB52628635CEE006B4023 /* HomeView.swift */; };
+ FBDDB52928635D04006B4023 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378927CB06BE0010E3D2 /* HomeViewModel.swift */; };
+ FBDDB52A28635D05006B4023 /* HomeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378927CB06BE0010E3D2 /* HomeViewModel.swift */; };
+ FBDDB52B28635D3C006B4023 /* ViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378E27CB06BE0010E3D2 /* ViewModelState.swift */; };
+ FBDDB52D28635D3F006B4023 /* ViewModelState.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378E27CB06BE0010E3D2 /* ViewModelState.swift */; };
+ FBDDB52F28635DA7006B4023 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB52E28635DA7006B4023 /* SettingsView.swift */; };
+ FBDDB53028635DA7006B4023 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB52E28635DA7006B4023 /* SettingsView.swift */; };
+ FBDDB53528635E05006B4023 /* CreatorContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB53428635E05006B4023 /* CreatorContentView.swift */; };
+ FBDDB53628635E05006B4023 /* CreatorContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB53428635E05006B4023 /* CreatorContentView.swift */; };
+ FBDDB53728635E36006B4023 /* CreatorContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378F27CB06BE0010E3D2 /* CreatorContentViewModel.swift */; };
+ FBDDB53928635E38006B4023 /* CreatorContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378F27CB06BE0010E3D2 /* CreatorContentViewModel.swift */; };
+ FBDDB53A28635E3B006B4023 /* LivestreamViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378C27CB06BE0010E3D2 /* LivestreamViewModel.swift */; };
+ FBDDB53B28635E3B006B4023 /* LivestreamViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378C27CB06BE0010E3D2 /* LivestreamViewModel.swift */; };
+ FBDDB53D286361DD006B4023 /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = FBDDB53C286361DD006B4023 /* CachedAsyncImage */; };
+ FBDDB53F286361E1006B4023 /* CachedAsyncImage in Frameworks */ = {isa = PBXBuildFile; productRef = FBDDB53E286361E1006B4023 /* CachedAsyncImage */; };
+ FBDDB54128636D1F006B4023 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FBDDB54028636D1F006B4023 /* Launch Screen.storyboard */; };
+ FBDDB54228636D1F006B4023 /* Launch Screen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FBDDB54028636D1F006B4023 /* Launch Screen.storyboard */; };
+ FBDDB54B286376FB006B4023 /* BlogPostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB549286376FB006B4023 /* BlogPostView.swift */; };
+ FBDDB54C28637718006B4023 /* BlogPostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378827CB06BE0010E3D2 /* BlogPostViewModel.swift */; };
+ FBDDB54D28637718006B4023 /* BlogPostViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378827CB06BE0010E3D2 /* BlogPostViewModel.swift */; };
+ FBDDB54F286377B0006B4023 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB54E286377B0006B4023 /* VisualEffectView.swift */; };
+ FBDDB550286377B0006B4023 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB54E286377B0006B4023 /* VisualEffectView.swift */; };
+ FBDDB5532863780E006B4023 /* FPColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB5522863780E006B4023 /* FPColors.swift */; };
+ FBDDB5542863780E006B4023 /* FPColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB5522863780E006B4023 /* FPColors.swift */; };
+ FBDDB55528637814006B4023 /* FPColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB5522863780E006B4023 /* FPColors.swift */; };
+ FBDDB55628637825006B4023 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB54E286377B0006B4023 /* VisualEffectView.swift */; };
+ FBDDB557286378D0006B4023 /* Swift+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E71FDB3B27D3F89200A9C88B /* Swift+Extensions.swift */; };
+ FBDDB559286378D2006B4023 /* Swift+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E71FDB3B27D3F89200A9C88B /* Swift+Extensions.swift */; };
+ FBDDB55A286379EF006B4023 /* BlogPostSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753378527CB06BE0010E3D2 /* BlogPostSelectionView.swift */; };
+ FBDDB55C28637A03006B4023 /* BlogPostSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB55B28637A03006B4023 /* BlogPostSelectionView.swift */; };
+ FBDDB55D28637A03006B4023 /* BlogPostSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBDDB55B28637A03006B4023 /* BlogPostSelectionView.swift */; };
+ FBDDB55E2863807B006B4023 /* SwiftUI+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A447E82815E1C0009BCE2E /* SwiftUI+Extensions.swift */; };
+ FBDDB5602863807C006B4023 /* SwiftUI+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7A447E82815E1C0009BCE2E /* SwiftUI+Extensions.swift */; };
+ FBF62588286B71C700283778 /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E753379527CB06BE0010E3D2 /* VideoView.swift */; };
+ FBF6258A286B71DB00283778 /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBF62589286B71DB00283778 /* VideoView.swift */; };
+ FBF6258B286B71DB00283778 /* VideoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBF62589286B71DB00283778 /* VideoView.swift */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -122,6 +205,26 @@
E7A447E82815E1C0009BCE2E /* SwiftUI+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SwiftUI+Extensions.swift"; sourceTree = ""; };
E7CCF2D1285314B600506E51 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; };
E7CCF2D2285315B400506E51 /* Wasserflug-tvOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Wasserflug-tvOS.entitlements"; sourceTree = ""; };
+ FB09BD3B2868C5EC00B52FED /* VideoPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoPlayerView.swift; sourceTree = ""; };
+ FB457F8028649753008A82AF /* CreatorSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorSearchView.swift; sourceTree = ""; };
+ FBDDB4D6286339B0006B4023 /* WasserflugApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WasserflugApp.swift; sourceTree = ""; };
+ FBDDB4D7286339B0006B4023 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; };
+ FBDDB4D8286339B2006B4023 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ FBDDB4DD286339B2006B4023 /* Wasserflug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Wasserflug.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ FBDDB4E2286339B2006B4023 /* Wasserflug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Wasserflug.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ FBDDB4E4286339B2006B4023 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; };
+ FBDDB4F628633A09006B4023 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; };
+ FBDDB51128633F36006B4023 /* Wasserflug.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wasserflug.swift; sourceTree = ""; };
+ FBDDB51F2863454E006B4023 /* Wasserflug--iOS--Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = "Wasserflug--iOS--Info.plist"; sourceTree = ""; };
+ FBDDB52628635CEE006B4023 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; };
+ FBDDB52E28635DA7006B4023 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; };
+ FBDDB53428635E05006B4023 /* CreatorContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorContentView.swift; sourceTree = ""; };
+ FBDDB54028636D1F006B4023 /* Launch Screen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = "Launch Screen.storyboard"; sourceTree = ""; };
+ FBDDB549286376FB006B4023 /* BlogPostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogPostView.swift; sourceTree = ""; };
+ FBDDB54E286377B0006B4023 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = ""; };
+ FBDDB5522863780E006B4023 /* FPColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FPColors.swift; sourceTree = ""; };
+ FBDDB55B28637A03006B4023 /* BlogPostSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogPostSelectionView.swift; sourceTree = ""; };
+ FBF62589286B71DB00283778 /* VideoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoView.swift; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -149,16 +252,39 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ FBDDB4DA286339B2006B4023 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ FBDDB53D286361DD006B4023 /* CachedAsyncImage in Frameworks */,
+ FBDDB4F2286339CD006B4023 /* FloatplaneAPIClient in Frameworks */,
+ FBDDB50228633A93006B4023 /* Vapor in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ FBDDB4DF286339B2006B4023 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ FBDDB53F286361E1006B4023 /* CachedAsyncImage in Frameworks */,
+ FBDDB4F4286339D4006B4023 /* FloatplaneAPIClient in Frameworks */,
+ FBDDB50428633A99006B4023 /* Vapor in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
E753373027CAFAFE0010E3D2 = {
isa = PBXGroup;
children = (
+ FBDDB51F2863454E006B4023 /* Wasserflug--iOS--Info.plist */,
E753376E27CB043D0010E3D2 /* Packages */,
E753373B27CAFAFE0010E3D2 /* Wasserflug-tvOS */,
E753374C27CAFAFF0010E3D2 /* Wasserflug-tvOSTests */,
E753375627CAFAFF0010E3D2 /* Wasserflug-tvOSUITests */,
+ FBDDB4D5286339B0006B4023 /* Shared */,
+ FBDDB4E3286339B2006B4023 /* macOS */,
E753373A27CAFAFE0010E3D2 /* Products */,
E75337BA27CB07900010E3D2 /* Frameworks */,
);
@@ -173,6 +299,8 @@
E753373927CAFAFE0010E3D2 /* Wasserflug.app */,
E753374927CAFAFF0010E3D2 /* Wasserflug-tvOSTests.xctest */,
E753375327CAFAFF0010E3D2 /* Wasserflug-tvOSUITests.xctest */,
+ FBDDB4DD286339B2006B4023 /* Wasserflug.app */,
+ FBDDB4E2286339B2006B4023 /* Wasserflug.app */,
);
name = Products;
sourceTree = "";
@@ -298,6 +426,69 @@
name = Frameworks;
sourceTree = "";
};
+ FBDDB4D5286339B0006B4023 /* Shared */ = {
+ isa = PBXGroup;
+ children = (
+ FBDDB551286377FC006B4023 /* Utilities */,
+ FBDDB54328637583006B4023 /* SupplementalViews */,
+ FBDDB4F928633A19006B4023 /* ViewModels */,
+ FBDDB4F5286339E6006B4023 /* Views */,
+ FBDDB4D6286339B0006B4023 /* WasserflugApp.swift */,
+ FBDDB4D7286339B0006B4023 /* ContentView.swift */,
+ FBDDB4D8286339B2006B4023 /* Assets.xcassets */,
+ FBDDB51128633F36006B4023 /* Wasserflug.swift */,
+ FBDDB54028636D1F006B4023 /* Launch Screen.storyboard */,
+ );
+ path = Shared;
+ sourceTree = "";
+ };
+ FBDDB4E3286339B2006B4023 /* macOS */ = {
+ isa = PBXGroup;
+ children = (
+ FBDDB4E4286339B2006B4023 /* macOS.entitlements */,
+ );
+ path = macOS;
+ sourceTree = "";
+ };
+ FBDDB4F5286339E6006B4023 /* Views */ = {
+ isa = PBXGroup;
+ children = (
+ FBDDB4F628633A09006B4023 /* LoginView.swift */,
+ FBDDB52628635CEE006B4023 /* HomeView.swift */,
+ FBDDB52E28635DA7006B4023 /* SettingsView.swift */,
+ FBDDB53428635E05006B4023 /* CreatorContentView.swift */,
+ FBDDB549286376FB006B4023 /* BlogPostView.swift */,
+ FB457F8028649753008A82AF /* CreatorSearchView.swift */,
+ FBF62589286B71DB00283778 /* VideoView.swift */,
+ );
+ path = Views;
+ sourceTree = "";
+ };
+ FBDDB4F928633A19006B4023 /* ViewModels */ = {
+ isa = PBXGroup;
+ children = (
+ );
+ path = ViewModels;
+ sourceTree = "";
+ };
+ FBDDB54328637583006B4023 /* SupplementalViews */ = {
+ isa = PBXGroup;
+ children = (
+ FBDDB54E286377B0006B4023 /* VisualEffectView.swift */,
+ FBDDB55B28637A03006B4023 /* BlogPostSelectionView.swift */,
+ FB09BD3B2868C5EC00B52FED /* VideoPlayerView.swift */,
+ );
+ path = SupplementalViews;
+ sourceTree = "";
+ };
+ FBDDB551286377FC006B4023 /* Utilities */ = {
+ isa = PBXGroup;
+ children = (
+ FBDDB5522863780E006B4023 /* FPColors.swift */,
+ );
+ path = Utilities;
+ sourceTree = "";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -359,6 +550,50 @@
productReference = E753375327CAFAFF0010E3D2 /* Wasserflug-tvOSUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
+ FBDDB4DC286339B2006B4023 /* Wasserflug (iOS) */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = FBDDB4EF286339B2006B4023 /* Build configuration list for PBXNativeTarget "Wasserflug (iOS)" */;
+ buildPhases = (
+ FBDDB4D9286339B2006B4023 /* Sources */,
+ FBDDB4DA286339B2006B4023 /* Frameworks */,
+ FBDDB4DB286339B2006B4023 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "Wasserflug (iOS)";
+ packageProductDependencies = (
+ FBDDB4F1286339CD006B4023 /* FloatplaneAPIClient */,
+ FBDDB50128633A93006B4023 /* Vapor */,
+ FBDDB53C286361DD006B4023 /* CachedAsyncImage */,
+ );
+ productName = "Wasserflug (iOS)";
+ productReference = FBDDB4DD286339B2006B4023 /* Wasserflug.app */;
+ productType = "com.apple.product-type.application";
+ };
+ FBDDB4E1286339B2006B4023 /* Wasserflug (macOS) */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = FBDDB4F0286339B2006B4023 /* Build configuration list for PBXNativeTarget "Wasserflug (macOS)" */;
+ buildPhases = (
+ FBDDB4DE286339B2006B4023 /* Sources */,
+ FBDDB4DF286339B2006B4023 /* Frameworks */,
+ FBDDB4E0286339B2006B4023 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = "Wasserflug (macOS)";
+ packageProductDependencies = (
+ FBDDB4F3286339D4006B4023 /* FloatplaneAPIClient */,
+ FBDDB50328633A99006B4023 /* Vapor */,
+ FBDDB53E286361E1006B4023 /* CachedAsyncImage */,
+ );
+ productName = "Wasserflug (macOS)";
+ productReference = FBDDB4E2286339B2006B4023 /* Wasserflug.app */;
+ productType = "com.apple.product-type.application";
+ };
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -366,7 +601,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
- LastSwiftUpdateCheck = 1320;
+ LastSwiftUpdateCheck = 1330;
LastUpgradeCheck = 1320;
TargetAttributes = {
E753373827CAFAFE0010E3D2 = {
@@ -380,6 +615,12 @@
CreatedOnToolsVersion = 13.2.1;
TestTargetID = E753373827CAFAFE0010E3D2;
};
+ FBDDB4DC286339B2006B4023 = {
+ CreatedOnToolsVersion = 13.3.1;
+ };
+ FBDDB4E1286339B2006B4023 = {
+ CreatedOnToolsVersion = 13.3.1;
+ };
};
};
buildConfigurationList = E753373427CAFAFE0010E3D2 /* Build configuration list for PBXProject "Wasserflug-tvOS" */;
@@ -402,6 +643,8 @@
E753373827CAFAFE0010E3D2 /* Wasserflug-tvOS */,
E753374827CAFAFF0010E3D2 /* Wasserflug-tvOSTests */,
E753375227CAFAFF0010E3D2 /* Wasserflug-tvOSUITests */,
+ FBDDB4DC286339B2006B4023 /* Wasserflug (iOS) */,
+ FBDDB4E1286339B2006B4023 /* Wasserflug (macOS) */,
);
};
/* End PBXProject section */
@@ -430,6 +673,26 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ FBDDB4DB286339B2006B4023 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ FBDDB54128636D1F006B4023 /* Launch Screen.storyboard in Resources */,
+ FBDDB52028634577006B4023 /* Assets.xcassets in Resources */,
+ FBDDB4E9286339B2006B4023 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ FBDDB4E0286339B2006B4023 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ FBDDB54228636D1F006B4023 /* Launch Screen.storyboard in Resources */,
+ FBDDB52128634577006B4023 /* Assets.xcassets in Resources */,
+ FBDDB4EA286339B2006B4023 /* Assets.xcassets in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -442,6 +705,7 @@
E75337B827CB06BE0010E3D2 /* RootTabView.swift in Sources */,
E75337A127CB06BE0010E3D2 /* PlayMediaView.swift in Sources */,
E70448E22803A9110045A1AE /* WaveformView.swift in Sources */,
+ FBDDB51228633F36006B4023 /* Wasserflug.swift in Sources */,
E75337B227CB06BE0010E3D2 /* PictureView.swift in Sources */,
E75337B327CB06BE0010E3D2 /* BlogPostView.swift in Sources */,
E75337B127CB06BE0010E3D2 /* HomeView.swift in Sources */,
@@ -449,27 +713,29 @@
E7A447E92815E1C0009BCE2E /* SwiftUI+Extensions.swift in Sources */,
E75337AB27CB06BE0010E3D2 /* CreatorContentViewModel.swift in Sources */,
E75337AF27CB06BE0010E3D2 /* LoginView.swift in Sources */,
+ FBDDB55628637825006B4023 /* VisualEffectView.swift in Sources */,
E753377B27CB066E0010E3D2 /* FPAPIService.swift in Sources */,
E753377827CB066E0010E3D2 /* FloatplaneAPIClient+Extensions.swift in Sources */,
E75337A627CB06BE0010E3D2 /* VideoViewModel.swift in Sources */,
E75337BE27CB08990010E3D2 /* EnvironmentKeys.swift in Sources */,
E75337B727CB06BE0010E3D2 /* ContentView.swift in Sources */,
+ FBDDB55A286379EF006B4023 /* BlogPostSelectionView.swift in Sources */,
E753377727CB066E0010E3D2 /* Authentication.swift in Sources */,
E753377F27CB06800010E3D2 /* Wasserflug_tvOSApp.swift in Sources */,
E75337A927CB06BE0010E3D2 /* AuthViewModel.swift in Sources */,
E75337AA27CB06BE0010E3D2 /* ViewModelState.swift in Sources */,
- E75337B027CB06BE0010E3D2 /* VideoView.swift in Sources */,
E753379E27CB06BE0010E3D2 /* BlogPostContentView.swift in Sources */,
E75337A027CB06BE0010E3D2 /* PlayButton.swift in Sources */,
+ FBDDB55528637814006B4023 /* FPColors.swift in Sources */,
E75337A527CB06BE0010E3D2 /* HomeViewModel.swift in Sources */,
E75337AD27CB06BE0010E3D2 /* CreatorSearchView.swift in Sources */,
+ FBF62588286B71C700283778 /* VideoView.swift in Sources */,
E71FDB7927E66D7E00A9C88B /* Persistence.swift in Sources */,
E753377D27CB066E0010E3D2 /* OSLoggingLogger.swift in Sources */,
E75337A727CB06BE0010E3D2 /* BaseViewModel.swift in Sources */,
E75337B527CB06BE0010E3D2 /* LivestreamPlayerView.swift in Sources */,
E75337A827CB06BE0010E3D2 /* LivestreamViewModel.swift in Sources */,
E753379F27CB06BE0010E3D2 /* VideoPlayerView.swift in Sources */,
- E75337A227CB06BE0010E3D2 /* BlogPostSelectionView.swift in Sources */,
E75337B427CB06BE0010E3D2 /* CreatorContentView.swift in Sources */,
E75337A327CB06BE0010E3D2 /* ErrorView.swift in Sources */,
E75337AE27CB06BE0010E3D2 /* SettingsView.swift in Sources */,
@@ -495,6 +761,88 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
+ FBDDB4D9286339B2006B4023 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ FBDDB52928635D04006B4023 /* HomeViewModel.swift in Sources */,
+ FBDDB557286378D0006B4023 /* Swift+Extensions.swift in Sources */,
+ FBDDB53528635E05006B4023 /* CreatorContentView.swift in Sources */,
+ FB18ED6F286880F100F53433 /* BlogPostView.swift in Sources */,
+ FBDDB55E2863807B006B4023 /* SwiftUI+Extensions.swift in Sources */,
+ FB457F8128649753008A82AF /* CreatorSearchView.swift in Sources */,
+ FBDDB4FF28633A89006B4023 /* FPAPIService.swift in Sources */,
+ FBDDB5532863780E006B4023 /* FPColors.swift in Sources */,
+ FBDDB52428635CD0006B4023 /* RootTabView.swift in Sources */,
+ FBDDB52228635446006B4023 /* EnvironmentKeys.swift in Sources */,
+ FBDDB51C2863423F006B4023 /* FloatplaneAPIClient+Extensions.swift in Sources */,
+ FBDDB53A28635E3B006B4023 /* LivestreamViewModel.swift in Sources */,
+ FBDDB4FD28633A57006B4023 /* AuthViewModel.swift in Sources */,
+ FBDDB4F728633A09006B4023 /* LoginView.swift in Sources */,
+ FBDDB54C28637718006B4023 /* BlogPostViewModel.swift in Sources */,
+ FBDDB55C28637A03006B4023 /* BlogPostSelectionView.swift in Sources */,
+ FBDDB4E7286339B2006B4023 /* ContentView.swift in Sources */,
+ FBDDB52B28635D3C006B4023 /* ViewModelState.swift in Sources */,
+ FBDDB50728633C45006B4023 /* ErrorView.swift in Sources */,
+ FBDDB50E28633E7C006B4023 /* OSLoggingLogger.swift in Sources */,
+ FBDDB51328633F36006B4023 /* Wasserflug.swift in Sources */,
+ FB09BD342868B74A00B52FED /* VideoViewModel.swift in Sources */,
+ FBF6258A286B71DB00283778 /* VideoView.swift in Sources */,
+ FBDDB4E5286339B2006B4023 /* WasserflugApp.swift in Sources */,
+ FBDDB50528633C04006B4023 /* BaseViewModel.swift in Sources */,
+ FBDDB54F286377B0006B4023 /* VisualEffectView.swift in Sources */,
+ FBDDB51528633FCB006B4023 /* Persistence.swift in Sources */,
+ FBDDB53728635E36006B4023 /* CreatorContentViewModel.swift in Sources */,
+ FBDDB52728635CEE006B4023 /* HomeView.swift in Sources */,
+ FBDDB51728633FEB006B4023 /* Wasserflug.xcdatamodeld in Sources */,
+ FBDDB5192863400A006B4023 /* Authentication.swift in Sources */,
+ FB457F832864AB0D008A82AF /* LivestreamPlayerView.swift in Sources */,
+ FBDDB52F28635DA7006B4023 /* SettingsView.swift in Sources */,
+ FB09BD3C2868C5EC00B52FED /* VideoPlayerView.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ FBDDB4DE286339B2006B4023 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ FBDDB52A28635D05006B4023 /* HomeViewModel.swift in Sources */,
+ FBDDB559286378D2006B4023 /* Swift+Extensions.swift in Sources */,
+ FBDDB54B286376FB006B4023 /* BlogPostView.swift in Sources */,
+ FBDDB53628635E05006B4023 /* CreatorContentView.swift in Sources */,
+ FBDDB5602863807C006B4023 /* SwiftUI+Extensions.swift in Sources */,
+ FB457F8228649753008A82AF /* CreatorSearchView.swift in Sources */,
+ FBDDB50028633A89006B4023 /* FPAPIService.swift in Sources */,
+ FBDDB5542863780E006B4023 /* FPColors.swift in Sources */,
+ FBDDB52528635CD1006B4023 /* RootTabView.swift in Sources */,
+ FBDDB52328635447006B4023 /* EnvironmentKeys.swift in Sources */,
+ FBDDB51E28634240006B4023 /* FloatplaneAPIClient+Extensions.swift in Sources */,
+ FBDDB53B28635E3B006B4023 /* LivestreamViewModel.swift in Sources */,
+ FBDDB4FE28633A57006B4023 /* AuthViewModel.swift in Sources */,
+ FBDDB4F828633A09006B4023 /* LoginView.swift in Sources */,
+ FBDDB54D28637718006B4023 /* BlogPostViewModel.swift in Sources */,
+ FBDDB55D28637A03006B4023 /* BlogPostSelectionView.swift in Sources */,
+ FBDDB4E8286339B2006B4023 /* ContentView.swift in Sources */,
+ FBDDB52D28635D3F006B4023 /* ViewModelState.swift in Sources */,
+ FBDDB50928633C47006B4023 /* ErrorView.swift in Sources */,
+ FBDDB51028633E7E006B4023 /* OSLoggingLogger.swift in Sources */,
+ FBDDB51428633F36006B4023 /* Wasserflug.swift in Sources */,
+ FB09BD352868B74A00B52FED /* VideoViewModel.swift in Sources */,
+ FBF6258B286B71DB00283778 /* VideoView.swift in Sources */,
+ FBDDB4E6286339B2006B4023 /* WasserflugApp.swift in Sources */,
+ FBDDB50628633C05006B4023 /* BaseViewModel.swift in Sources */,
+ FBDDB550286377B0006B4023 /* VisualEffectView.swift in Sources */,
+ FBDDB51628633FCB006B4023 /* Persistence.swift in Sources */,
+ FBDDB53928635E38006B4023 /* CreatorContentViewModel.swift in Sources */,
+ FBDDB52828635CEE006B4023 /* HomeView.swift in Sources */,
+ FBDDB51828633FEC006B4023 /* Wasserflug.xcdatamodeld in Sources */,
+ FBDDB51B2863400C006B4023 /* Authentication.swift in Sources */,
+ FB457F842864AB0D008A82AF /* LivestreamPlayerView.swift in Sources */,
+ FBDDB53028635DA7006B4023 /* SettingsView.swift in Sources */,
+ FB09BD3D2868C5EC00B52FED /* VideoPlayerView.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
@@ -765,6 +1113,125 @@
};
name = Release;
};
+ FBDDB4EB286339B2006B4023 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "Wasserflug-tvOS/Assets.xcassets Wasserflug-tvOS/Preview\\ Content/Preview\\ Assets.xcassets";
+ DEVELOPMENT_TEAM = P88CDRP8CR;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "Wasserflug--iOS--Info.plist";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 15.4;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.jamesnl.Wasserflug;
+ PRODUCT_NAME = Wasserflug;
+ SDKROOT = iphoneos;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ FBDDB4EC286339B2006B4023 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "Wasserflug-tvOS/Assets.xcassets Wasserflug-tvOS/Preview\\ Content/Preview\\ Assets.xcassets";
+ DEVELOPMENT_TEAM = P88CDRP8CR;
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_FILE = "Wasserflug--iOS--Info.plist";
+ INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
+ INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
+ INFOPLIST_KEY_UILaunchStoryboardName = "Launch Screen";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
+ IPHONEOS_DEPLOYMENT_TARGET = 15.4;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.jamesnl.Wasserflug;
+ PRODUCT_NAME = Wasserflug;
+ SDKROOT = iphoneos;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ FBDDB4ED286339B2006B4023 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "Wasserflug-tvOS/Assets.xcassets Wasserflug-tvOS/Preview\\ Content/Preview\\ Assets.xcassets";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 12.3;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.jamesnl.Wasserflug;
+ PRODUCT_NAME = Wasserflug;
+ SDKROOT = macosx;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Debug;
+ };
+ FBDDB4EE286339B2006B4023 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
+ CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements;
+ CODE_SIGN_STYLE = Automatic;
+ COMBINE_HIDPI_IMAGES = YES;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_ASSET_PATHS = "Wasserflug-tvOS/Assets.xcassets Wasserflug-tvOS/Preview\\ Content/Preview\\ Assets.xcassets";
+ ENABLE_PREVIEWS = YES;
+ GENERATE_INFOPLIST_FILE = YES;
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/../Frameworks",
+ );
+ MACOSX_DEPLOYMENT_TARGET = 12.3;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.jamesnl.Wasserflug;
+ PRODUCT_NAME = Wasserflug;
+ SDKROOT = macosx;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ SWIFT_VERSION = 5.0;
+ };
+ name = Release;
+ };
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -804,6 +1271,24 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
+ FBDDB4EF286339B2006B4023 /* Build configuration list for PBXNativeTarget "Wasserflug (iOS)" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ FBDDB4EB286339B2006B4023 /* Debug */,
+ FBDDB4EC286339B2006B4023 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ FBDDB4F0286339B2006B4023 /* Build configuration list for PBXNativeTarget "Wasserflug (macOS)" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ FBDDB4ED286339B2006B4023 /* Debug */,
+ FBDDB4EE286339B2006B4023 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
@@ -820,7 +1305,7 @@
repositoryURL = "https://github.com/vapor/vapor";
requirement = {
kind = exactVersion;
- version = 4.55.0;
+ version = 4.62.0;
};
};
/* End XCRemoteSwiftPackageReference section */
@@ -840,6 +1325,34 @@
isa = XCSwiftPackageProductDependency;
productName = FloatplaneAPIClient;
};
+ FBDDB4F1286339CD006B4023 /* FloatplaneAPIClient */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = FloatplaneAPIClient;
+ };
+ FBDDB4F3286339D4006B4023 /* FloatplaneAPIClient */ = {
+ isa = XCSwiftPackageProductDependency;
+ productName = FloatplaneAPIClient;
+ };
+ FBDDB50128633A93006B4023 /* Vapor */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = E753376927CB01FD0010E3D2 /* XCRemoteSwiftPackageReference "vapor" */;
+ productName = Vapor;
+ };
+ FBDDB50328633A99006B4023 /* Vapor */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = E753376927CB01FD0010E3D2 /* XCRemoteSwiftPackageReference "vapor" */;
+ productName = Vapor;
+ };
+ FBDDB53C286361DD006B4023 /* CachedAsyncImage */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = E753376627CAFF550010E3D2 /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */;
+ productName = CachedAsyncImage;
+ };
+ FBDDB53E286361E1006B4023 /* CachedAsyncImage */ = {
+ isa = XCSwiftPackageProductDependency;
+ package = E753376627CAFF550010E3D2 /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */;
+ productName = CachedAsyncImage;
+ };
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
diff --git a/Wasserflug-tvOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Wasserflug-tvOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
index 3c29deb..2429a66 100644
--- a/Wasserflug-tvOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
+++ b/Wasserflug-tvOS.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
@@ -15,8 +15,8 @@
"repositoryURL": "https://github.com/swift-server/async-http-client.git",
"state": {
"branch": null,
- "revision": "7a4dfe026f6ee0f8ad741b58df74c60af296365d",
- "version": "1.9.0"
+ "revision": "794dc9d42720af97cedd395e8cd2add9173ffd9a",
+ "version": "1.11.1"
}
},
{
@@ -51,8 +51,17 @@
"repositoryURL": "https://github.com/vapor/routing-kit.git",
"state": {
"branch": null,
- "revision": "5603b81ceb744b8318feab1e60943704977a866b",
- "version": "4.3.1"
+ "revision": "9e181d685a3dec1eef1fc6dacf606af364f86d68",
+ "version": "4.5.0"
+ }
+ },
+ {
+ "package": "swift-algorithms",
+ "repositoryURL": "https://github.com/apple/swift-algorithms.git",
+ "state": {
+ "branch": null,
+ "revision": "b14b7f4c528c942f121c8b860b9410b2bf57825e",
+ "version": "1.0.0"
}
},
{
@@ -114,8 +123,8 @@
"repositoryURL": "https://github.com/apple/swift-nio-http2.git",
"state": {
"branch": null,
- "revision": "000ca94f9de92c95b9ac85d44600b7b0fe25a3e5",
- "version": "1.19.2"
+ "revision": "108ac15087ea9b79abb6f6742699cf31de0e8772",
+ "version": "1.22.0"
}
},
{
@@ -136,6 +145,15 @@
"version": "1.11.4"
}
},
+ {
+ "package": "swift-numerics",
+ "repositoryURL": "https://github.com/apple/swift-numerics",
+ "state": {
+ "branch": null,
+ "revision": "0a5bc04095a675662cf24757cc0640aa2204253b",
+ "version": "1.0.2"
+ }
+ },
{
"package": "swiftui-cached-async-image",
"repositoryURL": "https://github.com/lorenzofiamingo/swiftui-cached-async-image",
@@ -150,8 +168,8 @@
"repositoryURL": "https://github.com/vapor/vapor",
"state": {
"branch": null,
- "revision": "18e9419cae5049e43ca1e8002ca3cf0449f2c8ed",
- "version": "4.55.0"
+ "revision": "12e2e7460ab912b65fb7a0fe47e4f638a7d5e642",
+ "version": "4.62.0"
}
},
{
diff --git a/Wasserflug-tvOS.xcodeproj/xcshareddata/xcschemes/Wasserflug-tvOS.xcscheme b/Wasserflug-tvOS.xcodeproj/xcshareddata/xcschemes/Wasserflug-tvOS.xcscheme
index e300488..40e3262 100644
--- a/Wasserflug-tvOS.xcodeproj/xcshareddata/xcschemes/Wasserflug-tvOS.xcscheme
+++ b/Wasserflug-tvOS.xcodeproj/xcshareddata/xcschemes/Wasserflug-tvOS.xcscheme
@@ -28,6 +28,16 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
+
+
+
+
some View {
if let namespace = namespace {
@@ -10,3 +11,37 @@ extension View {
}
}
}
+#endif
+
+extension View {
+ func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
+ clipShape( RoundedCorner(radius: radius, corners: corners) )
+ }
+}
+
+struct RoundedCorner: Shape {
+
+ var radius: CGFloat = .infinity
+ var corners: UIRectCorner = .allCorners
+
+ func path(in rect: CGRect) -> Path {
+ let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
+ return Path(path.cgPath)
+ }
+}
+
+// https://www.avanderlee.com/swiftui/conditional-view-modifier/
+extension View {
+ /// Applies the given transform if the given condition evaluates to `true`.
+ /// - Parameters:
+ /// - condition: The condition to evaluate.
+ /// - transform: The transform to apply to the source `View`.
+ /// - Returns: Either the original `View` or the modified `View` if the condition is `true`.
+ @ViewBuilder func `if`(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View {
+ if condition() {
+ transform(self)
+ } else {
+ self
+ }
+ }
+}
diff --git a/Wasserflug-tvOS/ViewModels/BaseViewModel.swift b/Wasserflug-tvOS/ViewModels/BaseViewModel.swift
index a18f12c..11b46a3 100644
--- a/Wasserflug-tvOS/ViewModels/BaseViewModel.swift
+++ b/Wasserflug-tvOS/ViewModels/BaseViewModel.swift
@@ -4,7 +4,7 @@ import Vapor
class BaseViewModel {
lazy var logger: Logger = {
- var logger = Wasserflug_tvOSApp.logger
+ var logger = Wasserflug.logger
logger[metadataKey: "class"] = "\(Self.Type.self)"
return logger
}()
diff --git a/Wasserflug-tvOS/ViewModels/CreatorContentViewModel.swift b/Wasserflug-tvOS/ViewModels/CreatorContentViewModel.swift
index 3d0cc39..119bc92 100644
--- a/Wasserflug-tvOS/ViewModels/CreatorContentViewModel.swift
+++ b/Wasserflug-tvOS/ViewModels/CreatorContentViewModel.swift
@@ -29,7 +29,31 @@ class CreatorContentViewModel: BaseViewModel, ObservableObject {
return nil
}
}
-
+
+ var coverImageWidth: CGFloat? {
+ if let cover = creator.cover {
+ return CGFloat(cover.width)
+ } else {
+ return nil
+ }
+ }
+
+ var coverImageHeight: CGFloat? {
+ if let cover = creator.cover {
+ return CGFloat(cover.height)
+ } else {
+ return nil
+ }
+ }
+
+ var coverRatio: CGFloat? {
+ if let cover = creator.cover {
+ return CGFloat(cover.aspectRatio)
+ } else {
+ return nil
+ }
+ }
+
var creatorProfileImagePath: URL? {
URL(string: creatorOwner.profileImage.path)
}
diff --git a/Wasserflug-tvOS/ViewModels/LivestreamViewModel.swift b/Wasserflug-tvOS/ViewModels/LivestreamViewModel.swift
index 56953fc..d93634a 100644
--- a/Wasserflug-tvOS/ViewModels/LivestreamViewModel.swift
+++ b/Wasserflug-tvOS/ViewModels/LivestreamViewModel.swift
@@ -7,12 +7,22 @@ import Vapor
class LivestreamViewModel: BaseViewModel, ObservableObject {
@Published var state: ViewModelState = .idle
- @Published var isLive: Bool = false
+ @Published var fetchedIsLive: Bool = false
@Published var isLoadingLiveStatus: Bool = false
+
+ var isLive: Bool {
+ if mockIsLive {
+ return true
+ } else {
+ return fetchedIsLive
+ }
+ }
private let fpApiService: FPAPIService
let creator: CreatorModelV2
+ private var mockIsLive: Bool = false
+
var avMetadataItems: [AVMetadataItem] {
return [
metadataItem(identifier: .commonIdentifierTitle, value: creator.liveStream?.title ?? "Livestream"),
@@ -29,6 +39,13 @@ class LivestreamViewModel: BaseViewModel, ObservableObject {
self.creator = creator
}
+ /// For preview mocking
+ init(fpApiService: FPAPIService, creator: CreatorModelV2, mockIsLive: Bool) {
+ self.fpApiService = fpApiService
+ self.creator = creator
+ self.mockIsLive = mockIsLive
+ }
+
func load() {
state = .loading
@@ -96,19 +113,19 @@ class LivestreamViewModel: BaseViewModel, ObservableObject {
switch result {
case let .success(clientResponse):
if clientResponse.status == .ok {
- self.isLive = true
+ self.fetchedIsLive = true
self.logger.debug("Livestream is live", metadata: [
"id": "\(self.creator.id)",
])
} else {
- self.isLive = false
+ self.fetchedIsLive = false
self.logger.debug("Livestream is not live", metadata: [
"id": "\(self.creator.id)",
])
}
case let .failure(error):
self.logger.warning("Encountered unexpected error when loading livestream status. Error: \(String(reflecting: error))")
- self.isLive = false
+ self.fetchedIsLive = false
}
}
})
diff --git a/Wasserflug-tvOS/ViewModels/VideoViewModel.swift b/Wasserflug-tvOS/ViewModels/VideoViewModel.swift
index 240be7a..03f44f7 100644
--- a/Wasserflug-tvOS/ViewModels/VideoViewModel.swift
+++ b/Wasserflug-tvOS/ViewModels/VideoViewModel.swift
@@ -83,7 +83,10 @@ class VideoViewModel: BaseViewModel, ObservableObject {
])
let baseCdn = response.cdn
let pathTemplate = response.resource.uri!
- let screen = UIScreen.main
+ var screenWidth = UIScreen.main.bounds.width
+ if screenWidth < UIScreen.main.bounds.height {
+ screenWidth = UIScreen.main.bounds.height
+ }
let levels = response.resource.data.qualityLevels?
.filter({ qualityLevel in
// Filter out resolutions larger than the device's screen resolution.
@@ -93,9 +96,9 @@ class VideoViewModel: BaseViewModel, ObservableObject {
// will allow for actually getting 1080p on 1080p screens, with only a little bit of downsampling.
// But, it wouldn't make sense to show 4k on a 1080p screen. Waste of bandwidth, and the
// hardware may not like the massive downsampling.
- let result = CGFloat(qualityLevel.width) <= (screen.bounds.width * 1.15)
+ let result = CGFloat(qualityLevel.width) <= (screenWidth * 1.15)
if !result {
- self.logger.warning("Ignoring quality level \(String(describing: qualityLevel)) due to larger-than-screen width of \(screen.bounds.width)")
+ self.logger.warning("Ignoring quality level \(String(describing: qualityLevel)) due to larger-than-screen width of \(screenWidth)")
}
return result
})
diff --git a/Wasserflug-tvOS/Views/ContentView.swift b/Wasserflug-tvOS/Views/ContentView.swift
index 0e5572e..b2a59a2 100644
--- a/Wasserflug-tvOS/Views/ContentView.swift
+++ b/Wasserflug-tvOS/Views/ContentView.swift
@@ -3,11 +3,6 @@ import FloatplaneAPIClient
import NIO
import Network
-enum FPColors {
- static let blue = Color(.sRGB, red: 0, green: 175.0/256.0, blue: 236.0/256.0, opacity: 1.0)
- static let darkBlue = Color(.sRGB, red: 0, green: 175.0/256.0, blue: 236.0/256.0, opacity: 1.0)
-}
-
struct ContentView: View {
@ObservedObject var viewModel: AuthViewModel
@@ -128,9 +123,3 @@ struct ContentView_Previews: PreviewProvider {
ContentView(viewModel: AuthViewModel(fpApiService: MockFPAPIService()))
}
}
-
-struct VisualEffectView: UIViewRepresentable {
- var effect: UIVisualEffect?
- func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { UIVisualEffectView() }
- func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { uiView.effect = effect }
-}
diff --git a/Wasserflug-tvOS/Views/LoginView.swift b/Wasserflug-tvOS/Views/LoginView.swift
index 5669fef..5092055 100644
--- a/Wasserflug-tvOS/Views/LoginView.swift
+++ b/Wasserflug-tvOS/Views/LoginView.swift
@@ -28,8 +28,7 @@ struct LoginView: View {
VStack {
Text("Login to Floatplane")
.bold()
- Spacer()
-
+ Spacer()
TextField("Username", text: $username)
.textContentType(.username)
.focused($focusedField, equals: .usernameField)
diff --git a/Wasserflug-tvOS/Views/RootTabView.swift b/Wasserflug-tvOS/Views/RootTabView.swift
index 3095757..4c46142 100644
--- a/Wasserflug-tvOS/Views/RootTabView.swift
+++ b/Wasserflug-tvOS/Views/RootTabView.swift
@@ -18,7 +18,11 @@ struct RootTabView: View {
HomeView(viewModel: HomeViewModel(userInfo: userInfo, fpApiService: fpApiService))
.tag(Selection.home)
.tabItem {
+ #if os(tvOS)
Text("Home")
+ #else
+ Label("Home", systemImage: "house")
+ #endif
}
ForEach(userInfo.userSubscriptions) { sub in
let creator = userInfo.creators[sub.creator]!
@@ -26,13 +30,21 @@ struct RootTabView: View {
CreatorContentView(viewModel: CreatorContentViewModel(fpApiService: fpApiService, creator: creator, creatorOwner: creatorOwner), livestreamViewModel: LivestreamViewModel(fpApiService: fpApiService, creator: creator))
.tag(Selection.creator(creator.id))
.tabItem {
- Text(creator.title)
+ #if os(tvOS)
+ Text(creator.title)
+ #else
+ Label(creator.title, systemImage: "person.crop.rectangle")
+ #endif
}
}
SettingsView()
.tag(Selection.settings)
.tabItem {
- Text("Settings")
+ #if os(tvOS)
+ Text("Settings")
+ #else
+ Label("Settings", systemImage: "gear")
+ #endif
}
}
}
diff --git a/Wasserflug-tvOS/Views/SettingsView.swift b/Wasserflug-tvOS/Views/SettingsView.swift
index fe1a163..3eeca19 100644
--- a/Wasserflug-tvOS/Views/SettingsView.swift
+++ b/Wasserflug-tvOS/Views/SettingsView.swift
@@ -14,7 +14,7 @@ struct SettingsView: View {
@State var showResetViewHistoryFailure = false
let logger: Logger = {
- var logger = Wasserflug_tvOSApp.logger
+ var logger = Wasserflug.logger
logger[metadataKey: "class"] = "\(Self.Type.self)"
return logger
}()
diff --git a/Wasserflug-tvOS/Wasserflug_tvOSApp.swift b/Wasserflug-tvOS/Wasserflug_tvOSApp.swift
index 39f33aa..0181254 100644
--- a/Wasserflug-tvOS/Wasserflug_tvOSApp.swift
+++ b/Wasserflug-tvOS/Wasserflug_tvOSApp.swift
@@ -6,81 +6,13 @@ import Logging
@main
struct Wasserflug_tvOSApp: App {
- static var logger: Logging.Logger {
- var logger = Logging.Logger(label: Bundle.main.bundleIdentifier!)
- #if DEBUG
- // For debugging, log at a lower level to get more information.
- logger.logLevel = .debug
- #else
- // For release mode, only log important items.
- logger.logLevel = .notice
- #endif
- return logger
- }
- static var networkLogger: Logging.Logger {
- var logger = Logging.Logger(label: Bundle.main.bundleIdentifier!)
- logger.logLevel = .info
- logger[metadataKey: "category"] = "network"
- return logger
- }
-
- let vaporApp = Vapor.Application(.production, .createNew)
- let fpApiService: FPAPIService = DefaultFPAPIService()
- let authViewModel: AuthViewModel
- let persistenceController = PersistenceController.shared
-
- init() {
- // Set custom user agent for network requests. This is particularly
- // required in order to pass the login phase with bypassing the captcha.
- Wasserflug_tvOSApp.setHttp(header: "User-Agent", value: "Wasserflug tvOS App, CFNetwork")
-
- // Attempt to use any previous authentication cookies, so the user does
- // not need to login on every app start.
- FloatplaneAPIClientAPI.loadAuthenticationCookiesFromStorage()
-
- // Use FP's date format for JSON encoding/decoding.
- let fpDateFormatter = DateFormatter()
- fpDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
- Configuration.contentConfiguration.use(encoder: JSONEncoder.custom(dates: .formatted(fpDateFormatter)), for: .json)
- Configuration.contentConfiguration.use(decoder: JSONDecoder.custom(dates: .formatted(fpDateFormatter)), for: .json)
-
- // Bootstrap core logging.
- LoggingSystem.bootstrap({ (label) -> LogHandler in
- var loggingLogger = OSLoggingLogger(label: label, category: "Wasserflug")
- loggingLogger.logLevel = .debug
- return MultiplexLogHandler([
- loggingLogger,
- ])
- })
-
- // Bootstrap API/Network logging.
- Configuration.apiClient = vaporApp.client
- .logging(to: Wasserflug_tvOSApp.networkLogger)
- Configuration.apiWrapper = { clientRequest in
- Wasserflug_tvOSApp.networkLogger.info("Sending \(clientRequest.method) request to \(clientRequest.url)")
- }
-
- // Create and store in @State the main view model.
- authViewModel = AuthViewModel(fpApiService: fpApiService)
- }
+ let wasserflug = Wasserflug();
var body: some Scene {
WindowGroup {
- ContentView(viewModel: authViewModel)
- .environment(\.fpApiService, fpApiService)
- .environment(\.managedObjectContext, persistenceController.container.viewContext)
- }
- }
-
- private static func setHttp(header: String, value: String) {
- FloatplaneAPIClientAPI.customHeaders.replaceOrAdd(name: header, value: value)
- if var headers = URLSession.shared.configuration.httpAdditionalHeaders {
- headers[header] = value
- URLSession.shared.configuration.httpAdditionalHeaders = headers
- } else {
- URLSession.shared.configuration.httpAdditionalHeaders = [
- header: value,
- ]
+ ContentView(viewModel: wasserflug.authViewModel)
+ .environment(\.fpApiService, wasserflug.fpApiService)
+ .environment(\.managedObjectContext, wasserflug.persistenceController.container.viewContext)
}
}
}
diff --git a/macOS/macOS.entitlements b/macOS/macOS.entitlements
new file mode 100644
index 0000000..f2ef3ae
--- /dev/null
+++ b/macOS/macOS.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.security.app-sandbox
+
+ com.apple.security.files.user-selected.read-only
+
+
+