diff --git a/JellyfinPlayer.xcodeproj/project.pbxproj b/JellyfinPlayer.xcodeproj/project.pbxproj index a522db30..77236bcd 100644 --- a/JellyfinPlayer.xcodeproj/project.pbxproj +++ b/JellyfinPlayer.xcodeproj/project.pbxproj @@ -478,7 +478,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer/Preview Content\""; DEVELOPMENT_TEAM = 9R8RREG67J; ENABLE_BITCODE = NO; @@ -508,7 +508,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 19; + CURRENT_PROJECT_VERSION = 20; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = "\"JellyfinPlayer/Preview Content\""; DEVELOPMENT_TEAM = 9R8RREG67J; diff --git a/JellyfinPlayer/ContinueWatchingView.swift b/JellyfinPlayer/ContinueWatchingView.swift index 570dc66f..8bce8cce 100644 --- a/JellyfinPlayer/ContinueWatchingView.swift +++ b/JellyfinPlayer/ContinueWatchingView.swift @@ -118,7 +118,7 @@ struct ContinueWatchingView: View { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 6, height: 6))!) + Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 48, height: 32))!) .resizable() .frame(width: 320, height: 180) .cornerRadius(10) @@ -148,7 +148,7 @@ struct ContinueWatchingView: View { WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=550&quality=80&tag=\(item.Image)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 6, height: 6))!) + Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 48, height: 32))!) .resizable() .frame(width: 320, height: 180) .cornerRadius(10) diff --git a/JellyfinPlayer/EpisodeItemView.swift b/JellyfinPlayer/EpisodeItemView.swift index 4b919e4c..dd2f2293 100644 --- a/JellyfinPlayer/EpisodeItemView.swift +++ b/JellyfinPlayer/EpisodeItemView.swift @@ -196,6 +196,9 @@ struct EpisodeItemView: View { .supportedOrientations(.landscape) .overrideViewPreference(.dark) .prefersHomeIndicatorAutoHidden(true) + .introspectTabBarController { (UITabBarController) in + UITabBarController.tabBar.isHidden = true + } } else { LoadingView(isShowing: $isLoading) { VStack(alignment:.leading) { @@ -570,11 +573,12 @@ struct EpisodeItemView: View { .navigationBarTitleDisplayMode(.inline) .navigationTitle("\(fullItem.Name) - S\(String(fullItem.ParentIndexNumber ?? 0)):E\(String(fullItem.IndexNumber ?? 0)) - \(fullItem.SeriesName ?? "")") .introspectTabBarController { (UITabBarController) in - UITabBarController.tabBar.isHidden = true + UITabBarController.tabBar.isHidden = false } }.onAppear(perform: loadData) .supportedOrientations(.allButUpsideDown) .overrideViewPreference(.unspecified) + .preferredColorScheme(.none) .prefersHomeIndicatorAutoHidden(false) } } diff --git a/JellyfinPlayer/Info.plist b/JellyfinPlayer/Info.plist index 0044587d..de1bc59e 100644 --- a/JellyfinPlayer/Info.plist +++ b/JellyfinPlayer/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 19 + 20 ITSAppUsesNonExemptEncryption LSRequiresIPhoneOS diff --git a/JellyfinPlayer/LatestMediaView.swift b/JellyfinPlayer/LatestMediaView.swift index b760b601..a2f11bb4 100644 --- a/JellyfinPlayer/LatestMediaView.swift +++ b/JellyfinPlayer/LatestMediaView.swift @@ -91,10 +91,10 @@ struct LatestMediaView: View { VStack(alignment: .leading) { if(item.Type == "Series") { Spacer().frame(height:10) - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=150&quality=80&tag=\(item.Image)")!) + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 6, height: 6))!) + Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) .resizable() .frame(width: 100, height: 150) .cornerRadius(10) @@ -122,10 +122,10 @@ struct LatestMediaView: View { ).shadow(radius: 6) } else { Spacer().frame(height:10) - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=150&quality=80&tag=\(item.Image)")!) + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 6, height: 6))!) + Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) .resizable() .frame(width: 100, height: 150) .cornerRadius(10) diff --git a/JellyfinPlayer/LibraryView.swift b/JellyfinPlayer/LibraryView.swift index 84fb0c5e..43ebc1ad 100644 --- a/JellyfinPlayer/LibraryView.swift +++ b/JellyfinPlayer/LibraryView.swift @@ -208,7 +208,7 @@ struct LibraryView: View { NavigationLink(destination: ItemView(item: item )) { VStack(alignment: .leading) { if(item.Type == "Movie") { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=150&quality=80&tag=\(item.Image)")) + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) .resizable() .placeholder { Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) @@ -218,8 +218,9 @@ struct LibraryView: View { } .frame(width:100, height: 150) .cornerRadius(10) + .shadow(radius: 5) } else { - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=150&quality=80&tag=\(item.Image)")) + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.Id)/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")) .resizable() .placeholder { Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) @@ -245,7 +246,7 @@ struct LibraryView: View { .opacity(0.8) .cornerRadius(10.0) .padding(3), alignment: .topTrailing - ) + ).shadow(radius: 5) } Text(item.Name) .font(.caption) diff --git a/JellyfinPlayer/Models/SettingsModel.swift b/JellyfinPlayer/Models/SettingsModel.swift index 8646f1bd..41155dde 100644 --- a/JellyfinPlayer/Models/SettingsModel.swift +++ b/JellyfinPlayer/Models/SettingsModel.swift @@ -12,6 +12,8 @@ struct UserSettings: Decodable { var RemoteMaxBitrate: Int; var AutoSelectSubtitles: Bool; var AutoSelectSubtitlesLangcode: String; + var SubtitlePositionOffset: Int; + var SubtitleFontName: String; } struct Bitrates: Codable, Hashable { diff --git a/JellyfinPlayer/MovieItemView.swift b/JellyfinPlayer/MovieItemView.swift index 7a4af16a..27b55bca 100644 --- a/JellyfinPlayer/MovieItemView.swift +++ b/JellyfinPlayer/MovieItemView.swift @@ -224,6 +224,7 @@ struct MovieItemView: View { _progressString.wrappedValue = "\(String(progminutes).leftPad(toWidth: 2, withString: "0"))m" } } + _isLoading.wrappedValue = false; } catch { } @@ -232,7 +233,6 @@ struct MovieItemView: View { debugPrint(error) break } - _isLoading.wrappedValue = false; } } @@ -243,6 +243,9 @@ struct MovieItemView: View { .preferredColorScheme(.dark) .overrideViewPreference(.dark) .prefersHomeIndicatorAutoHidden(true) + .introspectTabBarController { (UITabBarController) in + UITabBarController.tabBar.isHidden = true + } } else { LoadingView(isShowing: $isLoading) { VStack(alignment:.leading) { @@ -382,14 +385,14 @@ struct MovieItemView: View { WebImage(url: cast.Image) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 4, height: 4))!) + Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!) .resizable() .aspectRatio(contentMode: .fill) - .frame(width: 70, height: 70) + .frame(width: 100, height: 100) .cornerRadius(10) } .aspectRatio(contentMode: .fill) - .frame(width: 70, height: 70) + .frame(width: 100, height: 100) .cornerRadius(10).shadow(radius: 6) Text(cast.Name).font(.footnote).fontWeight(.regular).lineLimit(1).frame(width: 100).foregroundColor(Color.primary) if(cast.Role != "") { @@ -564,7 +567,7 @@ struct MovieItemView: View { WebImage(url: cast.Image) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { - Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 8, height: 8))!) + Image(uiImage: UIImage(blurHash: (cast.ImageBlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : cast.ImageBlurHash), size: CGSize(width: 16, height: 16))!) .resizable() .aspectRatio(contentMode: .fill) .frame(width: 100, height: 100) @@ -621,6 +624,7 @@ struct MovieItemView: View { }.onAppear(perform: loadData) .supportedOrientations(.allButUpsideDown) .overrideViewPreference(.unspecified) + .preferredColorScheme(.none) .prefersHomeIndicatorAutoHidden(false) } } diff --git a/JellyfinPlayer/NextUpView.swift b/JellyfinPlayer/NextUpView.swift index f850fda7..f228a223 100644 --- a/JellyfinPlayer/NextUpView.swift +++ b/JellyfinPlayer/NextUpView.swift @@ -79,7 +79,7 @@ struct NextUpView: View { NavigationLink(destination: ItemView(item: item)) { VStack(alignment: .leading) { Spacer().frame(height:10) - WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.SeriesId ?? "")/Images/\(item.ImageType)?maxWidth=150&quality=80&tag=\(item.Image)")!) + WebImage(url: URL(string: "\(globalData.server?.baseURI ?? "")/Items/\(item.SeriesId ?? "")/Images/\(item.ImageType)?maxWidth=250&quality=80&tag=\(item.Image)")!) .resizable() // Resizable like SwiftUI.Image, you must use this modifier or the view will use the image bitmap size .placeholder { Image(uiImage: UIImage(blurHash: (item.BlurHash == "" ? "W$H.4}D%bdo#a#xbtpxVW?W?jXWsXVt7Rjf5axWqxbWXnhada{s-" : item.BlurHash), size: CGSize(width: 16, height: 16))!) diff --git a/JellyfinPlayer/Resources/bitrates.json b/JellyfinPlayer/Resources/bitrates.json index db47b15d..2b5863ce 100644 --- a/JellyfinPlayer/Resources/bitrates.json +++ b/JellyfinPlayer/Resources/bitrates.json @@ -7,6 +7,10 @@ "name": "4K - 100 Mbps", "value": 100000000 }, + { + "name": "4K - 80 Mbps", + "value": 80000000 + }, { "name": "1080p - 60 Mbps", "value": 60000000 diff --git a/JellyfinPlayer/VideoPlayerView.swift b/JellyfinPlayer/VideoPlayerView.swift index 2fbc9b2f..9438afaa 100644 --- a/JellyfinPlayer/VideoPlayerView.swift +++ b/JellyfinPlayer/VideoPlayerView.swift @@ -44,7 +44,7 @@ struct VideoPlayerView: View { var item: DetailItem; @State private var pbitem: PlaybackItem = PlaybackItem(videoType: VideoType.direct, videoUrl: URL(string: "https://example.com")!, subtitles: []); @State private var streamLoading = false; - @State private var vlcplayer: VLCMediaPlayer = VLCMediaPlayer(options: ["--sub-margin=-50"]); + @State private var vlcplayer: VLCMediaPlayer = VLCMediaPlayer(); @State private var isPlaying = false; @State private var subtitles: [Subtitle] = []; @State private var audioTracks: [Subtitle] = []; @@ -272,7 +272,7 @@ struct VideoPlayerView: View { let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "") _subtitles.wrappedValue.append(disableSubtitleTrack); for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] { - if(stream["Type"].string == "Subtitle" && stream["Codec"] != "subrip") { //ignore ripped subtitles - we don't want to extract subtitles + if(stream["Type"].string == "Subtitle") { //ignore ripped subtitles - we don't want to extract subtitles let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")! let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "") _subtitles.wrappedValue.append(subtitle); @@ -315,7 +315,7 @@ struct VideoPlayerView: View { let disableSubtitleTrack = Subtitle(name: "Disabled", id: -1, url: URL(string: "https://example.com")!, delivery: "Embed", codec: "") _subtitles.wrappedValue.append(disableSubtitleTrack); for (_,stream):(String, JSON) in json["MediaSources"][0]["MediaStreams"] { - if(stream["Type"].string == "Subtitle" && stream["Codec"] != "subrip") { + if(stream["Type"].string == "Subtitle") { let deliveryUrl = URL(string: "\(globalData.server?.baseURI ?? "")\(stream["DeliveryUrl"].string ?? "")")! let subtitle = Subtitle(name: stream["DisplayTitle"].string ?? "", id: Int32(stream["Index"].int ?? 0), url: deliveryUrl, delivery: stream["DeliveryMethod"].string ?? "", codec: stream["Codec"].string ?? "") _subtitles.wrappedValue.append(subtitle); @@ -490,10 +490,14 @@ struct VideoPlayerView: View { .onAppear(perform: startStream) .navigationBarHidden(true) .overrideViewPreference(.dark) + .preferredColorScheme(.dark) .navigationBarBackButtonHidden(true) .edgesIgnoringSafeArea(.all) - .introspectTabBarController { (UITabBarController) in - UITabBarController.tabBar.isHidden = true + .withHostingWindow { window in + if let vc = window?.rootViewController { + let preferenceHost = vc as! PreferenceUIHostingController + preferenceHost._viewPreference = .dark + } } .statusBar(hidden: true) .onTapGesture(perform: resetTimer) diff --git a/JellyfinPlayer/Views/SettingsView.swift b/JellyfinPlayer/Views/SettingsView.swift index f068cfae..4b520ece 100644 --- a/JellyfinPlayer/Views/SettingsView.swift +++ b/JellyfinPlayer/Views/SettingsView.swift @@ -18,19 +18,23 @@ struct SettingsView: View { @State private var username: String = ""; @State private var inNetworkStreamBitrate: Int = 40000000; @State private var outOfNetworkStreamBitrate: Int = 40000000; + @State private var autoSelectSubtitles: Bool = false; + @State private var autoSelectSubtitlesLangcode: String = "none"; func onAppear() { _username.wrappedValue = globalData.user?.username ?? ""; let defaults = UserDefaults.standard _inNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "InNetworkBandwidth"); _outOfNetworkStreamBitrate.wrappedValue = defaults.integer(forKey: "OutOfNetworkBandwidth"); + _autoSelectSubtitles.wrappedValue = defaults.bool(forKey: "AutoSelectSubtitles"); + _autoSelectSubtitlesLangcode.wrappedValue = defaults.string(forKey: "AutoSelectSubtitlesLangcode") ?? ""; } var body: some View { NavigationView() { Form() { Section(header: Text("Playback settings")) { - Picker("Default local playback bitrate", selection: $inNetworkStreamBitrate) { + Picker("Default local quality", selection: $inNetworkStreamBitrate) { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in Text(bitrate.name).tag(bitrate.value) } @@ -39,7 +43,7 @@ struct SettingsView: View { defaults.setValue(_inNetworkStreamBitrate.wrappedValue, forKey: "InNetworkBandwidth") } - Picker("Default remote playback bitrate", selection: $outOfNetworkStreamBitrate) { + Picker("Default remote quality", selection: $outOfNetworkStreamBitrate) { ForEach(self.viewModel.bitrates, id: \.self) { bitrate in Text(bitrate.name).tag(bitrate.value) } @@ -49,38 +53,53 @@ struct SettingsView: View { } } + 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) { + + } + } + Section() { - Button { - let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Server") - let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + HStack() { + Text("Signed in as \(username)").foregroundColor(.primary) + Spacer() + Button { + let fetchRequest: NSFetchRequest = NSFetchRequest(entityName: "Server") + let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) - do { - try viewContext.execute(deleteRequest) - } catch _ as NSError { - // TODO: handle the error - } - - let fetchRequest2: NSFetchRequest = NSFetchRequest(entityName: "SignedInUser") - let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2) + do { + try viewContext.execute(deleteRequest) + } catch _ as NSError { + // TODO: handle the error + } + + let fetchRequest2: NSFetchRequest = NSFetchRequest(entityName: "SignedInUser") + let deleteRequest2 = NSBatchDeleteRequest(fetchRequest: fetchRequest2) - do { - try viewContext.execute(deleteRequest2) - } catch _ as NSError { - // TODO: handle the error + do { + try viewContext.execute(deleteRequest2) + } catch _ as NSError { + // TODO: handle the error + } + + globalData.server = nil + globalData.user = nil + globalData.authToken = "" + globalData.authHeader = "" + jsi.did = true + // TODO: This should redirect to the server selection screen + exit(-1) + } label: { + Text("Log out").font(.callout) } - - globalData.server = nil - globalData.user = nil - globalData.authToken = "" - globalData.authHeader = "" - jsi.did = true - // TODO: This should redirect to the server selection screen - exit(-1) - } label: { - Text("Log out") } } } + .navigationBarTitle("Settings", displayMode: .inline) .toolbar { ToolbarItemGroup(placement: .navigationBarLeading) {