From 28d2136a257cf2a729f2144dfc4bea26d64c869f Mon Sep 17 00:00:00 2001 From: Aiden Vigue Date: Fri, 25 Jun 2021 00:35:21 -0400 Subject: [PATCH 01/15] add watched state to item image views. --- JellyfinPlayer.xcodeproj/project.pbxproj | 16 ++-- .../xcshareddata/swiftpm/Package.resolved | 4 +- JellyfinPlayer/ConnectToServerView.swift | 11 +-- JellyfinPlayer/ContinueWatchingView.swift | 35 ++++----- JellyfinPlayer/HomeView.swift | 4 +- JellyfinPlayer/LatestMediaView.swift | 11 +++ JellyfinPlayer/LibraryFilterView.swift | 78 +++++++++++-------- JellyfinPlayer/LibraryListView.swift | 1 + JellyfinPlayer/LibrarySearchView.swift | 11 +++ JellyfinPlayer/LibraryView.swift | 22 ++++-- JellyfinPlayer/SeasonItemView.swift | 11 +++ JellyfinPlayer/SettingsView.swift | 4 + Shared/Extensions/ImageView.swift | 14 ++-- Shared/ServerLocator/ServerDiscovery.swift | 3 - .../UDPBroadCastConnection.swift | 10 +-- Shared/Typings/Typings.swift | 7 +- .../ViewModels/LibraryFilterViewModel.swift | 16 ++++ 17 files changed, 161 insertions(+), 97 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 088e4486..0b493e32 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -1066,7 +1066,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_PREVIEWS = YES; @@ -1094,7 +1094,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = "JellyfinPlayer tvOS/JellyfinPlayer tvOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_PREVIEWS = YES; @@ -1243,7 +1243,7 @@ CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_BITCODE = NO; @@ -1277,7 +1277,7 @@ CODE_SIGN_ENTITLEMENTS = JellyfinPlayer/JellyfinPlayer.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = 9R8RREG67J; @@ -1309,7 +1309,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 9R8RREG67J; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; @@ -1334,7 +1334,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 53; + CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_TEAM = 9R8RREG67J; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; @@ -1438,8 +1438,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/kean/NukeUI"; requirement = { - kind = exactVersion; - version = 0.3.0; + kind = upToNextMajorVersion; + minimumVersion = 0.3.0; }; }; 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */ = { diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved index 94c97f78..f56b4837 100644 --- a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -60,8 +60,8 @@ "repositoryURL": "https://github.com/kean/NukeUI", "state": { "branch": null, - "revision": "d2580b8d22b29c6244418d8e4b568f3162191460", - "version": "0.3.0" + "revision": "4516371912149ac024dec361827931b46a69c217", + "version": "0.6.2" } }, { diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index 00c42e6e..a5ad623d 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -105,7 +105,7 @@ struct ConnectToServerView: View { } } } else { - Section(header: Text("Server Information")) { + Section(header: Text("Manual Connection")) { TextField("Jellyfin Server URL", text: $uri) .disableAutocorrection(true) .autocapitalization(.none) @@ -123,23 +123,20 @@ struct ConnectToServerView: View { .disabled(viewModel.isLoading || uri.isEmpty) } - Section(header: Text("Local Servers")) { + Section(header: Text("Discovered Servers")) { if self.viewModel.searching { ProgressView() } ForEach(self.viewModel.servers, id: \.id) { server in Button(action: { - print(server.url) viewModel.connectToServer(at: server.url) }, label: { HStack { - VStack { Text(server.name) .font(.headline) - Text(server.host) + Text("• \(server.host)") .font(.subheadline) - - } + .foregroundColor(.secondary) Spacer() if viewModel.isLoading { ProgressView() diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index f9556d9a..d5bb2129 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -43,19 +43,6 @@ struct ContinueWatchingView: View { .frame(width: 320, height: 180) .cornerRadius(10) .shadow(radius: 4) - .overlay( - Group { - if item.type == "Episode" { - Text("\(item.name ?? "")") - .font(.caption) - .padding(6) - .foregroundColor(.white) - } - }.background(Color.black) - .opacity(0.8) - .cornerRadius(10.0) - .padding(6), alignment: .topTrailing - ) .overlay( Rectangle() .fill(Color(red: 172/255, green: 92/255, blue: 195/255)) @@ -63,12 +50,22 @@ struct ContinueWatchingView: View { .frame(width: CGFloat((item.userData?.playedPercentage ?? 0) * 3.2), height: 7) .padding(0), alignment: .bottomLeading ) - Text(item.seriesName ?? item.name ?? "") - .font(.callout) - .fontWeight(.semibold) - .foregroundColor(.primary) - .lineLimit(1) - .frame(width: 320, alignment: .leading) + HStack { + Text("\(item.seriesName ?? item.name ?? "")") + .font(.callout) + .fontWeight(.semibold) + .foregroundColor(.primary) + .lineLimit(1) + if(item.type == "Episode") { + Text("• S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0)) - \(item.name ?? "")") + .font(.callout) + .fontWeight(.semibold) + .foregroundColor(.secondary) + .lineLimit(1) + .offset(x: -1.4) + } + Spacer() + }.frame(width: 320, alignment: .leading) }.padding(.top, 10) .padding(.bottom, 5) } diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index afeec96e..e7f5b220 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -20,7 +20,7 @@ struct HomeView: View { ProgressView() } else { ScrollView { - LazyVStack(alignment: .leading) { + VStack(alignment: .leading) { if !viewModel.resumeItems.isEmpty { ContinueWatchingView(items: viewModel.resumeItems) } @@ -57,7 +57,6 @@ struct HomeView: View { var body: some View { innerBody .navigationTitle(MainTabView.Tab.home.localized) - /* .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { Button { @@ -70,6 +69,5 @@ struct HomeView: View { .fullScreenCover(isPresented: $showingSettings) { SettingsView(viewModel: SettingsViewModel(), close: $showingSettings) } - */ } } diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 6fa24521..8d9e8402 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -21,6 +21,17 @@ struct LatestMediaView: View { .frame(width: 100, height: 150) .cornerRadius(10) .shadow(radius: 4) + .overlay( + ZStack { + if(item.userData!.played ?? false) { + Image(systemName: "circle.fill") + .foregroundColor(.white) + Image(systemName: "checkmark.circle.fill") + .foregroundColor(Color(.systemBlue)) + } + }.padding(2) + .opacity(1) + , alignment: .topTrailing).opacity(1) Text(item.seriesName ?? item.name ?? "") .font(.caption) .fontWeight(.semibold) diff --git a/JellyfinPlayer/LibraryFilterView.swift b/JellyfinPlayer/LibraryFilterView.swift index 0719d7d2..89ecb7e2 100644 --- a/JellyfinPlayer/LibraryFilterView.swift +++ b/JellyfinPlayer/LibraryFilterView.swift @@ -21,45 +21,54 @@ struct LibraryFilterView: View { var body: some View { NavigationView { - ZStack { - Form { - if viewModel.enabledFilterType.contains(.genre) { - MultiSelector(label: "Genres", - options: viewModel.possibleGenres, - optionToString: { $0.name ?? "" }, - selected: $viewModel.modifiedFilters.withGenres) - } - if viewModel.enabledFilterType.contains(.filter) { - MultiSelector(label: "Filters", - options: viewModel.possibleItemFilters, - optionToString: { $0.localized }, - selected: $viewModel.modifiedFilters.filters) - } - if viewModel.enabledFilterType.contains(.tag) { - MultiSelector(label: "Tags", - options: viewModel.possibleTags, - optionToString: { $0 }, - selected: $viewModel.modifiedFilters.tags) - } - if viewModel.enabledFilterType.contains(.sortBy) { - MultiSelector(label: "Sort by", - options: viewModel.possibleSortBys, - optionToString: { $0.localized }, - selected: $viewModel.modifiedFilters.sortBy) - } - if viewModel.enabledFilterType.contains(.sortOrder) { - Picker(selection: $viewModel.modifiedFilters.sortOrder, label: Text("Order")) { - ForEach(viewModel.possibleSortOrders, id: \.self) { so in - Text("\(so.rawValue)").tag(so.rawValue) + VStack { + if viewModel.isLoading { + ProgressView() + } else { + Form { + if viewModel.enabledFilterType.contains(.genre) { + MultiSelector(label: "Genres", + options: viewModel.possibleGenres, + optionToString: { $0.name ?? "" }, + selected: $viewModel.modifiedFilters.withGenres) + } + if viewModel.enabledFilterType.contains(.filter) { + MultiSelector(label: "Filters", + options: viewModel.possibleItemFilters, + optionToString: { $0.localized }, + selected: $viewModel.modifiedFilters.filters) + } + if viewModel.enabledFilterType.contains(.tag) { + MultiSelector(label: "Tags", + options: viewModel.possibleTags, + optionToString: { $0 }, + selected: $viewModel.modifiedFilters.tags) + } + if viewModel.enabledFilterType.contains(.sortBy) { + Picker(selection: $viewModel.selectedSortBy, label: Text("Sort by")) { + ForEach(viewModel.possibleSortBys, id: \.self) { so in + Text(so.localized).tag(so) + } + } + } + if viewModel.enabledFilterType.contains(.sortOrder) { + Picker(selection: $viewModel.selectedSortOrder, label: Text("Display order")) { + ForEach(viewModel.possibleSortOrders, id: \.self) { so in + Text(so.rawValue).tag(so) + } } } } - } - if viewModel.isLoading { - ProgressView() + Button { + viewModel.resetFilters() + self.filters = viewModel.modifiedFilters + presentationMode.wrappedValue.dismiss() + } label: { + Text("Reset") + } } } - .navigationBarTitle("Filters", displayMode: .inline) + .navigationBarTitle("Filter Results", displayMode: .inline) .toolbar { ToolbarItemGroup(placement: .navigationBarLeading) { Button { @@ -70,6 +79,7 @@ struct LibraryFilterView: View { } ToolbarItemGroup(placement: .navigationBarTrailing) { Button { + viewModel.updateModifiedFilter() self.filters = viewModel.modifiedFilters presentationMode.wrappedValue.dismiss() } label: { diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index 9cf11ecd..5a1faef9 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -74,6 +74,7 @@ struct LibraryListView: View { }.padding(32) }.background(Color.black) .frame(minWidth: 100, maxWidth: .infinity) + .frame(height: 72) } .cornerRadius(10) .shadow(radius: 5) diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index bf086554..5a514538 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -35,6 +35,17 @@ struct LibrarySearchView: View { ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash()) .frame(width: 100, height: 150) .cornerRadius(10) + .overlay( + ZStack { + if(item.userData!.played ?? false) { + Image(systemName: "circle.fill") + .foregroundColor(.white) + Image(systemName: "checkmark.circle.fill") + .foregroundColor(Color(.systemBlue)) + } + }.padding(2) + .opacity(1) + , alignment: .topTrailing).opacity(1) Text(item.name ?? "") .font(.caption) .fontWeight(.semibold) diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index 0cc9004b..bd0b5bea 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -13,6 +13,7 @@ struct LibraryView: View { var title: String // MARK: tracks for grid + var defaultFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) @State var isShowingSearchView = false @State var isShowingFilterView = false @@ -38,6 +39,17 @@ struct LibraryView: View { ImageView(src: item.getPrimaryImage(maxWidth: 100), bh: item.getPrimaryImageBlurHash()) .frame(width: 100, height: 150) .cornerRadius(10) + .overlay( + ZStack { + if(item.userData!.played ?? false) { + Image(systemName: "circle.fill") + .foregroundColor(.white) + Image(systemName: "checkmark.circle.fill") + .foregroundColor(Color(.systemBlue)) + } + }.padding(2) + .opacity(1) + , alignment: .topTrailing).opacity(1) Text(item.name ?? "") .font(.caption) .fontWeight(.semibold) @@ -107,14 +119,14 @@ struct LibraryView: View { Image(systemName: "chevron.right") } } - Button(action: { + Label("Icon One", systemImage: "line.horizontal.3.decrease.circle") + .foregroundColor(viewModel.filters == defaultFilters ? .accentColor : Color(UIColor.systemOrange)) + .onTapGesture { isShowingFilterView = true - }) { - Image(systemName: "line.horizontal.3.decrease.circle") } - Button(action: { + Button { isShowingSearchView = true - }) { + } label: { Image(systemName: "magnifyingglass") } } diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 7215aa7d..2939fd3f 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -76,6 +76,17 @@ struct SeasonItemView: View { .frame(width: CGFloat(episode.userData!.playedPercentage ?? 0 * 1.5), height: 7) .padding(0), alignment: .bottomLeading ) + .overlay( + ZStack { + if(episode.userData!.played ?? false) { + Image(systemName: "circle.fill") + .foregroundColor(.white) + Image(systemName: "checkmark.circle.fill") + .foregroundColor(Color(.systemBlue)) + } + }.padding(2) + .opacity(1) + , alignment: .topTrailing).opacity(1) VStack(alignment: .leading) { HStack { Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline) diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 1dd2035b..333bd721 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -65,6 +65,10 @@ struct SettingsView: View { Text("Signed in as \(username)").foregroundColor(.primary) Spacer() Button { + let nc = NotificationCenter.default + nc.post(name: Notification.Name("didSignOut"), object: nil) + + SessionManager.current.logout() } label: { Text("Log out").font(.callout) diff --git a/Shared/Extensions/ImageView.swift b/Shared/Extensions/ImageView.swift index 70b084a2..0ce4d739 100644 --- a/Shared/Extensions/ImageView.swift +++ b/Shared/Extensions/ImageView.swift @@ -24,14 +24,16 @@ struct ImageView: View { } var body: some View { - LazyImage(source: source) - .placeholder { + LazyImage(source: source) { state in + if let image = state.image { + image + } else if state.error != nil { + Rectangle() + .fill(Color.gray) + } else { Image(uiImage: UIImage(blurHash: blurhash, size: CGSize(width: 16, height: 16))!) .resizable() } - .failure { - Rectangle() - .fill(Color.gray) - } + } } } diff --git a/Shared/ServerLocator/ServerDiscovery.swift b/Shared/ServerLocator/ServerDiscovery.swift index bc8d96ed..d35a44e7 100644 --- a/Shared/ServerLocator/ServerDiscovery.swift +++ b/Shared/ServerLocator/ServerDiscovery.swift @@ -66,11 +66,9 @@ public class ServerDiscovery { public init() { func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) { - print("RECIEVED \(ipAddress):\(String(port)) \(response)") } func errorHandler(error: UDPBroadcastConnection.ConnectionError) { - print(error) } self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler) } @@ -81,7 +79,6 @@ public class ServerDiscovery { let response = try JSONDecoder().decode(ServerLookupResponse.self, from: data) completion(response) } catch { - print(error) completion(nil) } } diff --git a/Shared/ServerLocator/UDPBroadCastConnection.swift b/Shared/ServerLocator/UDPBroadCastConnection.swift index f6fdc9d0..d28ab54e 100644 --- a/Shared/ServerLocator/UDPBroadCastConnection.swift +++ b/Shared/ServerLocator/UDPBroadCastConnection.swift @@ -123,7 +123,7 @@ open class UDPBroadcastConnection { // Set up cancel handler newResponseSource.setCancelHandler { - debugPrint("Closing UDP socket") + //debugPrint("Closing UDP socket") let UDPSocket = Int32(newResponseSource.handle) shutdown(UDPSocket, SHUT_RDWR) close(UDPSocket) @@ -158,12 +158,12 @@ open class UDPBroadcastConnection { guard let endpoint = withUnsafePointer(to: &socketAddress, { self.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1)) }) else { - debugPrint("Failed to get the address and port from the socket address received from recvfrom") + //debugPrint("Failed to get the address and port from the socket address received from recvfrom") self.closeConnection() return } - debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") + //debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") let responseBytes = Data(response[0.. 0 else { if let errorString = String(validatingUTF8: strerror(errno)) { - debugPrint("UDP connection failed to send data: \(errorString)") + //debugPrint("UDP connection failed to send data: \(errorString)") } closeConnection() throw ConnectionError.sendingMessageFailed(code: errno) @@ -221,7 +221,7 @@ open class UDPBroadcastConnection { if sent == broadcastMessageLength { // Success - debugPrint("UDP connection sent \(broadcastMessageLength) bytes") + //debugPrint("UDP connection sent \(broadcastMessageLength) bytes") } } } diff --git a/Shared/Typings/Typings.swift b/Shared/Typings/Typings.swift index fa220d70..99f9374c 100644 --- a/Shared/Typings/Typings.swift +++ b/Shared/Typings/Typings.swift @@ -18,7 +18,6 @@ struct LibraryFilters: Codable, Hashable { } public enum SortBy: String, Codable, CaseIterable { - case productionYear = "ProductionYear" case premiereDate = "PremiereDate" case name = "SortName" case dateAdded = "DateCreated" @@ -27,14 +26,12 @@ public enum SortBy: String, Codable, CaseIterable { extension SortBy { var localized: String { switch self { - case .productionYear: - return "Release Year" case .premiereDate: return "Premiere date" case .name: - return "Title" + return "Name" case .dateAdded: - return "Date Added" + return "Date added" } } } diff --git a/Shared/ViewModels/LibraryFilterViewModel.swift b/Shared/ViewModels/LibraryFilterViewModel.swift index d15dd52f..8b38372c 100644 --- a/Shared/ViewModels/LibraryFilterViewModel.swift +++ b/Shared/ViewModels/LibraryFilterViewModel.swift @@ -35,10 +35,26 @@ final class LibraryFilterViewModel: ViewModel { var possibleItemFilters = ItemFilter.supportedTypes @Published var enabledFilterType: [FilterType] + @Published + var selectedSortOrder: APISortOrder = .descending + @Published + var selectedSortBy: SortBy = .name + + func updateModifiedFilter() { + modifiedFilters.sortOrder = [selectedSortOrder] + modifiedFilters.sortBy = [selectedSortBy] + } + + func resetFilters() { + modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) + } init(filters: LibraryFilters? = nil, enabledFilterType: [FilterType] = [.tag, .genre, .sortBy, .sortOrder, .filter]) { self.enabledFilterType = enabledFilterType + self.selectedSortBy = filters!.sortBy.first! + self.selectedSortOrder = filters!.sortOrder.first! + super.init() if let filters = filters { self.modifiedFilters = filters From 3ee33461d34e47895df7e75f35965354175384af Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 25 Jun 2021 17:02:24 +0900 Subject: [PATCH 02/15] add Defaults package replaced UserDefaults.default with Defaults add prefer language settings --- .../VideoPlayerViewController.swift | 4 +- JellyfinPlayer.xcodeproj/project.pbxproj | 55 ++++++++++++---- .../xcshareddata/swiftpm/Package.resolved | 9 +++ JellyfinPlayer/JellyfinPlayer.entitlements | 4 +- JellyfinPlayer/PersistenceController.swift | 2 +- JellyfinPlayer/SettingsView.swift | 62 +++++++++++-------- JellyfinPlayer/VideoPlayer.swift | 5 +- Shared/Extensions/DefaultsExtension.swift | 19 ++++++ Shared/Singleton/SessionManager.swift | 6 +- Shared/ViewModels/SettingsViewModel.swift | 7 +++ Shared/ViewModels/SplashViewModel.swift | 9 +-- WidgetExtension/WidgetExtension.entitlements | 4 +- 12 files changed, 126 insertions(+), 60 deletions(-) create mode 100644 Shared/Extensions/DefaultsExtension.swift diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index afe48934..74e9262d 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -12,6 +12,7 @@ import TVVLCKit import MediaPlayer import JellyfinAPI import Combine +import Defaults protocol VideoPlayerSettingsDelegate: AnyObject { func selectNew(audioTrack id: Int32) @@ -149,8 +150,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, func fetchVideo() { // Fetch max bitrate from UserDefaults depending on current connection mode - let defaults = UserDefaults.standard - let maxBitrate = defaults.integer(forKey: "InNetworkBandwidth") + let maxBitrate = Defaults[.inNetworkBandwidth] // Build a device profile let builder = DeviceProfileBuilder() diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 0b493e32..3963e0f1 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -133,6 +133,10 @@ 628B95382670CDAB0091AF3B /* Model.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFF263B596B003A4E83 /* Model.xcdatamodeld */; }; 628B953A2670CE250091AF3B /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 628B95392670CE250091AF3B /* KeychainSwift */; }; 628B953C2670D2430091AF3B /* StringExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 621338922660107500A81A2A /* StringExtensions.swift */; }; + 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F452685BAF7003D0A6F /* Defaults */; }; + 62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 62CB3F472685BB3B003D0A6F /* Defaults */; }; + 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; + 62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */; }; 62E632DA267D2BC40063E547 /* LatestMediaViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */; }; 62E632DC267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; 62E632DD267D2E130063E547 /* LibrarySearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */; }; @@ -304,6 +308,7 @@ 628B952A2670CABE0091AF3B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 628B95362670CB800091AF3B /* JellyfinWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JellyfinWidget.swift; sourceTree = ""; }; 628B953B2670D1FC0091AF3B /* WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = WidgetExtension.entitlements; sourceTree = ""; }; + 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsExtension.swift; sourceTree = ""; }; 62E632D9267D2BC40063E547 /* LatestMediaViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LatestMediaViewModel.swift; sourceTree = ""; }; 62E632DB267D2E130063E547 /* LibrarySearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchViewModel.swift; sourceTree = ""; }; 62E632DF267D30CA0063E547 /* LibraryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryViewModel.swift; sourceTree = ""; }; @@ -331,6 +336,7 @@ 53EC6E1E267E80AC006DD26A /* Pods_JellyfinPlayer_tvOS.framework in Frameworks */, 53A431BF266B0FFE0016769F /* JellyfinAPI in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, + 62CB3F482685BB3B003D0A6F /* Defaults in Frameworks */, 5358708D2669D7A800D05A09 /* KeychainSwift in Frameworks */, 536D3D84267BEA550004248C /* ParallaxView in Frameworks */, 53ABFDDC267972BF00886593 /* TVServices.framework in Frameworks */, @@ -343,6 +349,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 62CB3F462685BAF7003D0A6F /* Defaults in Frameworks */, 5338F757263B7E2E0014BF09 /* KeychainSwift in Frameworks */, 53EC6E25267EB10F006DD26A /* SwiftyJSON in Frameworks */, 53EC6E21267E80B1006DD26A /* Pods_JellyfinPlayer_iOS.framework in Frameworks */, @@ -591,6 +598,7 @@ 6267B3D526710B8900A7371D /* CollectionExtensions.swift */, 6267B3D92671138200A7371D /* ImageExtensions.swift */, 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, + 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */, ); path = Extensions; sourceTree = ""; @@ -662,6 +670,7 @@ 53A431BE266B0FFE0016769F /* JellyfinAPI */, 53ABFDEC26799D7700886593 /* ActivityIndicator */, 536D3D83267BEA550004248C /* ParallaxView */, + 62CB3F472685BB3B003D0A6F /* Defaults */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* JellyfinPlayer tvOS.app */; @@ -693,6 +702,7 @@ 53A431BC266B0FF20016769F /* JellyfinAPI */, 625CB5792678C4A400530A6E /* ActivityIndicator */, 53EC6E24267EB10F006DD26A /* SwiftyJSON */, + 62CB3F452685BAF7003D0A6F /* Defaults */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* JellyfinPlayer iOS.app */; @@ -761,6 +771,7 @@ 625CB5782678C4A400530A6E /* XCRemoteSwiftPackageReference "ActivityIndicator" */, 536D3D82267BEA550004248C /* XCRemoteSwiftPackageReference "ParallaxView" */, 53EC6E23267EB10F006DD26A /* XCRemoteSwiftPackageReference "SwiftyJSON" */, + 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; @@ -944,6 +955,7 @@ 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, + 62CB3F4C2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, 62E632E4267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 5358706C2669D21700D05A09 /* PersistenceController.swift in Sources */, 535870AA2669D8AE00D05A09 /* BlurHashDecode.swift in Sources */, @@ -982,6 +994,7 @@ 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */, 625CB5772678C34300530A6E /* ConnectToServerViewModel.swift in Sources */, 536D3D78267BD5C30004248C /* ViewModel.swift in Sources */, + 62CB3F4B2685BB77003D0A6F /* DefaultsExtension.swift in Sources */, 53DE4BD02670961400739748 /* EpisodeItemView.swift in Sources */, 53F8377D265FF67C00F456B3 /* VideoPlayerSettingsView.swift in Sources */, 53192D5D265AA78A008A4215 /* DeviceProfileBuilder.swift in Sources */, @@ -1068,7 +1081,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = 4BHXT8RHFR; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; @@ -1077,7 +1090,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; + PRODUCT_BUNDLE_IDENTIFIER = dev.kwangmin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_VERSION = 5.0; @@ -1096,7 +1109,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = 4BHXT8RHFR; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; @@ -1105,7 +1118,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; + PRODUCT_BUNDLE_IDENTIFIER = dev.kwangmin.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_VERSION = 5.0; @@ -1245,7 +1258,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = 4BHXT8RHFR; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -1257,7 +1270,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; + PRODUCT_BUNDLE_IDENTIFIER = dev.pangmo5.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -1280,7 +1293,7 @@ CURRENT_PROJECT_VERSION = 54; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = 4BHXT8RHFR; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -1292,7 +1305,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; + PRODUCT_BUNDLE_IDENTIFIER = dev.pangmo5.swiftfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -1310,7 +1323,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 54; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = 4BHXT8RHFR; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -1319,7 +1332,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = dev.pangmo5.swiftfin.Widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; @@ -1335,7 +1348,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 54; - DEVELOPMENT_TEAM = 9R8RREG67J; + DEVELOPMENT_TEAM = 4BHXT8RHFR; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -1344,7 +1357,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin.widget; + PRODUCT_BUNDLE_IDENTIFIER = dev.pangmo5.swiftfin.Widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; @@ -1450,6 +1463,14 @@ minimumVersion = 1.1.0; }; }; + 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sindresorhus/Defaults"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.0.0; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1533,6 +1554,16 @@ package = 5338F755263B7E2E0014BF09 /* XCRemoteSwiftPackageReference "keychain-swift" */; productName = KeychainSwift; }; + 62CB3F452685BAF7003D0A6F /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; + 62CB3F472685BB3B003D0A6F /* Defaults */ = { + isa = XCSwiftPackageProductDependency; + package = 62CB3F442685BAF7003D0A6F /* XCRemoteSwiftPackageReference "Defaults" */; + productName = Defaults; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved index f56b4837..f98581b0 100644 --- a/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/JellyfinPlayer.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -19,6 +19,15 @@ "version": "0.6.0" } }, + { + "package": "Defaults", + "repositoryURL": "https://github.com/sindresorhus/Defaults", + "state": { + "branch": null, + "revision": "63d93f97ad545c8bceb125a8a36175ea705f7cf5", + "version": "5.0.0" + } + }, { "package": "Gifu", "repositoryURL": "https://github.com/kaishin/Gifu", diff --git a/JellyfinPlayer/JellyfinPlayer.entitlements b/JellyfinPlayer/JellyfinPlayer.entitlements index b6b038ce..58751d05 100644 --- a/JellyfinPlayer/JellyfinPlayer.entitlements +++ b/JellyfinPlayer/JellyfinPlayer.entitlements @@ -8,13 +8,13 @@ com.apple.security.application-groups - group.me.vigue.jellyfin.mobileclient + group.dev.pangmo5.swiftfin com.apple.security.network.client keychain-access-groups - $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain + $(AppIdentifierPrefix)dev.pangmo5.swiftfin.sharedKeychain diff --git a/JellyfinPlayer/PersistenceController.swift b/JellyfinPlayer/PersistenceController.swift index 5e7b23ae..3dc74491 100644 --- a/JellyfinPlayer/PersistenceController.swift +++ b/JellyfinPlayer/PersistenceController.swift @@ -30,7 +30,7 @@ struct PersistenceController { init(inMemory: Bool = false) { container = NSPersistentCloudKitContainer(name: "Model") container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.me.vigue.jellyfin.mobileclient")!.appendingPathComponent("\(container.name).sqlite"))] + .containerURL(forSecurityApplicationGroupIdentifier: "group.dev.pangmo5.swiftfin")!.appendingPathComponent("\(container.name).sqlite"))] if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 333bd721..c8c4fc1d 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -7,28 +7,30 @@ import CoreData import SwiftUI +import Defaults struct SettingsView: View { @Environment(\.managedObjectContext) private var viewContext - + @ObservedObject var viewModel: SettingsViewModel - + @Binding var close: Bool - @State private var inNetworkStreamBitrate: Int = 40_000_000 - @State private var outOfNetworkStreamBitrate: Int = 40_000_000 - @State private var autoSelectSubtitles: Bool = false - @State private var autoSelectSubtitlesLangcode: String = "none" + @Default(.inNetworkBandwidth) + var inNetworkStreamBitrate + @Default(.outOfNetworkBandwidth) + var outOfNetworkStreamBitrate + @Default(.isAutoSelectSubtitles) + var isAutoSelectSubtitles + @Default(.autoSelectSubtitlesLangCode) + var autoSelectSubtitlesLangcode + @Default(.autoSelectAudioLangCode) + var autoSelectAudioLangcode @State private var username: String = "" - + func onAppear() { - let defaults = UserDefaults.standard username = SessionManager.current.user.username ?? "" - inNetworkStreamBitrate = defaults.integer(forKey: "InNetworkBandwidth") - outOfNetworkStreamBitrate = defaults.integer(forKey: "OutOfNetworkBandwidth") - autoSelectSubtitles = defaults.bool(forKey: "AutoSelectSubtitles") - autoSelectSubtitlesLangcode = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? "" } - + var body: some View { NavigationView { Form { @@ -37,29 +39,35 @@ struct SettingsView: View { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in Text(bitrate.name).tag(bitrate.value) } - }.onChange(of: inNetworkStreamBitrate) { _ in - let defaults = UserDefaults.standard - defaults.setValue(_inNetworkStreamBitrate.wrappedValue, forKey: "InNetworkBandwidth") } - + Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in Text(bitrate.name).tag(bitrate.value) } - }.onChange(of: outOfNetworkStreamBitrate) { _ in - let defaults = UserDefaults.standard - defaults.setValue(_outOfNetworkStreamBitrate.wrappedValue, forKey: "OutOfNetworkBandwidth") } } - + Section(header: Text("Accessibility")) { - Toggle("Automatically show subtitles", isOn: $autoSelectSubtitles).onChange(of: autoSelectSubtitles, perform: { _ in - let defaults = UserDefaults.standard - defaults.setValue(autoSelectSubtitles, forKey: "AutoSelectSubtitles") - }) - Picker("Language preferences", selection: $autoSelectSubtitlesLangcode) {} + Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) + Picker("Subtitles Language preferences", selection: $autoSelectSubtitlesLangcode) { + Text("Auto") + .tag("Auto") + ForEach(viewModel.isoLanguageCodesPair, id: \.1) { + Text($0.0) + .tag($0.1) + } + } + Picker("Audio Language preferences", selection: $autoSelectAudioLangcode) { + Text("Auto") + .tag("Auto") + ForEach(viewModel.isoLanguageCodesPair, id: \.1) { + Text($0.0) + .tag($0.1) + } + } } - + Section { HStack { Text("Signed in as \(username)").foregroundColor(.primary) diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 24035f9d..f324b8a2 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -12,7 +12,7 @@ import MediaPlayer import Combine import GoogleCast import SwiftyJSON - +import Defaults enum PlayerDestination { case remote @@ -417,8 +417,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe mediaPlayer.drawable = videoContentView // Fetch max bitrate from UserDefaults depending on current connection mode - let defaults = UserDefaults.standard - let maxBitrate = defaults.integer(forKey: "InNetworkBandwidth") + let maxBitrate = Defaults[.inNetworkBandwidth] // Build a device profile let builder = DeviceProfileBuilder() diff --git a/Shared/Extensions/DefaultsExtension.swift b/Shared/Extensions/DefaultsExtension.swift new file mode 100644 index 00000000..ba24206d --- /dev/null +++ b/Shared/Extensions/DefaultsExtension.swift @@ -0,0 +1,19 @@ +// + /* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import Defaults + +extension Defaults.Keys { + static let inNetworkBandwidth = Key("InNetworkBandwidth", default: 40_000_000) + static let outOfNetworkBandwidth = Key("OutOfNetworkBandwidth", default: 40_000_000) + static let isAutoSelectSubtitles = Key("isAutoSelectSubtitles", default: false) + static let autoSelectSubtitlesLangCode = Key("AutoSelectSubtitlesLangCode", default: "Auto") + static let autoSelectAudioLangCode = Key("AutoSelectAudioLangCode", default: "Auto") +} diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index f0f27de8..6e559752 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -81,7 +81,7 @@ final class SessionManager { fileprivate func getAuthToken(userID: String) -> String? { let keychain = KeychainSwift() - keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" + keychain.accessGroup = "4BHXT8RHFR.dev.pangmo5.swiftfin.sharedKeychain" return keychain.get("AccessToken_\(userID)") } @@ -134,7 +134,7 @@ final class SessionManager { _ = try? PersistenceController.shared.container.viewContext.save() let keychain = KeychainSwift() - keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" + keychain.accessGroup = "4BHXT8RHFR.dev.pangmo5.swiftfin.sharedKeychain" keychain.set(accessToken!, forKey: "AccessToken_\(user.user_id!)") generateAuthHeader(with: accessToken) @@ -151,7 +151,7 @@ final class SessionManager { nc.post(name: Notification.Name("didSignOut"), object: nil) let keychain = KeychainSwift() - keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" + keychain.accessGroup = "4BHXT8RHFR.dev.pangmo5.swiftfin.sharedKeychain" keychain.delete("AccessToken_\(user?.user_id ?? "")") generateAuthHeader(with: nil) diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index cf3fe59a..8885b541 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -24,7 +24,9 @@ struct Bitrates: Codable, Hashable { } final class SettingsViewModel: ObservableObject { + let currentLocale = Locale.current var bitrates: [Bitrates] = [] + var isoLanguageCodesPair = [(name: String, isoCode: String)]() init() { let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")! @@ -39,5 +41,10 @@ final class SettingsViewModel: ObservableObject { } catch { print(error) } + + isoLanguageCodesPair = Locale.isoLanguageCodes.compactMap { + guard let name = currentLocale.localizedString(forLanguageCode: $0) else { return nil } + return (name, $0) + } } } diff --git a/Shared/ViewModels/SplashViewModel.swift b/Shared/ViewModels/SplashViewModel.swift index 28da5de6..7450f9de 100644 --- a/Shared/ViewModels/SplashViewModel.swift +++ b/Shared/ViewModels/SplashViewModel.swift @@ -10,6 +10,7 @@ import Foundation import Combine import Nuke +import Defaults #if !os(tvOS) import WidgetKit @@ -30,14 +31,6 @@ final class SplashViewModel: ViewModel { WidgetCenter.shared.reloadAllTimelines() #endif - let defaults = UserDefaults.standard - if defaults.integer(forKey: "InNetworkBandwidth") == 0 { - defaults.setValue(40_000_000, forKey: "InNetworkBandwidth") - } - if defaults.integer(forKey: "OutOfNetworkBandwidth") == 0 { - defaults.setValue(40_000_000, forKey: "OutOfNetworkBandwidth") - } - let nc = NotificationCenter.default nc.addObserver(self, selector: #selector(didLogIn), name: Notification.Name("didSignIn"), object: nil) nc.addObserver(self, selector: #selector(didLogOut), name: Notification.Name("didSignOut"), object: nil) diff --git a/WidgetExtension/WidgetExtension.entitlements b/WidgetExtension/WidgetExtension.entitlements index b164e1cb..b1737b11 100644 --- a/WidgetExtension/WidgetExtension.entitlements +++ b/WidgetExtension/WidgetExtension.entitlements @@ -4,11 +4,11 @@ com.apple.security.application-groups - group.me.vigue.jellyfin.mobileclient + group.dev.pangmo5.swiftfin keychain-access-groups - $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain + $(AppIdentifierPrefix)dev.pangmo5.swiftfin.sharedKeychain From f308219c34190dc7b7c371c78855b5bc2ed504bd Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 25 Jun 2021 17:42:40 +0900 Subject: [PATCH 03/15] Apply prefer language settings to VideoPlayer --- JellyfinPlayer/VideoPlayer.swift | 41 +++++++++++++++++++---- Shared/ViewModels/SettingsViewModel.swift | 2 +- Shared/ViewModels/VideoPlayerModel.swift | 2 ++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index f324b8a2..345733a5 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -455,7 +455,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe item.videoType = .transcode item.videoUrl = streamURL! - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "") + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", langCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -467,7 +467,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { deliveryUrl = nil } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt") + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", langCode: stream.language ?? "") if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) @@ -475,7 +475,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if stream.type == .audio { - let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!)) + let subtitle = AudioTrack(name: stream.displayTitle!, langCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) } @@ -499,7 +499,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe item.videoUrl = streamURL item.videoType = .directPlay - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "") + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", langCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -511,7 +511,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { deliveryUrl = nil } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!) + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!, langCode: stream.language ?? "") if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) @@ -519,7 +519,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if stream.type == .audio { - let subtitle = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!)) + let subtitle = AudioTrack(name: stream.displayTitle!, langCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) } @@ -542,7 +542,33 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe .store(in: &cancellables) } } - + + func setupTracksForPreferLanguage() { + subtitleTrackArray.forEach { subtitle in + if Defaults[.isAutoSelectSubtitles] { + if Defaults[.autoSelectSubtitlesLangCode] == "Auto", + subtitle.langCode.contains(Locale.current.languageCode ?? "") { + selectedCaptionTrack = subtitle.id + mediaPlayer.currentVideoSubTitleIndex = subtitle.id + } else if subtitle.langCode.contains(Defaults[.autoSelectSubtitlesLangCode]) { + selectedCaptionTrack = subtitle.id + mediaPlayer.currentVideoSubTitleIndex = subtitle.id + } + } + } + + audioTrackArray.forEach { audio in + if Defaults[.autoSelectAudioLangCode] == "Auto", + audio.langCode.contains(Locale.current.languageCode ?? "") { + selectedAudioTrack = audio.id + mediaPlayer.currentAudioTrackIndex = audio.id + } else if audio.langCode.contains(Defaults[.autoSelectAudioLangCode]) { + selectedAudioTrack = audio.id + mediaPlayer.currentAudioTrackIndex = audio.id + } + } + } + func startLocalPlaybackEngine(_ fetchCaptions: Bool) { print("Local playback engine starting.") mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) @@ -594,6 +620,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe mediaPlayer.pause() mediaPlayer.play() + setupTracksForPreferLanguage() print("Local engine started.") } diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index 8885b541..9db50528 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -45,6 +45,6 @@ final class SettingsViewModel: ObservableObject { isoLanguageCodesPair = Locale.isoLanguageCodes.compactMap { guard let name = currentLocale.localizedString(forLanguageCode: $0) else { return nil } return (name, $0) - } + }.sorted(by: { $0.name < $1.name }) } } diff --git a/Shared/ViewModels/VideoPlayerModel.swift b/Shared/ViewModels/VideoPlayerModel.swift index cb219a0d..2598b3f8 100644 --- a/Shared/ViewModels/VideoPlayerModel.swift +++ b/Shared/ViewModels/VideoPlayerModel.swift @@ -16,10 +16,12 @@ struct Subtitle { var url: URL? var delivery: SubtitleDeliveryMethod var codec: String + var langCode: String } struct AudioTrack { var name: String + var langCode: String var id: Int32 } From c27ebe5ad2effaddfcb7711c4ad2d32d7d5e29e1 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 25 Jun 2021 18:49:54 +0900 Subject: [PATCH 04/15] add SearchablePickerView --- JellyfinPlayer.xcodeproj/project.pbxproj | 6 ++ JellyfinPlayer/SettingsView.swift | 32 ++++----- Shared/Extensions/SearchablePickerView.swift | 73 ++++++++++++++++++++ Shared/ViewModels/SettingsViewModel.swift | 30 +++++--- 4 files changed, 114 insertions(+), 27 deletions(-) create mode 100644 Shared/Extensions/SearchablePickerView.swift diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 3963e0f1..e586b302 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -108,6 +108,8 @@ 621C638026672A30004216EA /* NukeUI in Frameworks */ = {isa = PBXBuildFile; productRef = 621C637F26672A30004216EA /* NukeUI */; }; 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; }; + 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; + 624C21762685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; }; 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; }; 625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56B2678C0FD00530A6E /* MainTabView.swift */; }; @@ -289,6 +291,7 @@ 621338922660107500A81A2A /* StringExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringExtensions.swift; sourceTree = ""; }; 621338B22660A07800A81A2A /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParallaxHeader.swift; sourceTree = ""; }; + 624C21742685CF60007F1390 /* SearchablePickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchablePickerView.swift; sourceTree = ""; }; 625CB5672678B6FB00530A6E /* SplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashView.swift; sourceTree = ""; }; 625CB5692678B71200530A6E /* SplashViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SplashViewModel.swift; sourceTree = ""; }; 625CB56B2678C0FD00530A6E /* MainTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabView.swift; sourceTree = ""; }; @@ -599,6 +602,7 @@ 6267B3D92671138200A7371D /* ImageExtensions.swift */, 62EC353326766B03000E9F2D /* DeviceRotationViewModifier.swift */, 62CB3F4A2685BB77003D0A6F /* DefaultsExtension.swift */, + 624C21742685CF60007F1390 /* SearchablePickerView.swift */, ); path = Extensions; sourceTree = ""; @@ -952,6 +956,7 @@ 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */, 5310695B2684E7EE00CFFDBA /* AudioView.swift in Sources */, + 624C21762685CF60007F1390 /* SearchablePickerView.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, @@ -1021,6 +1026,7 @@ 621338B32660A07800A81A2A /* LazyView.swift in Sources */, 531AC8BF26750DE20091C7EB /* ImageView.swift in Sources */, 62E632E0267D30CA0063E547 /* LibraryViewModel.swift in Sources */, + 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */, 62EC352F267666A5000E9F2D /* SessionManager.swift in Sources */, 62E632E3267D3BA60063E547 /* MovieItemViewModel.swift in Sources */, 091B5A8A2683142E00D78B61 /* ServerDiscovery.swift in Sources */, diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index c8c4fc1d..920fb24b 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -50,22 +50,22 @@ struct SettingsView: View { Section(header: Text("Accessibility")) { Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) - Picker("Subtitles Language preferences", selection: $autoSelectSubtitlesLangcode) { - Text("Auto") - .tag("Auto") - ForEach(viewModel.isoLanguageCodesPair, id: \.1) { - Text($0.0) - .tag($0.1) - } - } - Picker("Audio Language preferences", selection: $autoSelectAudioLangcode) { - Text("Auto") - .tag("Auto") - ForEach(viewModel.isoLanguageCodesPair, id: \.1) { - Text($0.0) - .tag($0.1) - } - } + SearchablePicker(label: "Subtitles Language preferences", + options: viewModel.langs, + optionToString: { $0.name }, + selected:Binding( + get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto }, + set: {autoSelectSubtitlesLangcode = $0.isoCode} + ) + ) + SearchablePicker(label: "Audio Language preferences", + options: viewModel.langs, + optionToString: { $0.name }, + selected: Binding( + get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto }, + set: { autoSelectAudioLangcode = $0.isoCode} + ) + ) } Section { diff --git a/Shared/Extensions/SearchablePickerView.swift b/Shared/Extensions/SearchablePickerView.swift new file mode 100644 index 00000000..5a9f20b8 --- /dev/null +++ b/Shared/Extensions/SearchablePickerView.swift @@ -0,0 +1,73 @@ +// +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ + +import Foundation +import SwiftUI + +private struct SearchablePickerView: View { + @Environment(\.presentationMode) + var presentationMode + + let options: [Selectable] + let optionToString: (Selectable) -> String + let label: String + + @State var text = "" + @Binding var selected: Selectable + + var body: some View { + VStack { + SearchBar(text: $text) + List(options.filter { + guard !text.isEmpty else { return true } + return optionToString($0).lowercased().contains(text.lowercased()) + }, id: \.self) { selectable in + Button(action: { + selected = selectable + presentationMode.wrappedValue.dismiss() + }) { + HStack { + Text(optionToString(selectable)).foregroundColor(Color.primary) + Spacer() + if selected == selectable { + Image(systemName: "checkmark").foregroundColor(.accentColor) + } + } + } + }.listStyle(GroupedListStyle()) + } + } +} + +struct SearchablePicker: View { + let label: String + let options: [Selectable] + let optionToString: (Selectable) -> String + + @Binding var selected: Selectable + + var body: some View { + NavigationLink(destination: searchablePickerView()) { + HStack { + Text(label) + Spacer() + Text(optionToString(selected)) + .foregroundColor(.gray) + .multilineTextAlignment(.trailing) + } + } + } + + private func searchablePickerView() -> some View { + SearchablePickerView(options: options, + optionToString: optionToString, + label: label, + selected: $selected) + } +} diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index 9db50528..12d03232 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -1,11 +1,11 @@ // - /* - * SwiftFin is subject to the terms of the Mozilla Public - * License, v2.0. If a copy of the MPL was not distributed with this - * file, you can obtain one at https://mozilla.org/MPL/2.0/. - * - * Copyright 2021 Aiden Vigue & Jellyfin Contributors - */ +/* + * SwiftFin is subject to the terms of the Mozilla Public + * License, v2.0. If a copy of the MPL was not distributed with this + * file, you can obtain one at https://mozilla.org/MPL/2.0/. + * + * Copyright 2021 Aiden Vigue & Jellyfin Contributors + */ import Foundation @@ -23,10 +23,17 @@ struct Bitrates: Codable, Hashable { public var value: Int } +struct Lang: Hashable { + var name: String + var isoCode: String + + static let auto = Lang(name: "Auto", isoCode: "Auto") +} + final class SettingsViewModel: ObservableObject { let currentLocale = Locale.current var bitrates: [Bitrates] = [] - var isoLanguageCodesPair = [(name: String, isoCode: String)]() + var langs = [Lang]() init() { let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")! @@ -41,10 +48,11 @@ final class SettingsViewModel: ObservableObject { } catch { print(error) } - - isoLanguageCodesPair = Locale.isoLanguageCodes.compactMap { + + self.langs = Locale.isoLanguageCodes.compactMap { guard let name = currentLocale.localizedString(forLanguageCode: $0) else { return nil } - return (name, $0) + return Lang(name: name, isoCode: $0) }.sorted(by: { $0.name < $1.name }) + self.langs.insert(.auto, at: 0) } } From 1ea6da0c2c8d69b51302865f8906703ad4a06247 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 25 Jun 2021 19:07:46 +0900 Subject: [PATCH 05/15] rollback signing --- JellyfinPlayer.xcodeproj/project.pbxproj | 20 ++++++++++---------- JellyfinPlayer/JellyfinPlayer.entitlements | 4 ++-- JellyfinPlayer/PersistenceController.swift | 2 +- JellyfinPlayer/SettingsView.swift | 2 +- Shared/Singleton/SessionManager.swift | 6 +++--- WidgetExtension/WidgetExtension.entitlements | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index e586b302..336e8f0c 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -1087,7 +1087,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = 4BHXT8RHFR; + DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; @@ -1115,7 +1115,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer tvOS/Preview Content\""; - DEVELOPMENT_TEAM = 4BHXT8RHFR; + DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = "$(inherited)"; INFOPLIST_FILE = "JellyfinPlayer tvOS/Info.plist"; @@ -1264,7 +1264,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 54; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 4BHXT8RHFR; + DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -1276,7 +1276,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.pangmo5.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -1299,7 +1299,7 @@ CURRENT_PROJECT_VERSION = 54; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 4BHXT8RHFR; + DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -1311,7 +1311,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.pangmo5.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTS_MACCATALYST = NO; @@ -1329,7 +1329,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 54; - DEVELOPMENT_TEAM = 4BHXT8RHFR; + DEVELOPMENT_TEAM = 9R8RREG67J; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -1338,7 +1338,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.pangmo5.swiftfin.Widget; + PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; @@ -1354,7 +1354,7 @@ CODE_SIGN_ENTITLEMENTS = WidgetExtension/WidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 54; - DEVELOPMENT_TEAM = 4BHXT8RHFR; + DEVELOPMENT_TEAM = 9R8RREG67J; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 14.1; LD_RUNPATH_SEARCH_PATHS = ( @@ -1363,7 +1363,7 @@ "@executable_path/../../Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.pangmo5.swiftfin.Widget; + PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin.widget; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; diff --git a/JellyfinPlayer/JellyfinPlayer.entitlements b/JellyfinPlayer/JellyfinPlayer.entitlements index 58751d05..b6b038ce 100644 --- a/JellyfinPlayer/JellyfinPlayer.entitlements +++ b/JellyfinPlayer/JellyfinPlayer.entitlements @@ -8,13 +8,13 @@ com.apple.security.application-groups - group.dev.pangmo5.swiftfin + group.me.vigue.jellyfin.mobileclient com.apple.security.network.client keychain-access-groups - $(AppIdentifierPrefix)dev.pangmo5.swiftfin.sharedKeychain + $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain diff --git a/JellyfinPlayer/PersistenceController.swift b/JellyfinPlayer/PersistenceController.swift index 3dc74491..5e7b23ae 100644 --- a/JellyfinPlayer/PersistenceController.swift +++ b/JellyfinPlayer/PersistenceController.swift @@ -30,7 +30,7 @@ struct PersistenceController { init(inMemory: Bool = false) { container = NSPersistentCloudKitContainer(name: "Model") container.persistentStoreDescriptions = [NSPersistentStoreDescription(url: FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.dev.pangmo5.swiftfin")!.appendingPathComponent("\(container.name).sqlite"))] + .containerURL(forSecurityApplicationGroupIdentifier: "group.me.vigue.jellyfin.mobileclient")!.appendingPathComponent("\(container.name).sqlite"))] if inMemory { container.persistentStoreDescriptions.first!.url = URL(fileURLWithPath: "/dev/null") diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 920fb24b..3e22665b 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -91,7 +91,7 @@ struct SettingsView: View { Button { close = false } label: { - Text("Back").font(.callout) + Image(systemName: "xmark") } } } diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index 6e559752..f0f27de8 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -81,7 +81,7 @@ final class SessionManager { fileprivate func getAuthToken(userID: String) -> String? { let keychain = KeychainSwift() - keychain.accessGroup = "4BHXT8RHFR.dev.pangmo5.swiftfin.sharedKeychain" + keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" return keychain.get("AccessToken_\(userID)") } @@ -134,7 +134,7 @@ final class SessionManager { _ = try? PersistenceController.shared.container.viewContext.save() let keychain = KeychainSwift() - keychain.accessGroup = "4BHXT8RHFR.dev.pangmo5.swiftfin.sharedKeychain" + keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" keychain.set(accessToken!, forKey: "AccessToken_\(user.user_id!)") generateAuthHeader(with: accessToken) @@ -151,7 +151,7 @@ final class SessionManager { nc.post(name: Notification.Name("didSignOut"), object: nil) let keychain = KeychainSwift() - keychain.accessGroup = "4BHXT8RHFR.dev.pangmo5.swiftfin.sharedKeychain" + keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" keychain.delete("AccessToken_\(user?.user_id ?? "")") generateAuthHeader(with: nil) diff --git a/WidgetExtension/WidgetExtension.entitlements b/WidgetExtension/WidgetExtension.entitlements index b1737b11..b164e1cb 100644 --- a/WidgetExtension/WidgetExtension.entitlements +++ b/WidgetExtension/WidgetExtension.entitlements @@ -4,11 +4,11 @@ com.apple.security.application-groups - group.dev.pangmo5.swiftfin + group.me.vigue.jellyfin.mobileclient keychain-access-groups - $(AppIdentifierPrefix)dev.pangmo5.swiftfin.sharedKeychain + $(AppIdentifierPrefix)me.vigue.jellyfin.sharedKeychain From 28d8fc7c476c71dc54bb07a1beb46463d9160761 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 25 Jun 2021 19:10:26 +0900 Subject: [PATCH 06/15] change "Auto" audioTrack logic --- JellyfinPlayer/VideoPlayer.swift | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 345733a5..295d05b5 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -558,11 +558,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } audioTrackArray.forEach { audio in - if Defaults[.autoSelectAudioLangCode] == "Auto", - audio.langCode.contains(Locale.current.languageCode ?? "") { - selectedAudioTrack = audio.id - mediaPlayer.currentAudioTrackIndex = audio.id - } else if audio.langCode.contains(Defaults[.autoSelectAudioLangCode]) { + if audio.langCode.contains(Defaults[.autoSelectAudioLangCode]) { selectedAudioTrack = audio.id mediaPlayer.currentAudioTrackIndex = audio.id } From 4eae19aa5364288f693685fb2a369a42e2d79802 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 25 Jun 2021 19:12:04 +0900 Subject: [PATCH 07/15] fix str --- JellyfinPlayer/SettingsView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 3e22665b..ca6b3204 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -50,7 +50,7 @@ struct SettingsView: View { Section(header: Text("Accessibility")) { Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) - SearchablePicker(label: "Subtitles Language preferences", + SearchablePicker(label: "Subtitles language preferences", options: viewModel.langs, optionToString: { $0.name }, selected:Binding( @@ -58,7 +58,7 @@ struct SettingsView: View { set: {autoSelectSubtitlesLangcode = $0.isoCode} ) ) - SearchablePicker(label: "Audio Language preferences", + SearchablePicker(label: "Audio language preferences", options: viewModel.langs, optionToString: { $0.name }, selected: Binding( From 32e314d9e31b796cf7794b1496c2f06b08271eaa Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 25 Jun 2021 19:13:35 +0900 Subject: [PATCH 08/15] rollback signing --- JellyfinPlayer.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index 336e8f0c..aefad28b 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -1096,7 +1096,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.kwangmin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_VERSION = 5.0; @@ -1124,7 +1124,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = dev.kwangmin.swiftfin; + PRODUCT_BUNDLE_IDENTIFIER = me.vigue.jellyfin; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = appletvos; SWIFT_VERSION = 5.0; From 9dbcfb694e65d384e03625f6ea2d4dbb03bcf1ad Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Fri, 25 Jun 2021 19:28:37 +0900 Subject: [PATCH 09/15] fix tvOS build error --- .../VideoPlayer/VideoPlayerViewController.swift | 6 +++--- JellyfinPlayer.xcodeproj/project.pbxproj | 2 -- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 74e9262d..da07fd6d 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -195,7 +195,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, item.videoUrl = streamURL - let disableSubtitleTrack = Subtitle(name: "None", id: -1, url: nil, delivery: .embed, codec: "") + let disableSubtitleTrack = Subtitle(name: "None", id: -1, url: nil, delivery: .embed, codec: "", langCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -208,7 +208,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt") + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", langCode: stream.language ?? "") if stream.isDefault == true{ selectedCaptionTrack = Int32(stream.index!) @@ -220,7 +220,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } if stream.type == .audio { - let track = AudioTrack(name: stream.displayTitle!, id: Int32(stream.index!)) + let track = AudioTrack(name: stream.displayTitle!, langCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index aefad28b..a1ad7550 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -109,7 +109,6 @@ 6225FCCB2663841E00E067F6 /* ParallaxHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6225FCCA2663841E00E067F6 /* ParallaxHeader.swift */; }; 6228B1C22670EB010067FD35 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5377CBFD263B596B003A4E83 /* PersistenceController.swift */; }; 624C21752685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; - 624C21762685CF60007F1390 /* SearchablePickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 624C21742685CF60007F1390 /* SearchablePickerView.swift */; }; 625CB5682678B6FB00530A6E /* SplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5672678B6FB00530A6E /* SplashView.swift */; }; 625CB56A2678B71200530A6E /* SplashViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB5692678B71200530A6E /* SplashViewModel.swift */; }; 625CB56C2678C0FD00530A6E /* MainTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625CB56B2678C0FD00530A6E /* MainTabView.swift */; }; @@ -956,7 +955,6 @@ 536D3D81267BDFC60004248C /* PortraitItemElement.swift in Sources */, 531690E5267ABD5C005D8AB9 /* MainTabView.swift in Sources */, 5310695B2684E7EE00CFFDBA /* AudioView.swift in Sources */, - 624C21762685CF60007F1390 /* SearchablePickerView.swift in Sources */, 091B5A8D268315D400D78B61 /* ServerDiscovery.swift in Sources */, 53ABFDE7267974EF00886593 /* ConnectToServerViewModel.swift in Sources */, 53ABFDEE26799DCD00886593 /* ImageView.swift in Sources */, From 5a27ddf714963ac094eed2b02e33e03c92f8d300 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sat, 26 Jun 2021 02:32:41 +0900 Subject: [PATCH 10/15] fix requested changes --- JellyfinPlayer/SettingsView.swift | 19 +++++++------------ JellyfinPlayer/VideoPlayer.swift | 4 ++-- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index ca6b3204..a2b44fd3 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -15,16 +15,11 @@ struct SettingsView: View { @ObservedObject var viewModel: SettingsViewModel @Binding var close: Bool - @Default(.inNetworkBandwidth) - var inNetworkStreamBitrate - @Default(.outOfNetworkBandwidth) - var outOfNetworkStreamBitrate - @Default(.isAutoSelectSubtitles) - var isAutoSelectSubtitles - @Default(.autoSelectSubtitlesLangCode) - var autoSelectSubtitlesLangcode - @Default(.autoSelectAudioLangCode) - var autoSelectAudioLangcode + @Default(.inNetworkBandwidth) var inNetworkStreamBitrate + @Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate + @Default(.isAutoSelectSubtitles) var isAutoSelectSubtitles + @Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode + @Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode @State private var username: String = "" func onAppear() { @@ -50,7 +45,7 @@ struct SettingsView: View { Section(header: Text("Accessibility")) { Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) - SearchablePicker(label: "Subtitles language preferences", + SearchablePicker(label: "Preferred subtitle language", options: viewModel.langs, optionToString: { $0.name }, selected:Binding( @@ -58,7 +53,7 @@ struct SettingsView: View { set: {autoSelectSubtitlesLangcode = $0.isoCode} ) ) - SearchablePicker(label: "Audio language preferences", + SearchablePicker(label: "Preferred audio language", options: viewModel.langs, optionToString: { $0.name }, selected: Binding( diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 295d05b5..4890aeec 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -543,7 +543,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } - func setupTracksForPreferLanguage() { + func setupTracksForPreferredDefaults() { subtitleTrackArray.forEach { subtitle in if Defaults[.isAutoSelectSubtitles] { if Defaults[.autoSelectSubtitlesLangCode] == "Auto", @@ -616,7 +616,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe mediaPlayer.pause() mediaPlayer.play() - setupTracksForPreferLanguage() + setupTracksForPreferredDefaults() print("Local engine started.") } From 9e27114cf07421f17b5dcc2c88d37f7b14fa7d1e Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sat, 26 Jun 2021 02:40:10 +0900 Subject: [PATCH 11/15] Remove unused import Fix formatting --- Shared/Extensions/SearchablePickerView.swift | 3 +-- Shared/ViewModels/SplashViewModel.swift | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Shared/Extensions/SearchablePickerView.swift b/Shared/Extensions/SearchablePickerView.swift index 5a9f20b8..91550271 100644 --- a/Shared/Extensions/SearchablePickerView.swift +++ b/Shared/Extensions/SearchablePickerView.swift @@ -11,8 +11,7 @@ import Foundation import SwiftUI private struct SearchablePickerView: View { - @Environment(\.presentationMode) - var presentationMode + @Environment(\.presentationMode) var presentationMode let options: [Selectable] let optionToString: (Selectable) -> String diff --git a/Shared/ViewModels/SplashViewModel.swift b/Shared/ViewModels/SplashViewModel.swift index 7450f9de..57776ab6 100644 --- a/Shared/ViewModels/SplashViewModel.swift +++ b/Shared/ViewModels/SplashViewModel.swift @@ -10,7 +10,6 @@ import Foundation import Combine import Nuke -import Defaults #if !os(tvOS) import WidgetKit From fe5f41838c3877704911ce50998999f1745a5e05 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sat, 26 Jun 2021 03:05:05 +0900 Subject: [PATCH 12/15] fix naming --- .../VideoPlayer/VideoPlayerViewController.swift | 6 +++--- Shared/ViewModels/SettingsViewModel.swift | 8 ++++---- Shared/ViewModels/VideoPlayerModel.swift | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index da07fd6d..12d8a4af 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -195,7 +195,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, item.videoUrl = streamURL - let disableSubtitleTrack = Subtitle(name: "None", id: -1, url: nil, delivery: .embed, codec: "", langCode: "") + let disableSubtitleTrack = Subtitle(name: "None", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -208,7 +208,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", langCode: stream.language ?? "") + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") if stream.isDefault == true{ selectedCaptionTrack = Int32(stream.index!) @@ -220,7 +220,7 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } if stream.type == .audio { - let track = AudioTrack(name: stream.displayTitle!, langCode: stream.language ?? "", id: Int32(stream.index!)) + let track = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index 12d03232..eba576d6 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -23,17 +23,17 @@ struct Bitrates: Codable, Hashable { public var value: Int } -struct Lang: Hashable { +struct TrackLanguage: Hashable { var name: String var isoCode: String - static let auto = Lang(name: "Auto", isoCode: "Auto") + static let auto = TrackLanguage(name: "Auto", isoCode: "Auto") } final class SettingsViewModel: ObservableObject { let currentLocale = Locale.current var bitrates: [Bitrates] = [] - var langs = [Lang]() + var langs = [TrackLanguage]() init() { let url = Bundle.main.url(forResource: "bitrates", withExtension: "json")! @@ -51,7 +51,7 @@ final class SettingsViewModel: ObservableObject { self.langs = Locale.isoLanguageCodes.compactMap { guard let name = currentLocale.localizedString(forLanguageCode: $0) else { return nil } - return Lang(name: name, isoCode: $0) + return TrackLanguage(name: name, isoCode: $0) }.sorted(by: { $0.name < $1.name }) self.langs.insert(.auto, at: 0) } diff --git a/Shared/ViewModels/VideoPlayerModel.swift b/Shared/ViewModels/VideoPlayerModel.swift index 2598b3f8..117c22bb 100644 --- a/Shared/ViewModels/VideoPlayerModel.swift +++ b/Shared/ViewModels/VideoPlayerModel.swift @@ -16,12 +16,12 @@ struct Subtitle { var url: URL? var delivery: SubtitleDeliveryMethod var codec: String - var langCode: String + var languageCode: String } struct AudioTrack { var name: String - var langCode: String + var languageCode: String var id: Int32 } From 3d2e12462fddf3855571d7b69efde0840b7faaee Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sat, 26 Jun 2021 03:14:58 +0900 Subject: [PATCH 13/15] fix build error --- JellyfinPlayer/VideoPlayer.swift | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 4890aeec..198dd4cd 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -455,7 +455,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe item.videoType = .transcode item.videoUrl = streamURL! - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", langCode: "") + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -467,7 +467,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { deliveryUrl = nil } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", langCode: stream.language ?? "") + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) @@ -475,7 +475,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if stream.type == .audio { - let subtitle = AudioTrack(name: stream.displayTitle!, langCode: stream.language ?? "", id: Int32(stream.index!)) + let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) } @@ -499,7 +499,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe item.videoUrl = streamURL item.videoType = .directPlay - let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", langCode: "") + let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) // Loop through media streams and add to array @@ -511,7 +511,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { deliveryUrl = nil } - let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!, langCode: stream.language ?? "") + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec!, languageCode: stream.language ?? "") if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) @@ -519,7 +519,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } if stream.type == .audio { - let subtitle = AudioTrack(name: stream.displayTitle!, langCode: stream.language ?? "", id: Int32(stream.index!)) + let subtitle = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) } @@ -547,10 +547,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe subtitleTrackArray.forEach { subtitle in if Defaults[.isAutoSelectSubtitles] { if Defaults[.autoSelectSubtitlesLangCode] == "Auto", - subtitle.langCode.contains(Locale.current.languageCode ?? "") { + subtitle.languageCode.contains(Locale.current.languageCode ?? "") { selectedCaptionTrack = subtitle.id mediaPlayer.currentVideoSubTitleIndex = subtitle.id - } else if subtitle.langCode.contains(Defaults[.autoSelectSubtitlesLangCode]) { + } else if subtitle.languageCode.contains(Defaults[.autoSelectSubtitlesLangCode]) { selectedCaptionTrack = subtitle.id mediaPlayer.currentVideoSubTitleIndex = subtitle.id } @@ -558,7 +558,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } audioTrackArray.forEach { audio in - if audio.langCode.contains(Defaults[.autoSelectAudioLangCode]) { + if audio.languageCode.contains(Defaults[.autoSelectAudioLangCode]) { selectedAudioTrack = audio.id mediaPlayer.currentAudioTrackIndex = audio.id } From 7fadbdfe7cd9581bf4509b8eb52685551f123d08 Mon Sep 17 00:00:00 2001 From: PangMo5 Date: Sat, 26 Jun 2021 03:34:19 +0900 Subject: [PATCH 14/15] fix build error --- JellyfinPlayer/SettingsView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index a2b44fd3..9c15655f 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -48,7 +48,7 @@ struct SettingsView: View { SearchablePicker(label: "Preferred subtitle language", options: viewModel.langs, optionToString: { $0.name }, - selected:Binding( + selected:Binding( get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto }, set: {autoSelectSubtitlesLangcode = $0.isoCode} ) @@ -56,7 +56,7 @@ struct SettingsView: View { SearchablePicker(label: "Preferred audio language", options: viewModel.langs, optionToString: { $0.name }, - selected: Binding( + selected: Binding( get: { viewModel.langs.first(where: { $0.isoCode == autoSelectAudioLangcode }) ?? .auto }, set: { autoSelectAudioLangcode = $0.isoCode} ) From e6ede1280d31d77843b0889ee6d549f7181e0462 Mon Sep 17 00:00:00 2001 From: acvigue Date: Fri, 25 Jun 2021 18:46:43 +0000 Subject: [PATCH 15/15] [create-pull-request] automated change --- JellyfinPlayer tvOS/ConnectToServerView.swift | 7 +- .../VideoPlayer/AudioView.swift | 31 +- .../InfoTabBarViewController.swift | 52 +-- .../VideoPlayer/MediaInfoView.swift | 65 ++- .../VideoPlayer/SubtitlesView.swift | 38 +- .../VideoPlayer/VideoPlayer.swift | 8 +- .../VideoPlayerViewController.swift | 419 +++++++++--------- JellyfinPlayer/ConnectToServerView.swift | 4 +- JellyfinPlayer/ContinueWatchingView.swift | 10 +- JellyfinPlayer/HomeView.swift | 6 +- JellyfinPlayer/LatestMediaView.swift | 7 +- JellyfinPlayer/LibraryListView.swift | 20 +- JellyfinPlayer/LibrarySearchView.swift | 7 +- JellyfinPlayer/LibraryView.swift | 5 +- JellyfinPlayer/NextUpView.swift | 4 +- JellyfinPlayer/SeasonItemView.swift | 5 +- JellyfinPlayer/SettingsView.swift | 19 +- JellyfinPlayer/VideoPlayer.swift | 205 ++++----- .../VideoPlayerCastDeviceSelector.swift | 9 +- JellyfinPlayer/VideoPlayerSettingsView.swift | 2 +- Shared/Extensions/APIExtensions.swift | 4 +- Shared/ServerLocator/ServerDiscovery.swift | 20 +- .../UDPBroadCastConnection.swift | 135 +++--- Shared/Singleton/SessionManager.swift | 2 +- .../ViewModels/ConnectToServerViewModel.swift | 16 +- .../ViewModels/LibraryFilterViewModel.swift | 6 +- Shared/ViewModels/SettingsViewModel.swift | 2 +- WidgetExtension/NextUpWidget.swift | 8 +- 28 files changed, 524 insertions(+), 592 deletions(-) diff --git a/JellyfinPlayer tvOS/ConnectToServerView.swift b/JellyfinPlayer tvOS/ConnectToServerView.swift index 6bda55ec..998e3689 100644 --- a/JellyfinPlayer tvOS/ConnectToServerView.swift +++ b/JellyfinPlayer tvOS/ConnectToServerView.swift @@ -105,7 +105,7 @@ struct ConnectToServerView: View { } } else { if !viewModel.isLoading { - + Form { Section(header: Text("Server Information")) { TextField("Jellyfin Server URL", text: $uri) @@ -144,15 +144,14 @@ struct ConnectToServerView: View { Image(systemName: "chevron.forward") .padding() } - + }) .disabled(viewModel.isLoading) } } .onAppear(perform: self.viewModel.discoverServers) } - } - else { + } else { ProgressView() } } diff --git a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift index 66e3a035..a513c98a 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/AudioView.swift @@ -10,19 +10,17 @@ import SwiftUI class AudioViewController: UIViewController { - - var height : CGFloat = 420 - + var height: CGFloat = 420 + override func viewDidLoad() { super.viewDidLoad() - + tabBarItem.title = "Audio" - + } - - func prepareAudioView(audioTracks: [AudioTrack], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) - { + + func prepareAudioView(audioTracks: [AudioTrack], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) { let contentView = UIHostingController(rootView: AudioView(selectedTrack: selectedTrack, audioTrackArray: audioTracks, delegate: delegate)) self.view.addSubview(contentView.view) contentView.view.translatesAutoresizingMaskIntoConstraints = false @@ -30,38 +28,37 @@ class AudioViewController: UIViewController { contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - + } } struct AudioView: View { - - @State var selectedTrack : Int32 = -1 + + @State var selectedTrack: Int32 = -1 @State var audioTrackArray: [AudioTrack] = [] - + weak var delegate: VideoPlayerSettingsDelegate? var body : some View { NavigationView { - VStack() { + VStack { List(audioTrackArray, id: \.id) { track in Button(action: { delegate?.selectNew(audioTrack: track.id) selectedTrack = track.id }, label: { - HStack(spacing: 10){ + HStack(spacing: 10) { if track.id == selectedTrack { Image(systemName: "checkmark") - } - else { + } else { Image(systemName: "checkmark") .hidden() } Text(track.name) } }) - + } } .frame(width: 400) diff --git a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift index c7cfd218..f8077693 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/InfoTabBarViewController.swift @@ -11,15 +11,14 @@ import TVUIKit import JellyfinAPI class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate { - - var videoPlayer : VideoPlayerViewController? = nil - var subtitleViewController : SubtitlesViewController? = nil - var audioViewController : AudioViewController? = nil - var mediaInfoController : MediaInfoViewController? = nil - var infoContainerPos : CGRect? = nil - var tabBarHeight : CGFloat = 0 - + var videoPlayer: VideoPlayerViewController? + var subtitleViewController: SubtitlesViewController? + var audioViewController: AudioViewController? + var mediaInfoController: MediaInfoViewController? + var infoContainerPos: CGRect? + var tabBarHeight: CGFloat = 0 + // override func viewWillAppear(_ animated: Bool) { // tabBar.standardAppearance.backgroundColor = .clear // tabBar.standardAppearance.backgroundImage = UIImage() @@ -40,40 +39,38 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate mediaInfoController = MediaInfoViewController() audioViewController = AudioViewController() subtitleViewController = SubtitlesViewController() - + viewControllers = [mediaInfoController!, audioViewController!, subtitleViewController!] tabBarHeight = tabBar.frame.size.height - + tabBar.standardAppearance.backgroundColor = .clear tabBar.standardAppearance.backgroundImage = UIImage() tabBar.standardAppearance.backgroundEffect = .none tabBar.barTintColor = .clear - + } - - func setupInfoViews(mediaItem: BaseItemDto, subtitleTracks: [Subtitle], selectedSubtitleTrack : Int32, audioTracks: [AudioTrack], selectedAudioTrack: Int32, delegate: VideoPlayerSettingsDelegate) { - + + func setupInfoViews(mediaItem: BaseItemDto, subtitleTracks: [Subtitle], selectedSubtitleTrack: Int32, audioTracks: [AudioTrack], selectedAudioTrack: Int32, delegate: VideoPlayerSettingsDelegate) { + mediaInfoController?.setMedia(item: mediaItem) - + audioViewController?.prepareAudioView(audioTracks: audioTracks, selectedTrack: selectedAudioTrack, delegate: delegate) - + subtitleViewController?.prepareSubtitleView(subtitleTracks: subtitleTracks, selectedTrack: selectedSubtitleTrack, delegate: delegate) - + if let videoPlayer = videoPlayer { infoContainerPos = CGRect(x: 88, y: 87, width: videoPlayer.infoViewContainer.frame.width, height: videoPlayer.infoViewContainer.frame.height) - + } - - - + } - + override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { guard let pos = infoContainerPos else { return } - + switch item.title { case "Audio": if var height = audioViewController?.height { @@ -83,7 +80,6 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate } - } break case "Info": @@ -97,7 +93,7 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate } break case "Subtitles": - if var height = subtitleViewController?.height{ + if var height = subtitleViewController?.height { height += tabBarHeight UIView.animate(withDuration: 0.6, delay: 0, options: .curveEaseOut) { [self] in videoPlayer?.infoViewContainer.frame = CGRect(x: pos.minX, y: pos.minY, width: pos.width, height: height) @@ -110,13 +106,11 @@ class InfoTabBarViewController: UITabBarController, UIGestureRecognizerDelegate break } } - + func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return true } - - - + // MARK: - Navigation // // In a storyboard-based application, you will often want to do a little preparation before navigation diff --git a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift index b210c572..68d952b7 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/MediaInfoView.swift @@ -12,18 +12,16 @@ import JellyfinAPI class MediaInfoViewController: UIViewController { private var contentView: UIHostingController! - - var height : CGFloat = 0 - - + + var height: CGFloat = 0 + override func viewDidLoad() { super.viewDidLoad() - + tabBarItem.title = "Info" } - - func setMedia(item: BaseItemDto) - { + + func setMedia(item: BaseItemDto) { contentView = UIHostingController(rootView: MediaInfoView(item: item)) self.view.addSubview(contentView.view) contentView.view.translatesAutoresizingMaskIntoConstraints = false @@ -31,40 +29,38 @@ class MediaInfoViewController: UIViewController { contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - + height = self.view.frame.height - + } } struct MediaInfoView: View { - @State var item : BaseItemDto? = nil - + @State var item: BaseItemDto? + var body: some View { if let item = item { HStack(spacing: 30) { - + VStack { ImageView(src: item.type == "Episode" ? item.getSeriesPrimaryImage(maxWidth: 200) : item.getPrimaryImage(maxWidth: 200), bh: item.type == "Episode" ? item.getSeriesPrimaryImageBlurHash() : item.getPrimaryImageBlurHash()) .frame(width: 200, height: 300) .cornerRadius(10) Spacer() } - + VStack(alignment: .leading, spacing: 10) { if item.type == "Episode" { Text(item.seriesName ?? "Series") .fontWeight(.bold) - + Text(item.name ?? "Episode") .foregroundColor(.secondary) - } - else - { + } else { Text(item.name ?? "Movie") .fontWeight(.bold) } - + HStack(spacing: 10) { if item.type == "Episode" { Text("S\(item.parentIndexNumber ?? 0) • E\(item.indexNumber ?? 0)") @@ -73,56 +69,53 @@ struct MediaInfoView: View { Text("•") Text(formatDate(date: date)) } - + } else if let year = item.productionYear { Text(String(year)) } - + if item.runTimeTicks != nil { Text("•") Text(item.getItemRuntime()) } - + if let rating = item.officialRating { Text("•") - + Text("\(rating)").font(.subheadline) .fontWeight(.semibold) .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) .overlay(RoundedRectangle(cornerRadius: 2) .stroke(Color.secondary, lineWidth: 1)) - + } } .foregroundColor(.secondary) - + if let overview = item.overview { Text(overview) .padding(.top) .foregroundColor(.secondary) } - - + Spacer() } - + Spacer() - + } .padding(.leading, 350) .padding(.trailing, 125) - } - else { + } else { EmptyView() } - + } - - - func formatDate(date : Date) -> String{ + + func formatDate(date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "d MMM yyyy" - + return formatter.string(from: date) } } diff --git a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift index 1ea20c2c..4d43864d 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/SubtitlesView.swift @@ -10,19 +10,17 @@ import SwiftUI class SubtitlesViewController: UIViewController { - - var height : CGFloat = 420 - + var height: CGFloat = 420 + override func viewDidLoad() { super.viewDidLoad() - + tabBarItem.title = "Subtitles" - + } - - func prepareSubtitleView(subtitleTracks: [Subtitle], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) - { + + func prepareSubtitleView(subtitleTracks: [Subtitle], selectedTrack: Int32, delegate: VideoPlayerSettingsDelegate) { let contentView = UIHostingController(rootView: SubtitleView(selectedTrack: selectedTrack, subtitleTrackArray: subtitleTracks, delegate: delegate)) self.view.addSubview(contentView.view) contentView.view.translatesAutoresizingMaskIntoConstraints = false @@ -30,44 +28,42 @@ class SubtitlesViewController: UIViewController { contentView.view.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true contentView.view.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true contentView.view.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true - + } } struct SubtitleView: View { - - @State var selectedTrack : Int32 = -1 + + @State var selectedTrack: Int32 = -1 @State var subtitleTrackArray: [Subtitle] = [] - + weak var delegate: VideoPlayerSettingsDelegate? - - + var body : some View { NavigationView { - VStack() { + VStack { List(subtitleTrackArray, id: \.id) { track in Button(action: { delegate?.selectNew(subtitleTrack: track.id) selectedTrack = track.id }, label: { - HStack(spacing: 10){ + HStack(spacing: 10) { if track.id == selectedTrack { Image(systemName: "checkmark") - } - else { + } else { Image(systemName: "checkmark") .hidden() } Text(track.name) } }) - + } } .frame(width: 400) .frame(maxHeight: 400) - + } } - + } diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift index bc007e0c..d2017909 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayer.swift @@ -12,17 +12,17 @@ import JellyfinAPI struct VideoPlayerView: UIViewControllerRepresentable { var item: BaseItemDto - + func makeUIViewController(context: Context) -> some UIViewController { - + let storyboard = UIStoryboard(name: "VideoPlayerStoryboard", bundle: nil) let viewController = storyboard.instantiateViewController(withIdentifier: "VideoPlayer") as! VideoPlayerViewController viewController.manifest = item return viewController } - + func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) { - + } } diff --git a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift index 12d8a4af..1e2ff466 100644 --- a/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift +++ b/JellyfinPlayer tvOS/VideoPlayer/VideoPlayerViewController.swift @@ -19,37 +19,36 @@ protocol VideoPlayerSettingsDelegate: AnyObject { func selectNew(subtitleTrack id: Int32) } -class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, VLCMediaPlayerDelegate, VLCMediaDelegate, UIGestureRecognizerDelegate { - +class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, VLCMediaPlayerDelegate, VLCMediaDelegate, UIGestureRecognizerDelegate { @IBOutlet weak var videoContentView: UIView! @IBOutlet weak var controlsView: UIView! - + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! - + @IBOutlet weak var transportBarView: UIView! @IBOutlet weak var scrubberView: UIView! @IBOutlet weak var scrubLabel: UILabel! @IBOutlet weak var gradientView: UIView! - + @IBOutlet weak var currentTimeLabel: UILabel! @IBOutlet weak var remainingTimeLabel: UILabel! - + @IBOutlet weak var infoViewContainer: UIView! - - var infoPanelDisplayPoint : CGPoint = .zero - var infoPanelHiddenPoint : CGPoint = .zero - + + var infoPanelDisplayPoint: CGPoint = .zero + var infoPanelHiddenPoint: CGPoint = .zero + var containerViewController: InfoTabBarViewController? - var focusedOnTabBar : Bool = false - var showingInfoPanel : Bool = false - + var focusedOnTabBar: Bool = false + var showingInfoPanel: Bool = false + var mediaPlayer = VLCMediaPlayer() - + var lastProgressReportTime: Double = 0 var lastTime: Float = 0.0 var startTime: Int = 0 - + var selectedAudioTrack: Int32 = -1 { didSet { print(selectedAudioTrack) @@ -60,73 +59,68 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, print(selectedCaptionTrack) } } - + var subtitleTrackArray: [Subtitle] = [] var audioTrackArray: [AudioTrack] = [] - + var playing: Bool = false var seeking: Bool = false var showingControls: Bool = false var loading: Bool = true - - var initialSeekPos : CGFloat = 0 + + var initialSeekPos: CGFloat = 0 var videoPos: Double = 0 var videoDuration: Double = 0 var controlsAppearTime: Double = 0 - - + var manifest: BaseItemDto = BaseItemDto() var playbackItem = PlaybackItem() var playSessionId: String = "" - + var cancellables = Set() - - + override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { - + super.didUpdateFocus(in: context, with: coordinator) - + // Check if focused on the tab bar, allows for swipe up to dismiss the info panel - if context.nextFocusedView!.description.contains("UITabBarButton") - { + if context.nextFocusedView!.description.contains("UITabBarButton") { // Set value after half a second so info panel is not dismissed instantly when swiping up from content DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.focusedOnTabBar = true } - } - else - { + } else { focusedOnTabBar = false } - + } - + override func viewDidLoad() { super.viewDidLoad() - + activityIndicator.isHidden = false activityIndicator.startAnimating() - + mediaPlayer.delegate = self mediaPlayer.drawable = videoContentView - + if let runTimeTicks = manifest.runTimeTicks { videoDuration = Double(runTimeTicks / 10_000_000) } - + // Black gradient behind transport bar - let gradientLayer:CAGradientLayer = CAGradientLayer() + let gradientLayer: CAGradientLayer = CAGradientLayer() gradientLayer.frame.size = self.gradientView.frame.size gradientLayer.colors = [UIColor.black.withAlphaComponent(0.6).cgColor, UIColor.black.withAlphaComponent(0).cgColor] gradientLayer.startPoint = CGPoint(x: 0.0, y: 1.0) gradientLayer.endPoint = CGPoint(x: 0.0, y: 0.0) self.gradientView.layer.addSublayer(gradientLayer) - + infoPanelDisplayPoint = infoViewContainer.center infoPanelHiddenPoint = CGPoint(x: infoPanelDisplayPoint.x, y: -infoViewContainer.frame.height) infoViewContainer.center = infoPanelHiddenPoint infoViewContainer.layer.cornerRadius = 40 - + let blurEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .regular)) blurEffectView.frame = infoViewContainer.bounds blurEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] @@ -134,124 +128,122 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, blurEffectView.clipsToBounds = true infoViewContainer.addSubview(blurEffectView) infoViewContainer.sendSubviewToBack(blurEffectView) - + transportBarView.layer.cornerRadius = CGFloat(5) - + setupGestures() - + fetchVideo() - + setupNowPlayingCC() - + // Adjust subtitle size mediaPlayer.perform(Selector(("setTextRendererFontSize:")), with: 16) - + } - + func fetchVideo() { // Fetch max bitrate from UserDefaults depending on current connection mode let maxBitrate = Defaults[.inNetworkBandwidth] - + // Build a device profile let builder = DeviceProfileBuilder() builder.setMaxBitrate(bitrate: maxBitrate) let profile = builder.buildProfile() - + guard let currentUser = SessionManager.current.user else { return } - + let playbackInfo = PlaybackInfoDto(userId: currentUser.user_id ?? "", maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, deviceProfile: profile, autoOpenLiveStream: true) - + DispatchQueue.global(qos: .userInitiated).async { [self] in MediaInfoAPI.getPostedPlaybackInfo(itemId: manifest.id!, userId: currentUser.user_id ?? "", maxStreamingBitrate: Int(maxBitrate), startTimeTicks: manifest.userData?.playbackPositionTicks ?? 0, autoOpenLiveStream: true, playbackInfoDto: playbackInfo) .sink(receiveCompletion: { result in print(result) }, receiveValue: { [self] response in - + videoContentView.setNeedsLayout() videoContentView.setNeedsDisplay() - + playSessionId = response.playSessionId ?? "" - + guard let mediaSource = response.mediaSources?.first.self else { return } - + let item = PlaybackItem() - let streamURL : URL - + let streamURL: URL + // Item is being transcoded by request of server if let transcodiungUrl = mediaSource.transcodingUrl { item.videoType = .transcode streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(transcodiungUrl)")! } // Item will be directly played by the client - else - { + else { item.videoType = .directPlay streamURL = URL(string: "\(ServerEnvironment.current.server.baseURI!)/Videos/\(manifest.id!)/stream?Static=true&mediaSourceId=\(manifest.id!)&deviceId=\(SessionManager.current.deviceID)&api_key=\(SessionManager.current.accessToken)&Tag=\(mediaSource.eTag!)")! } - + item.videoUrl = streamURL - + let disableSubtitleTrack = Subtitle(name: "None", id: -1, url: nil, delivery: .embed, codec: "", languageCode: "") subtitleTrackArray.append(disableSubtitleTrack) - + // Loop through media streams and add to array for stream in mediaSource.mediaStreams! { - + if stream.type == .subtitle { - var deliveryUrl: URL? = nil - + var deliveryUrl: URL? + if stream.deliveryMethod == .external { deliveryUrl = URL(string: "\(ServerEnvironment.current.server.baseURI!)\(stream.deliveryUrl!)")! } - + let subtitle = Subtitle(name: stream.displayTitle ?? "Unknown", id: Int32(stream.index!), url: deliveryUrl, delivery: stream.deliveryMethod!, codec: stream.codec ?? "webvtt", languageCode: stream.language ?? "") - - if stream.isDefault == true{ + + if stream.isDefault == true { selectedCaptionTrack = Int32(stream.index!) } - + if subtitle.delivery != .encode { subtitleTrackArray.append(subtitle) } } - + if stream.type == .audio { let track = AudioTrack(name: stream.displayTitle!, languageCode: stream.language ?? "", id: Int32(stream.index!)) - + if stream.isDefault! == true { selectedAudioTrack = Int32(stream.index!) } - + audioTrackArray.append(track) } } - + // If no default audio tracks select the first one if selectedAudioTrack == -1 && !audioTrackArray.isEmpty { selectedAudioTrack = audioTrackArray.first!.id } - - + self.sendPlayReport() playbackItem = item - + mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) mediaPlayer.media.delegate = self mediaPlayer.play() - + // 1 second = 10,000,000 ticks - + if let rawStartTicks = manifest.userData?.playbackPositionTicks { mediaPlayer.jumpForward(Int32(rawStartTicks / 10_000_000)) } - + // Pause and load captions into memory. mediaPlayer.pause() - + var shouldHaveSubtitleTracks = 0 subtitleTrackArray.forEach { sub in if sub.id != -1 && sub.delivery == .external && sub.codec != "subrip" { @@ -259,25 +251,24 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, mediaPlayer.addPlaybackSlave(sub.url!, type: .subtitle, enforce: false) } } - + // Wait for captions to load while mediaPlayer.numberOfSubtitlesTracks != shouldHaveSubtitleTracks {} - + // Select default track & resume playback mediaPlayer.currentVideoSubTitleIndex = selectedCaptionTrack mediaPlayer.pause() mediaPlayer.play() playing = true - + setupInfoPanel() - + }) .store(in: &cancellables) - - + } } - + func setupNowPlayingCC() { let commandCenter = MPRemoteCommandCenter.shared() commandCenter.playCommand.isEnabled = true @@ -286,40 +277,40 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, commandCenter.seekBackwardCommand.isEnabled = true commandCenter.changePlaybackPositionCommand.isEnabled = true commandCenter.enableLanguageOptionCommand.isEnabled = true - + // Add handler for Pause Command commandCenter.pauseCommand.addTarget { _ in self.pause() return .success } - + // Add handler for Play command commandCenter.playCommand.addTarget { _ in self.play() return .success } - + // Add handler for FF command commandCenter.seekForwardCommand.addTarget { _ in self.mediaPlayer.jumpForward(30) self.sendProgressReport(eventName: "timeupdate") return .success } - + // Add handler for RW command commandCenter.seekBackwardCommand.addTarget { _ in self.mediaPlayer.jumpBackward(15) self.sendProgressReport(eventName: "timeupdate") return .success } - + // Scrubber commandCenter.changePlaybackPositionCommand.addTarget { [weak self](remoteEvent) -> MPRemoteCommandHandlerStatus in guard let self = self else {return .commandFailed} - + if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent { let targetSeconds = event.positionTime - + let videoPosition = Double(self.mediaPlayer.time.intValue) let offset = targetSeconds - videoPosition if offset > 0 { @@ -328,56 +319,55 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000) } self.sendProgressReport(eventName: "unpause") - + return .success } else { return .commandFailed } } - + // commandCenter.enableLanguageOptionCommand.addTarget { [weak self](remoteEvent) in // guard let self = self else {return .commandFailed} // // // // } - + var runTicks = 0 var playbackTicks = 0 - + if let ticks = manifest.runTimeTicks { runTicks = Int(ticks / 10_000_000) } - + if let ticks = manifest.userData?.playbackPositionTicks { playbackTicks = Int(ticks / 10_000_000) } - + var nowPlayingInfo = [String: Any]() - + nowPlayingInfo[MPMediaItemPropertyTitle] = manifest.name ?? "Jellyfin Video" nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = AVMediaType.video nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = runTicks nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = playbackTicks - + if let imageData = NSData(contentsOf: manifest.getPrimaryImage(maxWidth: 200)) { if let artworkImage = UIImage(data: imageData as Data) { - let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (size) -> UIImage in + let artwork = MPMediaItemArtwork.init(boundsSize: artworkImage.size, requestHandler: { (_) -> UIImage in return artworkImage }) nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork } } - + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo - UIApplication.shared.beginReceivingRemoteControlEvents() } - - func updateNowPlayingCenter(time : Double?, playing : Bool?) { - + + func updateNowPlayingCenter(time: Double?, playing: Bool?) { + var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]() if let playing = playing { @@ -386,64 +376,60 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, if let time = time { nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = time } - + MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo } - - - + // Grabs a refference to the info panel view controller override func prepare(for segue: UIStoryboardSegue, sender: Any?) { if segue.identifier == "infoView" { containerViewController = segue.destination as? InfoTabBarViewController containerViewController?.videoPlayer = self - + } } - + // MARK: Player functions // Animate the scrubber when playing state changes func animateScrubber() { - let y : CGFloat = playing ? 0 : -20 + let y: CGFloat = playing ? 0 : -20 let height: CGFloat = playing ? 10 : 30 - + UIView.animate(withDuration: 0.2, delay: 0, options: .curveEaseIn, animations: { self.scrubberView.frame = CGRect(x: self.scrubberView.frame.minX, y: y, width: 2, height: height) }) } - - + func pause() { playing = false mediaPlayer.pause() - + self.sendProgressReport(eventName: "pause") - + self.updateNowPlayingCenter(time: nil, playing: false) - + animateScrubber() - - self.scrubLabel.frame = CGRect(x: self.scrubberView.frame.minX - self.scrubLabel.frame.width/2, y:self.scrubLabel.frame.minY, width: self.scrubLabel.frame.width, height: self.scrubLabel.frame.height) + + self.scrubLabel.frame = CGRect(x: self.scrubberView.frame.minX - self.scrubLabel.frame.width/2, y: self.scrubLabel.frame.minY, width: self.scrubLabel.frame.width, height: self.scrubLabel.frame.height) } - + func play () { playing = true mediaPlayer.play() - + self.updateNowPlayingCenter(time: nil, playing: true) self.sendProgressReport(eventName: "unpause") - + animateScrubber() } - - + func toggleInfoContainer() { showingInfoPanel.toggle() - + containerViewController?.view.isUserInteractionEnabled = showingInfoPanel - + if showingInfoPanel && seeking { scrubLabel.isHidden = true UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: { @@ -453,49 +439,48 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, self.scrubLabel.text = self.currentTimeLabel.text } seeking = false - + } - - UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut) { [self] in + + UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut) { [self] in infoViewContainer.center = showingInfoPanel ? infoPanelDisplayPoint : infoPanelHiddenPoint } - + } - + // MARK: Gestures func setupGestures() { - + let playPauseGesture = UITapGestureRecognizer(target: self, action: #selector(self.selectButtonTapped)) let playPauseType = UIPress.PressType.playPause - playPauseGesture.allowedPressTypes = [NSNumber(value: playPauseType.rawValue)]; + playPauseGesture.allowedPressTypes = [NSNumber(value: playPauseType.rawValue)] view.addGestureRecognizer(playPauseGesture) - + let selectGesture = UITapGestureRecognizer(target: self, action: #selector(self.selectButtonTapped)) let selectType = UIPress.PressType.select - selectGesture.allowedPressTypes = [NSNumber(value: selectType.rawValue)]; + selectGesture.allowedPressTypes = [NSNumber(value: selectType.rawValue)] view.addGestureRecognizer(selectGesture) - + let backTapGesture = UITapGestureRecognizer(target: self, action: #selector(self.backButtonPressed(tap:))) let backPress = UIPress.PressType.menu - backTapGesture.allowedPressTypes = [NSNumber(value: backPress.rawValue)]; + backTapGesture.allowedPressTypes = [NSNumber(value: backPress.rawValue)] view.addGestureRecognizer(backTapGesture) - + let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.userPanned(panGestureRecognizer:))) view.addGestureRecognizer(panGestureRecognizer) - - + let swipeRecognizer = UISwipeGestureRecognizer(target: self, action: #selector(self.swipe(swipe:))) swipeRecognizer.direction = .right view.addGestureRecognizer(swipeRecognizer) - + let swipeRecognizerl = UISwipeGestureRecognizer(target: self, action: #selector(self.swipe(swipe:))) swipeRecognizerl.direction = .left view.addGestureRecognizer(swipeRecognizerl) - + } - - @objc func backButtonPressed(tap : UITapGestureRecognizer) { - + + @objc func backButtonPressed(tap: UITapGestureRecognizer) { + // Dismiss info panel if showingInfoPanel { if focusedOnTabBar { @@ -503,75 +488,72 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } return } - + // Cancel seek and move back to initial position - if(seeking) { + if seeking { scrubLabel.isHidden = true UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: { self.scrubberView.frame = CGRect(x: self.initialSeekPos, y: 0, width: 2, height: 10) }) play() seeking = false - } - else - { + } else { // Dismiss view mediaPlayer.stop() sendStopReport() self.navigationController?.popViewController(animated: true) } } - - @objc func userPanned(panGestureRecognizer : UIPanGestureRecognizer) { + + @objc func userPanned(panGestureRecognizer: UIPanGestureRecognizer) { if loading { return } - + let translation = panGestureRecognizer.translation(in: view) let velocity = panGestureRecognizer.velocity(in: view) - + // Swiped up - Handle dismissing info panel if translation.y < -700 && (focusedOnTabBar && showingInfoPanel) { toggleInfoContainer() return } - + if showingInfoPanel { return } - + // Swiped down - Show the info panel if translation.y > 700 { toggleInfoContainer() return } - + // Ignore seek if video is playing if playing { return } - + // Save current position if seek is cancelled and show the scrubLabel - if(!seeking) { + if !seeking { initialSeekPos = self.scrubberView.frame.minX seeking = true self.scrubLabel.isHidden = false } - + let newPos = (self.scrubberView.frame.minX + velocity.x/100).clamped(to: 0...transportBarView.frame.width) - + UIView.animate(withDuration: 0.8, delay: 0, options: .curveEaseOut, animations: { let time = (Double(self.scrubberView.frame.minX) * self.videoDuration) / Double(self.transportBarView.frame.width) - + self.scrubberView.frame = CGRect(x: newPos, y: self.scrubberView.frame.minY, width: 2, height: 30) self.scrubLabel.frame = CGRect(x: (newPos - self.scrubLabel.frame.width/2), y: self.scrubLabel.frame.minY, width: self.scrubLabel.frame.width, height: self.scrubLabel.frame.height) self.scrubLabel.text = (self.formatSecondsToHMS(time)) - + }) - - + } - + // Not currently used @objc func swipe(swipe: UISwipeGestureRecognizer!) { print("swiped") @@ -593,53 +575,51 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, default: break } - + } - + /// Play/Pause or Select is pressed on the AppleTV remote @objc func selectButtonTapped() { if loading { return } - + showingControls = true controlsView.isHidden = false controlsAppearTime = CACurrentMediaTime() - - + // Move to seeked position - if(seeking) { + if seeking { scrubLabel.isHidden = true - + // Move current time to the scrubbed position UIView.animate(withDuration: 0.4, delay: 0, options: .curveEaseOut, animations: { [self] in - + self.currentTimeLabel.frame = CGRect(x: CGFloat(scrubLabel.frame.minX + transportBarView.frame.minX), y: currentTimeLabel.frame.minY, width: currentTimeLabel.frame.width, height: currentTimeLabel.frame.height) - + }) - + let time = (Double(self.scrubberView.frame.minX) * self.videoDuration) / Double(self.transportBarView.frame.width) - + self.currentTimeLabel.text = self.scrubLabel.text self.remainingTimeLabel.text = "-" + formatSecondsToHMS(videoDuration - time) - + mediaPlayer.position = Float(self.scrubberView.frame.minX) / Float(self.transportBarView.frame.width) - + play() - + seeking = false return } - + playing ? pause() : play() } - - + // MARK: Jellyfin Playstate updates func sendProgressReport(eventName: String) { if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: (!playing), isMuted: false, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - + PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) .sink(receiveCompletion: { result in print(result) @@ -649,10 +629,10 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, .store(in: &cancellables) } } - + func sendStopReport() { let stopInfo = PlaybackStopInfo(item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, positionTicks: Int64(mediaPlayer.position * Float(manifest.runTimeTicks!)), liveStreamId: nil, playSessionId: playSessionId, failed: nil, nextMediaType: nil, playlistItemId: "playlistItem0", nowPlayingQueue: []) - + PlaystateAPI.reportPlaybackStopped(playbackStopInfo: stopInfo) .sink(receiveCompletion: { result in print(result) @@ -661,14 +641,14 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, }) .store(in: &cancellables) } - + func sendPlayReport() { startTime = Int(Date().timeIntervalSince1970) * 10000000 - + print("sending play report!") - + let startInfo = PlaybackStartInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: false, isMuted: false, positionTicks: manifest.userData?.playbackPositionTicks, playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") - + PlaystateAPI.reportPlaybackStart(playbackStartInfo: startInfo) .sink(receiveCompletion: { result in print(result) @@ -677,10 +657,9 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, }) .store(in: &cancellables) } - - + // MARK: VLC Delegate - + func mediaPlayerStateChanged(_ aNotification: Notification!) { let currentState: VLCMediaPlayerState = mediaPlayer.state switch currentState { @@ -695,19 +674,19 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, break case .stopped: print("stopped") - + break case .ended: print("ended") - + break case .opening: print("opening") - + break case .paused: print("paused") - + break case .playing: print("Video is playing") @@ -728,14 +707,14 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, default: print("default") break - + } - + } - + // Move time along transport bar func mediaPlayerTimeChanged(_ aNotification: Notification!) { - + if loading { loading = false DispatchQueue.main.async { [self] in @@ -744,20 +723,20 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, } updateNowPlayingCenter(time: nil, playing: true) } - + let time = mediaPlayer.position if time != lastTime { self.currentTimeLabel.text = formatSecondsToHMS(Double(mediaPlayer.time.intValue/1000)) self.remainingTimeLabel.text = "-" + formatSecondsToHMS(Double(abs(mediaPlayer.remainingTime.intValue/1000))) - + self.videoPos = Double(mediaPlayer.position) - + let newPos = videoPos * Double(self.transportBarView.frame.width) if !newPos.isNaN && self.playing { self.scrubberView.frame = CGRect(x: newPos, y: 0, width: 2, height: 10) self.currentTimeLabel.frame = CGRect(x: CGFloat(newPos) + transportBarView.frame.minX - currentTimeLabel.frame.width/2, y: currentTimeLabel.frame.minY, width: currentTimeLabel.frame.width, height: currentTimeLabel.frame.height) } - + if showingControls { if CACurrentMediaTime() - controlsAppearTime > 5 { showingControls = false @@ -770,34 +749,32 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, controlsAppearTime = 999_999_999_999_999 } } - + } - + lastTime = time - + if CACurrentMediaTime() - lastProgressReportTime > 5 { sendProgressReport(eventName: "timeupdate") lastProgressReportTime = CACurrentMediaTime() } } - - + // MARK: Settings Delegate func selectNew(audioTrack id: Int32) { selectedAudioTrack = id mediaPlayer.currentAudioTrackIndex = id } - + func selectNew(subtitleTrack id: Int32) { selectedCaptionTrack = id mediaPlayer.currentVideoSubTitleIndex = id } - + func setupInfoPanel() { containerViewController?.setupInfoViews(mediaItem: manifest, subtitleTracks: subtitleTrackArray, selectedSubtitleTrack: selectedCaptionTrack, audioTracks: audioTrackArray, selectedAudioTrack: selectedAudioTrack, delegate: self) } - - + func formatSecondsToHMS(_ seconds: Double) -> String { let timeHMSFormatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() @@ -808,16 +785,16 @@ class VideoPlayerViewController: UIViewController, VideoPlayerSettingsDelegate, formatter.zeroFormattingBehavior = .pad return formatter }() - + guard !seconds.isNaN, let text = timeHMSFormatter.string(from: seconds) else { return "00:00" } - + return text.hasPrefix("0") && text.count > 4 ? .init(text.dropFirst()) : text } - + // When VLC video starts playing a real device can no longer receive gesture recognisers, adding this in hopes to fix the issue but no luck func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { print("recognisesimultaneousvideoplayer") diff --git a/JellyfinPlayer/ConnectToServerView.swift b/JellyfinPlayer/ConnectToServerView.swift index a5ad623d..d3d651bd 100644 --- a/JellyfinPlayer/ConnectToServerView.swift +++ b/JellyfinPlayer/ConnectToServerView.swift @@ -122,7 +122,7 @@ struct ConnectToServerView: View { } .disabled(viewModel.isLoading || uri.isEmpty) } - + Section(header: Text("Discovered Servers")) { if self.viewModel.searching { ProgressView() @@ -142,7 +142,7 @@ struct ConnectToServerView: View { ProgressView() } } - + }) } } diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index d5bb2129..67396556 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -12,27 +12,27 @@ import JellyfinAPI struct ProgressBar: Shape { func path(in rect: CGRect) -> Path { var path = Path() - + let tl = CGPoint(x: rect.minX, y: rect.minY) let tr = CGPoint(x: rect.maxX, y: rect.minY) let br = CGPoint(x: rect.maxX, y: rect.maxY) let bls = CGPoint(x: rect.minX + 10, y: rect.maxY) let blc = CGPoint(x: rect.minX + 10, y: rect.maxY - 10) - + path.move(to: tl) path.addLine(to: tr) path.addLine(to: br) path.addLine(to: bls) path.addRelativeArc(center: blc, radius: 10, startAngle: Angle.degrees(90), delta: Angle.degrees(90)) - + return path } } struct ContinueWatchingView: View { var items: [BaseItemDto] - + var body: some View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { @@ -56,7 +56,7 @@ struct ContinueWatchingView: View { .fontWeight(.semibold) .foregroundColor(.primary) .lineLimit(1) - if(item.type == "Episode") { + if item.type == "Episode" { Text("• S\(String(item.parentIndexNumber ?? 0)):E\(String(item.indexNumber ?? 0)) - \(item.name ?? "")") .font(.callout) .fontWeight(.semibold) diff --git a/JellyfinPlayer/HomeView.swift b/JellyfinPlayer/HomeView.swift index e7f5b220..7c3767e9 100644 --- a/JellyfinPlayer/HomeView.swift +++ b/JellyfinPlayer/HomeView.swift @@ -13,10 +13,10 @@ import SwiftUI struct HomeView: View { @StateObject var viewModel = HomeViewModel() @State var showingSettings = false - + @ViewBuilder var innerBody: some View { - if(viewModel.isLoading) { + if viewModel.isLoading { ProgressView() } else { ScrollView { @@ -53,7 +53,7 @@ struct HomeView: View { } } } - + var body: some View { innerBody .navigationTitle(MainTabView.Tab.home.localized) diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index 8d9e8402..2595bf18 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -9,7 +9,7 @@ import SwiftUI struct LatestMediaView: View { @StateObject var viewModel: LatestMediaViewModel - + var body: some View { ScrollView(.horizontal, showsIndicators: false) { LazyHStack { @@ -23,15 +23,14 @@ struct LatestMediaView: View { .shadow(radius: 4) .overlay( ZStack { - if(item.userData!.played ?? false) { + if item.userData!.played ?? false { Image(systemName: "circle.fill") .foregroundColor(.white) Image(systemName: "checkmark.circle.fill") .foregroundColor(Color(.systemBlue)) } }.padding(2) - .opacity(1) - , alignment: .topTrailing).opacity(1) + .opacity(1), alignment: .topTrailing).opacity(1) Text(item.seriesName ?? item.name ?? "") .font(.caption) .fontWeight(.semibold) diff --git a/JellyfinPlayer/LibraryListView.swift b/JellyfinPlayer/LibraryListView.swift index 5a1faef9..9b7c450b 100644 --- a/JellyfinPlayer/LibraryListView.swift +++ b/JellyfinPlayer/LibraryListView.swift @@ -13,12 +13,12 @@ struct LibraryListView: View { var body: some View { ScrollView { - LazyVStack() { + LazyVStack { NavigationLink(destination: LazyView { LibraryView(viewModel: .init(filters: viewModel.withFavorites), title: "Favorites") }) { - ZStack() { - HStack() { + ZStack { + HStack { Spacer() Text("Your Favorites") .foregroundColor(.black) @@ -34,12 +34,12 @@ struct LibraryListView: View { .cornerRadius(10) .shadow(radius: 5) .padding(.bottom, 5) - + NavigationLink(destination: LazyView { Text("WIP") }) { - ZStack() { - HStack() { + ZStack { + HStack { Spacer() Text("All Genres") .foregroundColor(.black) @@ -55,16 +55,16 @@ struct LibraryListView: View { .cornerRadius(10) .shadow(radius: 5) .padding(.bottom, 15) - + ForEach(viewModel.libraries, id: \.id) { library in - if(library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows") { + if library.collectionType ?? "" == "movies" || library.collectionType ?? "" == "tvshows" { NavigationLink(destination: LazyView { LibraryView(viewModel: .init(parentID: library.id), title: library.name ?? "") }) { - ZStack() { + ZStack { ImageView(src: library.getPrimaryImage(maxWidth: 500), bh: library.getPrimaryImageBlurHash()) .opacity(0.4) - HStack() { + HStack { Spacer() Text(library.name ?? "") .foregroundColor(.white) diff --git a/JellyfinPlayer/LibrarySearchView.swift b/JellyfinPlayer/LibrarySearchView.swift index 5a514538..59b12808 100644 --- a/JellyfinPlayer/LibrarySearchView.swift +++ b/JellyfinPlayer/LibrarySearchView.swift @@ -24,7 +24,7 @@ struct LibrarySearchView: View { Spacer().frame(height: 6) SearchBar(text: $searchQuery) ZStack { - if(!viewModel.isLoading) { + if !viewModel.isLoading { ScrollView(.vertical) { if !viewModel.items.isEmpty { Spacer().frame(height: 16) @@ -37,15 +37,14 @@ struct LibrarySearchView: View { .cornerRadius(10) .overlay( ZStack { - if(item.userData!.played ?? false) { + if item.userData!.played ?? false { Image(systemName: "circle.fill") .foregroundColor(.white) Image(systemName: "checkmark.circle.fill") .foregroundColor(Color(.systemBlue)) } }.padding(2) - .opacity(1) - , alignment: .topTrailing).opacity(1) + .opacity(1), alignment: .topTrailing).opacity(1) Text(item.name ?? "") .font(.caption) .fontWeight(.semibold) diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index bd0b5bea..576b27a9 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -41,15 +41,14 @@ struct LibraryView: View { .cornerRadius(10) .overlay( ZStack { - if(item.userData!.played ?? false) { + if item.userData!.played ?? false { Image(systemName: "circle.fill") .foregroundColor(.white) Image(systemName: "checkmark.circle.fill") .foregroundColor(Color(.systemBlue)) } }.padding(2) - .opacity(1) - , alignment: .topTrailing).opacity(1) + .opacity(1), alignment: .topTrailing).opacity(1) Text(item.name ?? "") .font(.caption) .fontWeight(.semibold) diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index 411a1a37..7fbd811d 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -10,9 +10,9 @@ import Combine import JellyfinAPI struct NextUpView: View { - + var items: [BaseItemDto] - + var body: some View { VStack(alignment: .leading) { Text("Next Up") diff --git a/JellyfinPlayer/SeasonItemView.swift b/JellyfinPlayer/SeasonItemView.swift index 2939fd3f..451756fc 100644 --- a/JellyfinPlayer/SeasonItemView.swift +++ b/JellyfinPlayer/SeasonItemView.swift @@ -78,15 +78,14 @@ struct SeasonItemView: View { ) .overlay( ZStack { - if(episode.userData!.played ?? false) { + if episode.userData!.played ?? false { Image(systemName: "circle.fill") .foregroundColor(.white) Image(systemName: "checkmark.circle.fill") .foregroundColor(Color(.systemBlue)) } }.padding(2) - .opacity(1) - , alignment: .topTrailing).opacity(1) + .opacity(1), alignment: .topTrailing).opacity(1) VStack(alignment: .leading) { HStack { Text("S\(String(episode.parentIndexNumber ?? 0)):E\(String(episode.indexNumber ?? 0))").font(.subheadline) diff --git a/JellyfinPlayer/SettingsView.swift b/JellyfinPlayer/SettingsView.swift index 9c15655f..b3cab36d 100644 --- a/JellyfinPlayer/SettingsView.swift +++ b/JellyfinPlayer/SettingsView.swift @@ -11,9 +11,9 @@ import Defaults struct SettingsView: View { @Environment(\.managedObjectContext) private var viewContext - + @ObservedObject var viewModel: SettingsViewModel - + @Binding var close: Bool @Default(.inNetworkBandwidth) var inNetworkStreamBitrate @Default(.outOfNetworkBandwidth) var outOfNetworkStreamBitrate @@ -21,11 +21,11 @@ struct SettingsView: View { @Default(.autoSelectSubtitlesLangCode) var autoSelectSubtitlesLangcode @Default(.autoSelectAudioLangCode) var autoSelectAudioLangcode @State private var username: String = "" - + func onAppear() { username = SessionManager.current.user.username ?? "" } - + var body: some View { NavigationView { Form { @@ -35,20 +35,20 @@ struct SettingsView: View { Text(bitrate.name).tag(bitrate.value) } } - + Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in Text(bitrate.name).tag(bitrate.value) } } } - + Section(header: Text("Accessibility")) { Toggle("Automatically show subtitles", isOn: $isAutoSelectSubtitles) SearchablePicker(label: "Preferred subtitle language", options: viewModel.langs, optionToString: { $0.name }, - selected:Binding( + selected: Binding( get: { viewModel.langs.first(where: { $0.isoCode == autoSelectSubtitlesLangcode }) ?? .auto }, set: {autoSelectSubtitlesLangcode = $0.isoCode} ) @@ -62,7 +62,7 @@ struct SettingsView: View { ) ) } - + Section { HStack { Text("Signed in as \(username)").foregroundColor(.primary) @@ -70,8 +70,7 @@ struct SettingsView: View { Button { let nc = NotificationCenter.default nc.post(name: Notification.Name("didSignOut"), object: nil) - - + SessionManager.current.logout() } label: { Text("Log out").font(.callout) diff --git a/JellyfinPlayer/VideoPlayer.swift b/JellyfinPlayer/VideoPlayer.swift index 198dd4cd..3f52b644 100644 --- a/JellyfinPlayer/VideoPlayer.swift +++ b/JellyfinPlayer/VideoPlayer.swift @@ -53,10 +53,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe var startTime: Int = 0 var controlsAppearTime: Double = 0 var isSeeking: Bool = false - - var playerDestination: PlayerDestination = .local; - var discoveredCastDevices: [GCKDevice] = []; - var selectedCastDevice: GCKDevice?; + + var playerDestination: PlayerDestination = .local + var discoveredCastDevices: [GCKDevice] = [] + var selectedCastDevice: GCKDevice? var jellyfinCastChannel: GCKGenericChannel? var remotePositionTicks: Int = 0 private var castDiscoveryManager: GCKDiscoveryManager { @@ -65,7 +65,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe private var castSessionManager: GCKSessionManager { return GCKCastContext.sharedInstance().sessionManager } - var hasSentRemoteSeek: Bool = false; + var hasSentRemoteSeek: Bool = false var selectedAudioTrack: Int32 = -1 var selectedCaptionTrack: Int32 = -1 @@ -77,11 +77,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe var manifest: BaseItemDto = BaseItemDto() var playbackItem = PlaybackItem() var remoteTimeUpdateTimer: Timer? - // MARK: IBActions @IBAction func seekSliderStart(_ sender: Any) { - if(playerDestination == .local) { + if playerDestination == .local { sendProgressReport(eventName: "pause") mediaPlayer.pause() } else { @@ -111,8 +110,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Scrub is value from 0..1 - find position in video and add / or remove. let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) let offset = secondsScrubbedTo - videoPosition - - if(playerDestination == .local) { + + if playerDestination == .local { if offset > 0 { mediaPlayer.jumpForward(Int32(offset)) } else { @@ -130,22 +129,22 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBAction func exitButtonPressed(_ sender: Any) { sendStopReport() mediaPlayer.stop() - - if(castSessionManager.hasConnectedCastSession()) { + + if castSessionManager.hasConnectedCastSession() { castSessionManager.endSessionAndStopCasting(true) } - + delegate?.exitPlayer(self) } @IBAction func controlViewTapped(_ sender: Any) { - if(playerDestination == .local) { + if playerDestination == .local { videoControlsView.isHidden = true } } @IBAction func contentViewTapped(_ sender: Any) { - if(playerDestination == .local) { + if playerDestination == .local { videoControlsView.isHidden = false controlsAppearTime = CACurrentMediaTime() } @@ -153,7 +152,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBAction func jumpBackTapped(_ sender: Any) { if paused == false { - if(playerDestination == .local) { + if playerDestination == .local { mediaPlayer.jumpBackward(15) } else { self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000)-15]) @@ -163,7 +162,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBAction func jumpForwardTapped(_ sender: Any) { if paused == false { - if(playerDestination == .local) { + if playerDestination == .local { mediaPlayer.jumpForward(30) } else { self.sendJellyfinCommand(command: "Seek", options: ["position": (remotePositionTicks/10_000_000)+30]) @@ -174,7 +173,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe @IBOutlet weak var mainActionButton: UIButton! @IBAction func mainActionButtonPressed(_ sender: Any) { if paused { - if(playerDestination == .local) { + if playerDestination == .local { mediaPlayer.play() mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) paused = false @@ -184,7 +183,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe paused = false } } else { - if(playerDestination == .local) { + if playerDestination == .local { mediaPlayer.pause() mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) paused = true @@ -210,10 +209,10 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) } } - - //MARK: Cast methods + + // MARK: Cast methods @IBAction func castButtonPressed(_ sender: Any) { - if(selectedCastDevice == nil) { + if selectedCastDevice == nil { castDeviceVC = VideoPlayerCastDeviceSelectorView() castDeviceVC?.delegate = self @@ -228,33 +227,33 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } else { castSessionManager.endSessionAndStopCasting(true) - selectedCastDevice = nil; + selectedCastDevice = nil self.castButton.isEnabled = true self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) playerDestination = .local } } - + func castPopoverDismissed() { castDeviceVC?.dismiss(animated: true, completion: nil) - if(playerDestination == .local) { + if playerDestination == .local { self.mediaPlayer.play() } self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } - + func castDeviceChanged() { - if(selectedCastDevice != nil) { + if selectedCastDevice != nil { playerDestination = .remote castSessionManager.add(self) castSessionManager.startSession(with: selectedCastDevice!) } } - - //MARK: Cast End + + // MARK: Cast End func settingsPopoverDismissed() { optionsVC?.dismiss(animated: true, completion: nil) - if(playerDestination == .local) { + if playerDestination == .local { self.mediaPlayer.play() self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) } @@ -270,7 +269,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Add handler for Pause Command commandCenter.pauseCommand.addTarget { _ in - if(self.playerDestination == .local) { + if self.playerDestination == .local { self.mediaPlayer.pause() self.sendProgressReport(eventName: "pause") } else { @@ -282,7 +281,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Add handler for Play command commandCenter.playCommand.addTarget { _ in - if(self.playerDestination == .local) { + if self.playerDestination == .local { self.mediaPlayer.play() self.sendProgressReport(eventName: "unpause") } else { @@ -294,7 +293,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Add handler for FF command commandCenter.seekForwardCommand.addTarget { _ in - if(self.playerDestination == .local) { + if self.playerDestination == .local { self.mediaPlayer.jumpForward(30) self.sendProgressReport(eventName: "timeupdate") } else { @@ -305,7 +304,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe // Add handler for RW command commandCenter.seekBackwardCommand.addTarget { _ in - if(self.playerDestination == .local) { + if self.playerDestination == .local { self.mediaPlayer.jumpBackward(15) self.sendProgressReport(eventName: "timeupdate") } else { @@ -320,11 +319,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent { let targetSeconds = event.positionTime - + let videoPosition = Double(self.mediaPlayer.time.intValue) let offset = targetSeconds - videoPosition - - if(self.playerDestination == .local) { + + if self.playerDestination == .local { if offset > 0 { self.mediaPlayer.jumpForward(Int32(offset)/1000) } else { @@ -332,7 +331,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } self.sendProgressReport(eventName: "unpause") } else { - + } return .success @@ -356,8 +355,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } else { titleLabel.text = "S\(String(manifest.parentIndexNumber ?? 0)):E\(String(manifest.indexNumber ?? 0)) “\(manifest.name ?? "")”" } - - if(!UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat) { + + if !UIDevice.current.orientation.isLandscape || UIDevice.current.orientation.isFlat { let value = UIInterfaceOrientation.landscapeRight.rawValue UIDevice.current.setValue(value, forKey: "orientation") UIViewController.attemptRotationToDeviceOrientation() @@ -366,7 +365,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } func mediaHasStartedPlaying() { - castButton.isHidden = true; + castButton.isHidden = true let discoveryCriteria = GCKDiscoveryCriteria(applicationID: "F007D354") let gckCastOptions = GCKCastOptions(discoveryCriteria: discoveryCriteria) GCKCastContext.setSharedInstanceWith(gckCastOptions) @@ -374,11 +373,11 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castDiscoveryManager.add(self) castDiscoveryManager.startDiscovery() } - + func didUpdateDeviceList() { - let totalDevices = castDiscoveryManager.deviceCount; + let totalDevices = castDiscoveryManager.deviceCount discoveredCastDevices = [] - if(totalDevices > 0) { + if totalDevices > 0 { for i in 0...totalDevices-1 { let device = castDiscoveryManager.device(at: i) discoveredCastDevices.append(device) @@ -395,15 +394,15 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe castButton.setImage(nil, for: .normal) } } - + override func viewWillDisappear(_ animated: Bool) { self.tabBarController?.tabBar.isHidden = false self.navigationController?.isNavigationBarHidden = false overrideUserInterfaceStyle = .unspecified super.viewWillDisappear(animated) } - - //MARK: viewDidAppear + + // MARK: viewDidAppear override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) overrideUserInterfaceStyle = .dark @@ -532,7 +531,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe selectedAudioTrack = audioTrackArray[0].id } } - + self.sendPlayReport() playbackItem = item } @@ -542,7 +541,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe .store(in: &cancellables) } } - + func setupTracksForPreferredDefaults() { subtitleTrackArray.forEach { subtitle in if Defaults[.isAutoSelectSubtitles] { @@ -556,7 +555,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + audioTrackArray.forEach { audio in if audio.languageCode.contains(Defaults[.autoSelectAudioLangCode]) { selectedAudioTrack = audio.id @@ -564,7 +563,7 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + func startLocalPlaybackEngine(_ fetchCaptions: Bool) { print("Local playback engine starting.") mediaPlayer.media = VLCMedia(url: playbackItem.videoUrl) @@ -572,17 +571,17 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe sendPlayReport() // 1 second = 10,000,000 ticks - var startTicks: Int64 = 0; - if(remotePositionTicks == 0) { + var startTicks: Int64 = 0 + if remotePositionTicks == 0 { print("Using server-reported start time") startTicks = manifest.userData?.playbackPositionTicks ?? 0 } else { print("Using remote-reported start time") startTicks = Int64(remotePositionTicks) } - + if startTicks != 0 { - let videoPosition = Double(mediaPlayer.time.intValue / 1000); + let videoPosition = Double(mediaPlayer.time.intValue / 1000) let secondsScrubbedTo = startTicks / 10_000_000 let offset = secondsScrubbedTo - Int64(videoPosition) print("Seeking to position: \(secondsScrubbedTo)") @@ -592,8 +591,8 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe mediaPlayer.jumpBackward(Int32(abs(offset))) } } - - if(fetchCaptions) { + + if fetchCaptions { print("Fetching captions.") // Pause and load captions into memory. mediaPlayer.pause() @@ -603,21 +602,21 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } } - + self.mediaHasStartedPlaying() delegate?.hideLoadingView(self) - + videoContentView.setNeedsLayout() videoContentView.setNeedsDisplay() self.view.setNeedsLayout() self.view.setNeedsDisplay() self.videoControlsView.setNeedsLayout() self.videoControlsView.setNeedsDisplay() - + mediaPlayer.pause() mediaPlayer.play() setupTracksForPreferredDefaults() - + print("Local engine started.") } @@ -633,15 +632,15 @@ class PlayerViewController: UIViewController, GCKDiscoveryManagerListener, GCKRe } } -//MARK: - GCKGenericChannelDelegate +// MARK: - GCKGenericChannelDelegate extension PlayerViewController: GCKGenericChannelDelegate { @objc func updateRemoteTime() { castButton.setImage(UIImage(named: "CastConnected"), for: .normal) - if(!paused) { - remotePositionTicks = remotePositionTicks + 2_000_000; //add 0.2 secs every timer evt. + if !paused { + remotePositionTicks = remotePositionTicks + 2_000_000; // add 0.2 secs every timer evt. } - - if(isSeeking == false) { + + if isSeeking == false { let remainingTime = (manifest.runTimeTicks! - Int64(remotePositionTicks))/10_000_000 let hours = remainingTime / 3600 let minutes = (remainingTime % 3600) / 60 @@ -653,36 +652,36 @@ extension PlayerViewController: GCKGenericChannelDelegate { timeTextStr = "\(String(Int((minutes))).leftPad(toWidth: 2, withString: "0")):\(String(Int((seconds))).leftPad(toWidth: 2, withString: "0"))" } timeText.text = timeTextStr - + let playbackProgress = Float(remotePositionTicks) / Float(manifest.runTimeTicks!) seekSlider.setValue(playbackProgress, animated: true) } } - + func cast(_ channel: GCKGenericChannel, didReceiveTextMessage message: String, withNamespace protocolNamespace: String) { if let data = message.data(using: .utf8) { if let json = try? JSON(data: data) { let messageType = json["type"].string ?? "" - if(messageType == "playbackprogress") { + if messageType == "playbackprogress" { dump(json) - if(remotePositionTicks > 100) { - if(hasSentRemoteSeek == false) { - hasSentRemoteSeek = true; + if remotePositionTicks > 100 { + if hasSentRemoteSeek == false { + hasSentRemoteSeek = true sendJellyfinCommand(command: "Seek", options: [ "position": Int(Float(manifest.runTimeTicks! / 10_000_000) * mediaPlayer.position) ]) } } paused = json["data"]["PlayState"]["IsPaused"].boolValue - self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0; - if(remoteTimeUpdateTimer == nil) { + self.remotePositionTicks = json["data"]["PlayState"]["PositionTicks"].int ?? 0 + if remoteTimeUpdateTimer == nil { remoteTimeUpdateTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(updateRemoteTime), userInfo: nil, repeats: true) } } } } } - + func sendJellyfinCommand(command: String, options: [String: Any]) { let payload: [String: Any] = [ "options": options, @@ -698,12 +697,12 @@ extension PlayerViewController: GCKGenericChannelDelegate { ] print(payload) let jsonData = JSON(payload) - + jellyfinCastChannel?.sendTextMessage(jsonData.rawString()!, error: nil) - - if(command == "Seek") { + + if command == "Seek" { remotePositionTicks = remotePositionTicks + ((options["position"] as! Int) * 10_000_000) - //Send playback report as Jellyfin Chromecast isn't smarter than a rock. + // Send playback report as Jellyfin Chromecast isn't smarter than a rock. let progressInfo = PlaybackProgressInfo(canSeek: true, item: manifest, itemId: manifest.id, sessionId: playSessionId, mediaSourceId: manifest.id, audioStreamIndex: Int(selectedAudioTrack), subtitleStreamIndex: Int(selectedCaptionTrack), isPaused: paused, isMuted: false, positionTicks: Int64(remotePositionTicks), playbackStartTimeTicks: Int64(startTime), volumeLevel: 100, brightness: 100, aspectRatio: nil, playMethod: playbackItem.videoType, liveStreamId: nil, playSessionId: playSessionId, repeatMode: .repeatNone, nowPlayingQueue: [], playlistItemId: "playlistItem0") PlaystateAPI.reportPlaybackProgress(playbackProgressInfo: progressInfo) @@ -717,25 +716,25 @@ extension PlayerViewController: GCKGenericChannelDelegate { } } -//MARK: - GCKSessionManagerListener +// MARK: - GCKSessionManagerListener extension PlayerViewController: GCKSessionManagerListener { func sessionDidStart(manager: GCKSessionManager, didStart session: GCKCastSession) { self.sendStopReport() mediaPlayer.stop() - + playerDestination = .remote - videoContentView.isHidden = true; - videoControlsView.isHidden = false; + videoContentView.isHidden = true + videoControlsView.isHidden = false castButton.setImage(UIImage(named: "CastConnected"), for: .normal) manager.currentCastSession?.start() - + jellyfinCastChannel!.delegate = self session.add(jellyfinCastChannel!) - + if let client = session.remoteMediaClient { client.add(self) } - + let playNowOptions: [String: Any] = [ "items": [[ "Id": self.manifest.id!, @@ -748,44 +747,43 @@ extension PlayerViewController: GCKSessionManagerListener { ] sendJellyfinCommand(command: "PlayNow", options: playNowOptions) } - + func sessionManager(_ sessionManager: GCKSessionManager, didStart session: GCKCastSession) { print("starting session") self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") self.sessionDidStart(manager: sessionManager, didStart: session) } - + func sessionManager(_ sessionManager: GCKSessionManager, didResumeCastSession session: GCKCastSession) { self.jellyfinCastChannel = GCKGenericChannel(namespace: "urn:x-cast:com.connectsdk") print("resuming session") self.sessionDidStart(manager: sessionManager, didStart: session) } - + func sessionManager(_ sessionManager: GCKSessionManager, didFailToStart session: GCKCastSession, withError error: Error) { dump(error) } - func sessionManager(_ sessionManager: GCKSessionManager, didEnd session: GCKCastSession, withError error: Error?) { print("didEnd") - playerDestination = .local; - videoContentView.isHidden = false; + playerDestination = .local + videoContentView.isHidden = false remoteTimeUpdateTimer?.invalidate() castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) startLocalPlaybackEngine(false) } - + func sessionManager(_ sessionManager: GCKSessionManager, didSuspend session: GCKCastSession, with reason: GCKConnectionSuspendReason) { print("didSuspend") - playerDestination = .local; - videoContentView.isHidden = false; + playerDestination = .local + videoContentView.isHidden = false remoteTimeUpdateTimer?.invalidate() castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) startLocalPlaybackEngine(false) } } -//MARK: - VLCMediaPlayer Delegates +// MARK: - VLCMediaPlayer Delegates extension PlayerViewController: VLCMediaPlayerDelegate { func mediaPlayerStateChanged(_ aNotification: Notification!) { let currentState: VLCMediaPlayerState = mediaPlayer.state @@ -820,7 +818,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate { break } } - + func mediaPlayerTimeChanged(_ aNotification: Notification!) { let time = mediaPlayer.position if abs(time-lastTime) > 0.00005 { @@ -851,18 +849,7 @@ extension PlayerViewController: VLCMediaPlayerDelegate { } } - - - - - - - - - - - -//MARK: End VideoPlayerVC +// MARK: End VideoPlayerVC struct VLCPlayerWithControls: UIViewControllerRepresentable { var item: BaseItemDto @Environment(\.presentationMode) var presentationMode @@ -909,7 +896,7 @@ struct VLCPlayerWithControls: UIViewControllerRepresentable { } } -//MARK: - Play State Update Methods +// MARK: - Play State Update Methods extension PlayerViewController { func sendProgressReport(eventName: String) { if (eventName == "timeupdate" && mediaPlayer.state == .playing) || eventName != "timeupdate" { diff --git a/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift b/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift index 02a253b5..5608b1a4 100644 --- a/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift +++ b/JellyfinPlayer/VideoPlayerCastDeviceSelector.swift @@ -15,7 +15,7 @@ class VideoPlayerCastDeviceSelectorView: UIViewController { override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .landscape } - + override func viewDidLoad() { super.viewDidLoad() contentView = UIHostingController(rootView: VideoPlayerCastDeviceSelector(delegate: self.delegate ?? PlayerViewController())) @@ -43,9 +43,9 @@ struct VideoPlayerCastDeviceSelector: View { var body: some View { NavigationView { Group { - if(!delegate.discoveredCastDevices.isEmpty) { + if !delegate.discoveredCastDevices.isEmpty { List(delegate.discoveredCastDevices, id: \.deviceID) { device in - HStack() { + HStack { Text(device.friendlyName!) .font(.subheadline) .fontWeight(.medium) @@ -55,7 +55,7 @@ struct VideoPlayerCastDeviceSelector: View { delegate?.castDeviceChanged() delegate?.castPopoverDismissed() } label: { - HStack() { + HStack { Text("Connect") .font(.caption) .fontWeight(.medium) @@ -91,4 +91,3 @@ struct VideoPlayerCastDeviceSelector: View { }.offset(y: UIDevice.current.userInterfaceIdiom == .pad ? 14 : 0) } } - diff --git a/JellyfinPlayer/VideoPlayerSettingsView.swift b/JellyfinPlayer/VideoPlayerSettingsView.swift index 051258af..896d92b8 100644 --- a/JellyfinPlayer/VideoPlayerSettingsView.swift +++ b/JellyfinPlayer/VideoPlayerSettingsView.swift @@ -15,7 +15,7 @@ class VideoPlayerSettingsView: UIViewController { override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .landscape } - + override func viewDidLoad() { super.viewDidLoad() contentView = UIHostingController(rootView: VideoPlayerSettings(delegate: self.delegate ?? PlayerViewController())) diff --git a/Shared/Extensions/APIExtensions.swift b/Shared/Extensions/APIExtensions.swift index 139842f2..b4db6390 100644 --- a/Shared/Extensions/APIExtensions.swift +++ b/Shared/Extensions/APIExtensions.swift @@ -95,12 +95,12 @@ extension BaseItemDto { let imageType = "Primary" var imageTag = self.imageTags?["Primary"] ?? "" var imageItemId = self.id ?? "" - + if imageTag == "" || imageItemId == "" { imageTag = self.seriesPrimaryImageTag ?? "" imageItemId = self.seriesId ?? "" } - + let x = UIScreen.main.nativeScale * CGFloat(maxWidth) let urlString = "\(ServerEnvironment.current.server.baseURI!)/Items/\(imageItemId)/Images/\(imageType)?maxWidth=\(String(Int(x)))&quality=60&tag=\(imageTag)" diff --git a/Shared/ServerLocator/ServerDiscovery.swift b/Shared/ServerLocator/ServerDiscovery.swift index d35a44e7..04dfec15 100644 --- a/Shared/ServerLocator/ServerDiscovery.swift +++ b/Shared/ServerLocator/ServerDiscovery.swift @@ -17,7 +17,7 @@ public class ServerDiscovery { public let username: String public let password: String public let deviceId: String - + public init(_ host: String, _ port: Int, _ username: String, _ password: String, _ deviceId: String = UUID().uuidString) { self.host = host self.port = port @@ -26,17 +26,17 @@ public class ServerDiscovery { self.deviceId = deviceId } } - + public struct ServerLookupResponse: Codable, Hashable, Identifiable { - + public func hash(into hasher: inout Hasher) { return hasher.combine(id) } - + private let address: String public let id: String public let name: String - + public var url: URL { URL(string: self.address)! } @@ -47,7 +47,7 @@ public class ServerDiscovery { } return self.address } - + public var port: Int { let components = URLComponents(string: self.address) if let port = components?.port { @@ -55,7 +55,7 @@ public class ServerDiscovery { } return 8096 } - + enum CodingKeys: String, CodingKey { case address = "Address" case id = "Id" @@ -63,16 +63,16 @@ public class ServerDiscovery { } } private let broadcastConn: UDPBroadcastConnection - + public init() { func receiveHandler(_ ipAddress: String, _ port: Int, _ response: Data) { } - + func errorHandler(error: UDPBroadcastConnection.ConnectionError) { } self.broadcastConn = try! UDPBroadcastConnection(port: 7359, handler: receiveHandler, errorHandler: errorHandler) } - + public func locateServer(completion: @escaping (ServerLookupResponse?) -> Void) { func receiveHandler(_ ipAddress: String, _ port: Int, _ data: Data) { do { diff --git a/Shared/ServerLocator/UDPBroadCastConnection.swift b/Shared/ServerLocator/UDPBroadCastConnection.swift index d28ab54e..de0cd770 100644 --- a/Shared/ServerLocator/UDPBroadCastConnection.swift +++ b/Shared/ServerLocator/UDPBroadCastConnection.swift @@ -16,38 +16,37 @@ import Darwin let INADDR_ANY = in_addr(s_addr: 0) let INADDR_BROADCAST = in_addr(s_addr: 0xffffffff) - /// An object representing the UDP broadcast connection. Uses a dispatch source to handle the incoming traffic on the UDP socket. open class UDPBroadcastConnection { - + // MARK: Properties - + /// The address of the UDP socket. var address: sockaddr_in - + /// Type of a closure that handles incoming UDP packets. public typealias ReceiveHandler = (_ ipAddress: String, _ port: Int, _ response: Data) -> Void /// Closure that handles incoming UDP packets. var handler: ReceiveHandler? - + /// Type of a closure that handles errors that were encountered during receiving UDP packets. public typealias ErrorHandler = (_ error: ConnectionError) -> Void /// Closure that handles errors that were encountered during receiving UDP packets. var errorHandler: ErrorHandler? - + /// A dispatch source for reading data from the UDP socket. var responseSource: DispatchSourceRead? - + /// The dispatch queue to run responseSource & reconnection on var dispatchQueue: DispatchQueue = DispatchQueue.main - + /// Bind to port to start listening without first sending a message var shouldBeBound: Bool = false - + // MARK: Initializers - + /// Initializes the UDP connection with the correct port address. - + /// - Note: This doesn't open a socket! The socket is opened transparently as needed when sending broadcast messages. If you want to open a socket immediately, use the `bindIt` parameter. This will also try to reopen the socket if it gets closed. /// /// - Parameters: @@ -58,13 +57,13 @@ open class UDPBroadcastConnection { /// - Throws: Throws a `ConnectionError` if an error occurs. public init(port: UInt16, bindIt: Bool = false, handler: ReceiveHandler?, errorHandler: ErrorHandler?) throws { self.address = sockaddr_in( - sin_len: __uint8_t(MemoryLayout.size), + sin_len: __uint8_t(MemoryLayout.size), sin_family: sa_family_t(AF_INET), - sin_port: UDPBroadcastConnection.htonsPort(port: port), - sin_addr: INADDR_BROADCAST, - sin_zero: ( 0, 0, 0, 0, 0, 0, 0, 0 ) + sin_port: UDPBroadcastConnection.htonsPort(port: port), + sin_addr: INADDR_BROADCAST, + sin_zero: ( 0, 0, 0, 0, 0, 0, 0, 0 ) ) - + self.handler = handler self.errorHandler = errorHandler self.shouldBeBound = bindIt @@ -72,34 +71,33 @@ open class UDPBroadcastConnection { try createSocket() } } - + deinit { if responseSource != nil { responseSource!.cancel() } } - + // MARK: Interface - - + /// Create a UDP socket for broadcasting and set up cancel and event handlers /// /// - Throws: Throws a `ConnectionError` if an error occurs. fileprivate func createSocket() throws { - + // Create new socket let newSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) guard newSocket > 0 else { throw ConnectionError.createSocketFailed } - + // Enable broadcast on socket - var broadcastEnable = Int32(1); - let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout.size)); + var broadcastEnable = Int32(1) + let ret = setsockopt(newSocket, SOL_SOCKET, SO_BROADCAST, &broadcastEnable, socklen_t(MemoryLayout.size)) if ret == -1 { debugPrint("Couldn't enable broadcast on socket") close(newSocket) throw ConnectionError.enableBroadcastFailed } - + // Bind socket if needed if shouldBeBound { var saddr = sockaddr(sa_len: 0, sa_family: 0, @@ -114,34 +112,34 @@ open class UDPBroadcastConnection { throw ConnectionError.bindSocketFailed } } - + // Disable global SIGPIPE handler so that the app doesn't crash setNoSigPipe(socket: newSocket) - + // Set up a dispatch source let newResponseSource = DispatchSource.makeReadSource(fileDescriptor: newSocket, queue: dispatchQueue) - + // Set up cancel handler newResponseSource.setCancelHandler { - //debugPrint("Closing UDP socket") + // debugPrint("Closing UDP socket") let UDPSocket = Int32(newResponseSource.handle) shutdown(UDPSocket, SHUT_RDWR) close(UDPSocket) } - + // Set up event handler (gets called when data arrives at the UDP socket) newResponseSource.setEventHandler { [unowned self] in guard let source = self.responseSource else { return } - + var socketAddress = sockaddr_storage() var socketAddressLength = socklen_t(MemoryLayout.size) let response = [UInt8](repeating: 0, count: 4096) let UDPSocket = Int32(source.handle) - + let bytesRead = withUnsafeMutablePointer(to: &socketAddress) { recvfrom(UDPSocket, UnsafeMutableRawPointer(mutating: response), response.count, 0, UnsafeMutableRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1), &socketAddressLength) } - + do { guard bytesRead > 0 else { self.closeConnection() @@ -155,18 +153,18 @@ open class UDPBroadcastConnection { throw ConnectionError.receiveFailed(code: errno) } } - + guard let endpoint = withUnsafePointer(to: &socketAddress, { self.getEndpointFromSocketAddress(socketAddressPointer: UnsafeRawPointer($0).bindMemory(to: sockaddr.self, capacity: 1)) }) else { - //debugPrint("Failed to get the address and port from the socket address received from recvfrom") + // debugPrint("Failed to get the address and port from the socket address received from recvfrom") self.closeConnection() return } - - //debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") - + + // debugPrint("UDP connection received \(bytesRead) bytes from \(endpoint.host):\(endpoint.port)") + let responseBytes = Data(response[0.. 0 else { if let errorString = String(validatingUTF8: strerror(errno)) { - //debugPrint("UDP connection failed to send data: \(errorString)") + // debugPrint("UDP connection failed to send data: \(errorString)") } closeConnection() throw ConnectionError.sendingMessageFailed(code: errno) } - + if sent == broadcastMessageLength { // Success - //debugPrint("UDP connection sent \(broadcastMessageLength) bytes") + // debugPrint("UDP connection sent \(broadcastMessageLength) bytes") } } } - + /// Close the connection. /// /// - Parameter reopen: Automatically reopens the connection if true. Defaults to true. @@ -244,16 +242,16 @@ open class UDPBroadcastConnection { } } } - + // MARK: - Helper - + /// Convert a sockaddr structure into an IP address string and port. /// /// - Parameter socketAddressPointer: socketAddressPointer: Pointer to a socket address. /// - Returns: Returns a tuple of the host IP address and the port in the socket address given. func getEndpointFromSocketAddress(socketAddressPointer: UnsafePointer) -> (host: String, port: Int)? { let socketAddress = UnsafePointer(socketAddressPointer).pointee - + switch Int32(socketAddress.sa_family) { case AF_INET: var socketAddressInet = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in.self) @@ -262,7 +260,7 @@ open class UDPBroadcastConnection { let hostCString = inet_ntop(AF_INET, &socketAddressInet.sin_addr, &buffer, socklen_t(length)) let port = Int(UInt16(socketAddressInet.sin_port).byteSwapped) return (String(cString: hostCString!), port) - + case AF_INET6: var socketAddressInet6 = UnsafeRawPointer(socketAddressPointer).load(as: sockaddr_in6.self) let length = Int(INET6_ADDRSTRLEN) + 2 @@ -270,60 +268,57 @@ open class UDPBroadcastConnection { let hostCString = inet_ntop(AF_INET6, &socketAddressInet6.sin6_addr, &buffer, socklen_t(length)) let port = Int(UInt16(socketAddressInet6.sin6_port).byteSwapped) return (String(cString: hostCString!), port) - + default: return nil } } - - + // MARK: - Private - + /// Prevents crashes when blocking calls are pending and the app is paused (via Home button). /// /// - Parameter socket: The socket for which the signal should be disabled. fileprivate func setNoSigPipe(socket: CInt) { - var no_sig_pipe: Int32 = 1; - setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout.size)); + var no_sig_pipe: Int32 = 1 + setsockopt(socket, SOL_SOCKET, SO_NOSIGPIPE, &no_sig_pipe, socklen_t(MemoryLayout.size)) } - + fileprivate class func htonsPort(port: in_port_t) -> in_port_t { let isLittleEndian = Int(OSHostByteOrder()) == OSLittleEndian return isLittleEndian ? _OSSwapInt16(port) : port } - + fileprivate class func ntohs(value: CUnsignedShort) -> CUnsignedShort { return (value << 8) + (value >> 8) } - + } - - // Created by Gunter Hager on 25.03.19. // Copyright © 2019 Gunter Hager. All rights reserved. // public extension UDPBroadcastConnection { - + enum ConnectionError: Error { // Creating socket case createSocketFailed case enableBroadcastFailed case bindSocketFailed - + // Sending message case messageEncodingFailed case sendingMessageFailed(code: Int32) - + // Receiving data case receivedEndOfFile case receiveFailed(code: Int32) - + // Closing socket case reopeningSocketFailed(error: Error) - + // Underlying case underlying(error: Error) } - + } diff --git a/Shared/Singleton/SessionManager.swift b/Shared/Singleton/SessionManager.swift index f0f27de8..bcfc516d 100644 --- a/Shared/Singleton/SessionManager.swift +++ b/Shared/Singleton/SessionManager.swift @@ -149,7 +149,7 @@ final class SessionManager { func logout() { let nc = NotificationCenter.default nc.post(name: Notification.Name("didSignOut"), object: nil) - + let keychain = KeychainSwift() keychain.accessGroup = "9R8RREG67J.me.vigue.jellyfin.sharedKeychain" keychain.delete("AccessToken_\(user?.user_id ?? "")") diff --git a/Shared/ViewModels/ConnectToServerViewModel.swift b/Shared/ViewModels/ConnectToServerViewModel.swift index 0c34b20d..5c3898e3 100644 --- a/Shared/ViewModels/ConnectToServerViewModel.swift +++ b/Shared/ViewModels/ConnectToServerViewModel.swift @@ -14,7 +14,7 @@ import JellyfinAPI final class ConnectToServerViewModel: ViewModel { @Published var isConnectedServer = false - + var uriSubject = CurrentValueSubject("") var usernameSubject = CurrentValueSubject("") var passwordSubject = CurrentValueSubject("") @@ -25,11 +25,11 @@ final class ConnectToServerViewModel: ViewModel { var publicUsers = [UserDto]() @Published var selectedPublicUser = UserDto() - + private let discovery: ServerDiscovery = ServerDiscovery() @Published var servers: [ServerDiscovery.ServerLookupResponse] = [] @Published var searching = false - + override init() { super.init() getPublicUsers() @@ -74,8 +74,8 @@ final class ConnectToServerViewModel: ViewModel { }) .store(in: &cancellables) } - - func connectToServer(at url : URL) { + + func connectToServer(at url: URL) { ServerEnvironment.current.create(with: url.absoluteString) .trackActivity(loading) .sink(receiveCompletion: { result in @@ -90,15 +90,15 @@ final class ConnectToServerViewModel: ViewModel { }) .store(in: &cancellables) } - + func discoverServers() { searching = true - + // Timeout after 5 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 5) { self.searching = false } - + discovery.locateServer { [self] (server) in if let server = server, !servers.contains(server) { servers.append(server) diff --git a/Shared/ViewModels/LibraryFilterViewModel.swift b/Shared/ViewModels/LibraryFilterViewModel.swift index 8b38372c..bf6a01ec 100644 --- a/Shared/ViewModels/LibraryFilterViewModel.swift +++ b/Shared/ViewModels/LibraryFilterViewModel.swift @@ -39,12 +39,12 @@ final class LibraryFilterViewModel: ViewModel { var selectedSortOrder: APISortOrder = .descending @Published var selectedSortBy: SortBy = .name - + func updateModifiedFilter() { modifiedFilters.sortOrder = [selectedSortOrder] modifiedFilters.sortBy = [selectedSortBy] } - + func resetFilters() { modifiedFilters = LibraryFilters(filters: [], sortOrder: [.ascending], withGenres: [], tags: [], sortBy: [.name]) } @@ -54,7 +54,7 @@ final class LibraryFilterViewModel: ViewModel { self.enabledFilterType = enabledFilterType self.selectedSortBy = filters!.sortBy.first! self.selectedSortOrder = filters!.sortOrder.first! - + super.init() if let filters = filters { self.modifiedFilters = filters diff --git a/Shared/ViewModels/SettingsViewModel.swift b/Shared/ViewModels/SettingsViewModel.swift index eba576d6..8ad1033a 100644 --- a/Shared/ViewModels/SettingsViewModel.swift +++ b/Shared/ViewModels/SettingsViewModel.swift @@ -26,7 +26,7 @@ struct Bitrates: Codable, Hashable { struct TrackLanguage: Hashable { var name: String var isoCode: String - + static let auto = TrackLanguage(name: "Auto", isoCode: "Auto") } diff --git a/WidgetExtension/NextUpWidget.swift b/WidgetExtension/NextUpWidget.swift index 83b89c98..26f91c61 100644 --- a/WidgetExtension/NextUpWidget.swift +++ b/WidgetExtension/NextUpWidget.swift @@ -28,8 +28,8 @@ struct NextUpWidgetProvider: TimelineProvider { let server = ServerEnvironment.current.server let savedUser = SessionManager.current.user var tempCancellables = Set() - - if(server != nil && savedUser != nil) { + + if server != nil && savedUser != nil { JellyfinAPI.basePath = server!.baseURI ?? "" TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people], @@ -73,8 +73,8 @@ struct NextUpWidgetProvider: TimelineProvider { let savedUser = SessionManager.current.user var tempCancellables = Set() - - if(server != nil && savedUser != nil) { + + if server != nil && savedUser != nil { JellyfinAPI.basePath = server!.baseURI ?? "" TvShowsAPI.getNextUp(userId: savedUser!.user_id, limit: 3, fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people],