diff --git a/Shared/Coordinators/SettingsCoordinator.swift b/Shared/Coordinators/SettingsCoordinator.swift index f540f219..7b08685e 100644 --- a/Shared/Coordinators/SettingsCoordinator.swift +++ b/Shared/Coordinators/SettingsCoordinator.swift @@ -11,7 +11,6 @@ import Stinsen import SwiftUI final class SettingsCoordinator: NavigationCoordinatable { - let stack = NavigationStack(initial: \SettingsCoordinator.start) @Root @@ -32,6 +31,8 @@ final class SettingsCoordinator: NavigationCoordinatable { #if !os(tvOS) @Route(.push) var quickConnect = makeQuickConnectSettings + @Route(.push) + var fontPicker = makeFontPicker #endif @ViewBuilder @@ -71,6 +72,12 @@ final class SettingsCoordinator: NavigationCoordinatable { let viewModel = QuickConnectSettingsViewModel() QuickConnectSettingsView(viewModel: viewModel) } + + @ViewBuilder + func makeFontPicker() -> some View { + FontPickerView() + .navigationTitle(L10n.subtitleFont) + } #endif @ViewBuilder diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift index a6ccd682..833f3bbd 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift @@ -44,137 +44,137 @@ extension BaseItemDto { var viewModels: [VideoPlayerViewModel] = [] for currentMediaSource in mediaSources { - let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first - let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? [] - let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? [] + let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first + let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? [] + let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? [] - let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! }) + let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! }) - let defaultSubtitleStream = subtitleStreams - .first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 }) + let defaultSubtitleStream = subtitleStreams + .first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 }) - // MARK: Build Streams + // MARK: Build Streams - let directStreamURL: URL - let transcodedStreamURL: URLComponents? - var hlsStreamURL: URL - let mediaSourceID: String - let streamType: ServerStreamType + let directStreamURL: URL + let transcodedStreamURL: URLComponents? + var hlsStreamURL: URL + let mediaSourceID: String + let streamType: ServerStreamType - if mediaSources.count > 1 { - mediaSourceID = currentMediaSource.id! - } else { - mediaSourceID = self.id! - } - - let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder( - itemId: self.id!, - _static: true, - tag: self.etag, - playSessionId: response.playSessionId, - minSegments: 6, - mediaSourceId: mediaSourceID - ) - directStreamURL = URL(string: directStreamBuilder.URLString)! - - if let transcodeURL = currentMediaSource.transcodingUrl { - streamType = .transcode - transcodedStreamURL = URLComponents( - string: SessionManager.main.currentLogin.server.currentURI - .appending(transcodeURL) - )! - } else { - streamType = .direct - transcodedStreamURL = nil - } - - let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder( - itemId: id ?? "", - mediaSourceId: id ?? "", - _static: true, - tag: currentMediaSource.eTag, - deviceProfileId: nil, - playSessionId: response.playSessionId, - segmentContainer: "ts", - segmentLength: nil, - minSegments: 2, - deviceId: UIDevice.vendorUUIDString, - audioCodec: audioStreams - .compactMap(\.codec) - .joined(separator: ","), - breakOnNonKeyFrames: true, - requireAvc: true, - transcodingMaxAudioChannels: 6, - videoCodec: videoStream?.codec, - videoStreamIndex: videoStream?.index, - enableAdaptiveBitrateStreaming: true - ) - - var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)! - hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken) - - hlsStreamURL = hlsStreamComponents.url! - - // MARK: VidoPlayerViewModel Creation - - var subtitle: String? - - // MARK: Attach media content to self - - var modifiedSelfItem = self - modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams - - // TODO: other forms of media subtitle - if self.itemType == .episode { - if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() { - subtitle = "\(seriesName) - \(episodeLocator)" - } - } - - let subtitlesEnabled = defaultSubtitleStream != nil - - let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode - let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay - - let overlayType = Defaults[.overlayType] - - let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode - let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode - - var fileName: String? - if let lastInPath = currentMediaSource.path?.split(separator: "/").last { - fileName = String(lastInPath) - } - - let videoPlayerViewModel = VideoPlayerViewModel( - item: modifiedSelfItem, - title: modifiedSelfItem.name ?? "", - subtitle: subtitle, - directStreamURL: directStreamURL, - transcodedStreamURL: transcodedStreamURL?.url, - hlsStreamURL: hlsStreamURL, - streamType: streamType, - response: response, - audioStreams: audioStreams, - subtitleStreams: subtitleStreams, - chapters: modifiedSelfItem.chapters ?? [], - selectedAudioStreamIndex: defaultAudioStream?.index ?? -1, - selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1, - subtitlesEnabled: subtitlesEnabled, - autoplayEnabled: autoplayEnabled, - overlayType: overlayType, - shouldShowPlayPreviousItem: shouldShowPlayPreviousItem, - shouldShowPlayNextItem: shouldShowPlayNextItem, - shouldShowAutoPlay: shouldShowAutoPlay, - container: currentMediaSource.container ?? "", - filename: fileName, - versionName: currentMediaSource.name - ) - - viewModels.append(videoPlayerViewModel) + if mediaSources.count > 1 { + mediaSourceID = currentMediaSource.id! + } else { + mediaSourceID = self.id! } - return viewModels + let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder( + itemId: self.id!, + _static: true, + tag: self.etag, + playSessionId: response.playSessionId, + minSegments: 6, + mediaSourceId: mediaSourceID + ) + directStreamURL = URL(string: directStreamBuilder.URLString)! + + if let transcodeURL = currentMediaSource.transcodingUrl { + streamType = .transcode + transcodedStreamURL = URLComponents( + string: SessionManager.main.currentLogin.server.currentURI + .appending(transcodeURL) + )! + } else { + streamType = .direct + transcodedStreamURL = nil + } + + let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder( + itemId: id ?? "", + mediaSourceId: id ?? "", + _static: true, + tag: currentMediaSource.eTag, + deviceProfileId: nil, + playSessionId: response.playSessionId, + segmentContainer: "ts", + segmentLength: nil, + minSegments: 2, + deviceId: UIDevice.vendorUUIDString, + audioCodec: audioStreams + .compactMap(\.codec) + .joined(separator: ","), + breakOnNonKeyFrames: true, + requireAvc: true, + transcodingMaxAudioChannels: 6, + videoCodec: videoStream?.codec, + videoStreamIndex: videoStream?.index, + enableAdaptiveBitrateStreaming: true + ) + + var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)! + hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken) + + hlsStreamURL = hlsStreamComponents.url! + + // MARK: VidoPlayerViewModel Creation + + var subtitle: String? + + // MARK: Attach media content to self + + var modifiedSelfItem = self + modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams + + // TODO: other forms of media subtitle + if self.itemType == .episode { + if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() { + subtitle = "\(seriesName) - \(episodeLocator)" + } + } + + let subtitlesEnabled = defaultSubtitleStream != nil + + let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode + let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay + + let overlayType = Defaults[.overlayType] + + let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode + let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode + + var fileName: String? + if let lastInPath = currentMediaSource.path?.split(separator: "/").last { + fileName = String(lastInPath) + } + + let videoPlayerViewModel = VideoPlayerViewModel( + item: modifiedSelfItem, + title: modifiedSelfItem.name ?? "", + subtitle: subtitle, + directStreamURL: directStreamURL, + transcodedStreamURL: transcodedStreamURL?.url, + hlsStreamURL: hlsStreamURL, + streamType: streamType, + response: response, + audioStreams: audioStreams, + subtitleStreams: subtitleStreams, + chapters: modifiedSelfItem.chapters ?? [], + selectedAudioStreamIndex: defaultAudioStream?.index ?? -1, + selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1, + subtitlesEnabled: subtitlesEnabled, + autoplayEnabled: autoplayEnabled, + overlayType: overlayType, + shouldShowPlayPreviousItem: shouldShowPlayPreviousItem, + shouldShowPlayNextItem: shouldShowPlayNextItem, + shouldShowAutoPlay: shouldShowAutoPlay, + container: currentMediaSource.container ?? "", + filename: fileName, + versionName: currentMediaSource.name + ) + + viewModels.append(videoPlayerViewModel) + } + + return viewModels } .eraseToAnyPublisher() } @@ -211,137 +211,137 @@ extension BaseItemDto { var viewModels: [VideoPlayerViewModel] = [] for currentMediaSource in mediaSources { - let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first - let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? [] - let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? [] + let videoStream = currentMediaSource.mediaStreams?.filter { $0.type == .video }.first + let audioStreams = currentMediaSource.mediaStreams?.filter { $0.type == .audio } ?? [] + let subtitleStreams = currentMediaSource.mediaStreams?.filter { $0.type == .subtitle } ?? [] - let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! }) + let defaultAudioStream = audioStreams.first(where: { $0.index! == currentMediaSource.defaultAudioStreamIndex! }) - let defaultSubtitleStream = subtitleStreams - .first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 }) + let defaultSubtitleStream = subtitleStreams + .first(where: { $0.index! == currentMediaSource.defaultSubtitleStreamIndex ?? -1 }) - // MARK: Build Streams + // MARK: Build Streams - let directStreamURL: URL - let transcodedStreamURL: URLComponents? - var hlsStreamURL: URL - let mediaSourceID: String - let streamType: ServerStreamType + let directStreamURL: URL + let transcodedStreamURL: URLComponents? + var hlsStreamURL: URL + let mediaSourceID: String + let streamType: ServerStreamType - if mediaSources.count > 1 { - mediaSourceID = currentMediaSource.id! - } else { - mediaSourceID = self.id! - } - - let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder( - itemId: self.id!, - _static: true, - tag: self.etag, - playSessionId: response.playSessionId, - minSegments: 6, - mediaSourceId: mediaSourceID - ) - directStreamURL = URL(string: directStreamBuilder.URLString)! - - if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.liveTVForceDirectPlay] { - streamType = .transcode - transcodedStreamURL = URLComponents( - string: SessionManager.main.currentLogin.server.currentURI - .appending(transcodeURL) - )! - } else { - streamType = .direct - transcodedStreamURL = nil - } - - let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder( - itemId: id ?? "", - mediaSourceId: id ?? "", - _static: true, - tag: currentMediaSource.eTag, - deviceProfileId: nil, - playSessionId: response.playSessionId, - segmentContainer: "ts", - segmentLength: nil, - minSegments: 2, - deviceId: UIDevice.vendorUUIDString, - audioCodec: audioStreams - .compactMap(\.codec) - .joined(separator: ","), - breakOnNonKeyFrames: true, - requireAvc: true, - transcodingMaxAudioChannels: 6, - videoCodec: videoStream?.codec, - videoStreamIndex: videoStream?.index, - enableAdaptiveBitrateStreaming: true - ) - - var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)! - hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken) - - hlsStreamURL = hlsStreamComponents.url! - - // MARK: VidoPlayerViewModel Creation - - var subtitle: String? - - // MARK: Attach media content to self - - var modifiedSelfItem = self - modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams - - // TODO: other forms of media subtitle - if self.itemType == .episode { - if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() { - subtitle = "\(seriesName) - \(episodeLocator)" - } - } - - let subtitlesEnabled = defaultSubtitleStream != nil - - let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode - let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay - - let overlayType = Defaults[.overlayType] - - let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode - let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode - - var fileName: String? - if let lastInPath = currentMediaSource.path?.split(separator: "/").last { - fileName = String(lastInPath) - } - - let videoPlayerViewModel = VideoPlayerViewModel( - item: modifiedSelfItem, - title: modifiedSelfItem.name ?? "", - subtitle: subtitle, - directStreamURL: directStreamURL, - transcodedStreamURL: transcodedStreamURL?.url, - hlsStreamURL: hlsStreamURL, - streamType: streamType, - response: response, - audioStreams: audioStreams, - subtitleStreams: subtitleStreams, - chapters: modifiedSelfItem.chapters ?? [], - selectedAudioStreamIndex: defaultAudioStream?.index ?? -1, - selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1, - subtitlesEnabled: subtitlesEnabled, - autoplayEnabled: autoplayEnabled, - overlayType: overlayType, - shouldShowPlayPreviousItem: shouldShowPlayPreviousItem, - shouldShowPlayNextItem: shouldShowPlayNextItem, - shouldShowAutoPlay: shouldShowAutoPlay, - container: currentMediaSource.container ?? "", - filename: fileName, - versionName: currentMediaSource.name - ) - - viewModels.append(videoPlayerViewModel) + if mediaSources.count > 1 { + mediaSourceID = currentMediaSource.id! + } else { + mediaSourceID = self.id! } - return viewModels + let directStreamBuilder = VideosAPI.getVideoStreamWithRequestBuilder( + itemId: self.id!, + _static: true, + tag: self.etag, + playSessionId: response.playSessionId, + minSegments: 6, + mediaSourceId: mediaSourceID + ) + directStreamURL = URL(string: directStreamBuilder.URLString)! + + if let transcodeURL = currentMediaSource.transcodingUrl, !Defaults[.Experimental.liveTVForceDirectPlay] { + streamType = .transcode + transcodedStreamURL = URLComponents( + string: SessionManager.main.currentLogin.server.currentURI + .appending(transcodeURL) + )! + } else { + streamType = .direct + transcodedStreamURL = nil + } + + let hlsStreamBuilder = DynamicHlsAPI.getMasterHlsVideoPlaylistWithRequestBuilder( + itemId: id ?? "", + mediaSourceId: id ?? "", + _static: true, + tag: currentMediaSource.eTag, + deviceProfileId: nil, + playSessionId: response.playSessionId, + segmentContainer: "ts", + segmentLength: nil, + minSegments: 2, + deviceId: UIDevice.vendorUUIDString, + audioCodec: audioStreams + .compactMap(\.codec) + .joined(separator: ","), + breakOnNonKeyFrames: true, + requireAvc: true, + transcodingMaxAudioChannels: 6, + videoCodec: videoStream?.codec, + videoStreamIndex: videoStream?.index, + enableAdaptiveBitrateStreaming: true + ) + + var hlsStreamComponents = URLComponents(string: hlsStreamBuilder.URLString)! + hlsStreamComponents.addQueryItem(name: "api_key", value: SessionManager.main.currentLogin.user.accessToken) + + hlsStreamURL = hlsStreamComponents.url! + + // MARK: VidoPlayerViewModel Creation + + var subtitle: String? + + // MARK: Attach media content to self + + var modifiedSelfItem = self + modifiedSelfItem.mediaStreams = currentMediaSource.mediaStreams + + // TODO: other forms of media subtitle + if self.itemType == .episode { + if let seriesName = self.seriesName, let episodeLocator = self.getEpisodeLocator() { + subtitle = "\(seriesName) - \(episodeLocator)" + } + } + + let subtitlesEnabled = defaultSubtitleStream != nil + + let shouldShowAutoPlay = Defaults[.shouldShowAutoPlay] && itemType == .episode + let autoplayEnabled = Defaults[.autoplayEnabled] && shouldShowAutoPlay + + let overlayType = Defaults[.overlayType] + + let shouldShowPlayPreviousItem = Defaults[.shouldShowPlayPreviousItem] && itemType == .episode + let shouldShowPlayNextItem = Defaults[.shouldShowPlayNextItem] && itemType == .episode + + var fileName: String? + if let lastInPath = currentMediaSource.path?.split(separator: "/").last { + fileName = String(lastInPath) + } + + let videoPlayerViewModel = VideoPlayerViewModel( + item: modifiedSelfItem, + title: modifiedSelfItem.name ?? "", + subtitle: subtitle, + directStreamURL: directStreamURL, + transcodedStreamURL: transcodedStreamURL?.url, + hlsStreamURL: hlsStreamURL, + streamType: streamType, + response: response, + audioStreams: audioStreams, + subtitleStreams: subtitleStreams, + chapters: modifiedSelfItem.chapters ?? [], + selectedAudioStreamIndex: defaultAudioStream?.index ?? -1, + selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1, + subtitlesEnabled: subtitlesEnabled, + autoplayEnabled: autoplayEnabled, + overlayType: overlayType, + shouldShowPlayPreviousItem: shouldShowPlayPreviousItem, + shouldShowPlayNextItem: shouldShowPlayNextItem, + shouldShowAutoPlay: shouldShowAutoPlay, + container: currentMediaSource.container ?? "", + filename: fileName, + versionName: currentMediaSource.name + ) + + viewModels.append(videoPlayerViewModel) + } + + return viewModels } .eraseToAnyPublisher() } diff --git a/Shared/Extensions/VLCPlayer+subtitles.swift b/Shared/Extensions/VLCPlayer+subtitles.swift index fc9b4151..b3da03ee 100644 --- a/Shared/Extensions/VLCPlayer+subtitles.swift +++ b/Shared/Extensions/VLCPlayer+subtitles.swift @@ -6,6 +6,7 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // +import UIKit #if os(tvOS) import TVVLCKit #else @@ -22,4 +23,14 @@ extension VLCMediaPlayer { with: size.textRendererFontSize ) } + + /// Applies font to the player + /// + /// This is pretty hacky until VLCKit 4 has a public API to support this + func setSubtitleFont(fontName: String) { + perform( + Selector(("setTextRendererFont:")), + with: fontName + ) + } } diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index d85399b3..29c82ada 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -396,6 +396,8 @@ internal enum L10n { internal static var studio: String { return L10n.tr("Localizable", "studio") } /// Studios internal static var studios: String { return L10n.tr("Localizable", "studios") } + /// Subtitle Font + internal static var subtitleFont: String { return L10n.tr("Localizable", "subtitleFont") } /// Subtitles internal static var subtitles: String { return L10n.tr("Localizable", "subtitles") } /// Subtitle Size diff --git a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift index 29b4180c..c6a4854b 100644 --- a/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift +++ b/Shared/SwiftfinStore/SwiftfinStoreDefaults.swift @@ -8,6 +8,7 @@ import Defaults import Foundation +import UIKit extension SwiftfinStore { enum Defaults { @@ -69,6 +70,11 @@ extension Defaults.Keys { ) static let autoplayEnabled = Key("autoPlayNextItem", default: true, suite: SwiftfinStore.Defaults.generalSuite) static let resumeOffset = Key("resumeOffset", default: false, suite: SwiftfinStore.Defaults.generalSuite) + static let subtitleFontName = Key( + "subtitleFontName", + default: UIFont.systemFont(ofSize: 14).fontName, + suite: SwiftfinStore.Defaults.generalSuite + ) static let subtitleSize = Key("subtitleSize", default: .regular, suite: SwiftfinStore.Defaults.generalSuite) // Should show video player items diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 3d8e4b85..323c071c 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -221,6 +221,7 @@ 62C29EA326D1030F00C1D2E7 /* ConnectToServerCoodinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */; }; 62C29EA626D1036A00C1D2E7 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */; }; 62C29EA826D103D500C1D2E7 /* LibraryListCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */; }; + 62C83B08288C6A630004ED0C /* FontPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62C83B07288C6A630004ED0C /* FontPicker.swift */; }; 62E1DCC3273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; 62E1DCC4273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; 62E1DCC5273CE19800C9AE76 /* URLExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */; }; @@ -732,6 +733,7 @@ 62C29EA226D1030F00C1D2E7 /* ConnectToServerCoodinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectToServerCoodinator.swift; sourceTree = ""; }; 62C29EA526D1036A00C1D2E7 /* HomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = ""; }; 62C29EA726D103D500C1D2E7 /* LibraryListCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListCoordinator.swift; sourceTree = ""; }; + 62C83B07288C6A630004ED0C /* FontPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontPicker.swift; sourceTree = ""; }; 62E1DCC2273CE19800C9AE76 /* URLExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtensions.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 = ""; }; @@ -1658,6 +1660,7 @@ E1D4BF7B2719D05000A11E64 /* BasicAppSettingsView.swift */, 5338F74D263B61370014BF09 /* ConnectToServerView.swift */, 5389276D263C25100035E14B /* ContinueWatchingView.swift */, + 62C83B07288C6A630004ED0C /* FontPicker.swift */, 625CB56E2678C23300530A6E /* HomeView.swift */, E1EBCB45278BD595009FE6E9 /* ItemOverviewView.swift */, E14F7D0A26DB3714007C3AE6 /* ItemView */, @@ -2414,6 +2417,7 @@ 6220D0C026D61C5000B8E046 /* ItemCoordinator.swift in Sources */, E13DD3F227179378009D4DAF /* UserSignInCoordinator.swift in Sources */, 621338932660107500A81A2A /* StringExtensions.swift in Sources */, + 62C83B08288C6A630004ED0C /* FontPicker.swift in Sources */, 53FF7F2A263CF3F500585C35 /* LatestMediaView.swift in Sources */, E122A9132788EAAD0060FA63 /* MediaStreamExtension.swift in Sources */, E1C812C5277A90B200918266 /* URLComponentsExtensions.swift in Sources */, diff --git a/Swiftfin/Views/FontPicker.swift b/Swiftfin/Views/FontPicker.swift new file mode 100644 index 00000000..5c98354b --- /dev/null +++ b/Swiftfin/Views/FontPicker.swift @@ -0,0 +1,35 @@ +// +// 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 (c) 2022 Jellyfin & Jellyfin Contributors +// + +import Defaults +import SwiftUI +import UIKit + +struct FontPickerView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIFontPickerViewController { + let configuration = UIFontPickerViewController.Configuration() + configuration.includeFaces = true + + let fontViewController = UIFontPickerViewController(configuration: configuration) + fontViewController.delegate = context.coordinator + return fontViewController + } + + func updateUIViewController(_ uiViewController: UIFontPickerViewController, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator() + } + + class Coordinator: NSObject, UIFontPickerViewControllerDelegate { + func fontPickerViewControllerDidPickFont(_ viewController: UIFontPickerViewController) { + guard let descriptor = viewController.selectedFontDescriptor else { return } + Defaults[.subtitleFontName] = descriptor.postscriptName + } + } +} diff --git a/Swiftfin/Views/SettingsView/SettingsView.swift b/Swiftfin/Views/SettingsView/SettingsView.swift index 3e62f128..5f9696d2 100644 --- a/Swiftfin/Views/SettingsView/SettingsView.swift +++ b/Swiftfin/Views/SettingsView/SettingsView.swift @@ -48,6 +48,8 @@ struct SettingsView: View { var resumeOffset @Default(.subtitleSize) var subtitleSize + @Default(.subtitleFontName) + var subtitleFontName var body: some View { Form { @@ -185,6 +187,20 @@ struct SettingsView: View { Text(appearance.localizedName).tag(appearance.rawValue) } } + + Button { + settingsRouter.route(to: \.fontPicker) + } label: { + HStack { + L10n.subtitleFont.text + .foregroundColor(.primary) + Spacer() + Text(subtitleFontName) + .foregroundColor(.gray) + Image(systemName: "chevron.right") + } + } + Picker(L10n.subtitleSize, selection: $subtitleSize) { ForEach(SubtitleSize.allCases, id: \.self) { size in Text(size.label).tag(size.rawValue) diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift index 52dc9d4d..908222a3 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift @@ -568,6 +568,7 @@ extension VLCPlayerViewController { vlcMediaPlayer.delegate = self vlcMediaPlayer.drawable = videoContentView + vlcMediaPlayer.setSubtitleFont(fontName: Defaults[.subtitleFontName]) vlcMediaPlayer.setSubtitleSize(Defaults[.subtitleSize]) stopOverlayDismissTimer() diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index be2f9868..75d7b202 100644 Binary files a/Translations/en.lproj/Localizable.strings and b/Translations/en.lproj/Localizable.strings differ