From fa01de49a690062329cec88627e959f93c98aca9 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sat, 15 Jan 2022 22:23:14 -0700 Subject: [PATCH 1/5] ios chapters --- .../BaseItemDto+VideoPlayerViewModel.swift | 1 + .../BaseItemDtoExtensions.swift | 18 +++ .../ChapterInfoExtensions.swift | 44 ++++++++ Shared/Generated/Strings.swift | 2 + .../VideoPlayerViewModel.swift | 20 ++++ .../ConfirmCloseOverlay.swift | 0 .../SmallMenuOverlay.swift | 0 .../tvOSVLCOverlay.swift | 0 Swiftfin.xcodeproj/project.pbxproj | 53 +++++++-- .../xcshareddata/swiftpm/Package.resolved | 18 +++ .../VLCPlayerChapterOverlayView.swift | 103 ++++++++++++++++++ .../{ => Overlays}/VLCPlayerOverlayView.swift | 17 ++- .../VideoPlayer/PlayerOverlayDelegate.swift | 4 + .../VideoPlayer/VLCPlayerViewController.swift | 87 +++++++++++++++ Translations/en.lproj/Localizable.strings | Bin 11440 -> 11490 bytes 15 files changed, 356 insertions(+), 11 deletions(-) create mode 100644 Shared/Extensions/JellyfinAPIExtensions/ChapterInfoExtensions.swift rename Swiftfin tvOS/Views/VideoPlayer/{tvOSOverlay => Overlays}/ConfirmCloseOverlay.swift (100%) rename Swiftfin tvOS/Views/VideoPlayer/{tvOSOverlay => Overlays}/SmallMenuOverlay.swift (100%) rename Swiftfin tvOS/Views/VideoPlayer/{tvOSOverlay => Overlays}/tvOSVLCOverlay.swift (100%) create mode 100644 Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerChapterOverlayView.swift rename Swiftfin/Views/VideoPlayer/{ => Overlays}/VLCPlayerOverlayView.swift (96%) diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift index a619bf9c..b877197e 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDto+VideoPlayerViewModel.swift @@ -112,6 +112,7 @@ extension BaseItemDto { response: response, audioStreams: audioStreams, subtitleStreams: subtitleStreams, + chapters: modifiedSelfItem.chapters ?? [], selectedAudioStreamIndex: defaultAudioStream?.index ?? -1, selectedSubtitleStreamIndex: defaultSubtitleStream?.index ?? -1, subtitlesEnabled: subtitlesEnabled, diff --git a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift index bef3e514..8c353cb7 100644 --- a/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift +++ b/Shared/Extensions/JellyfinAPIExtensions/BaseItemDtoExtensions.swift @@ -312,4 +312,22 @@ public extension BaseItemDto { dateFormatter.dateStyle = .medium return dateFormatter.string(from: premiereDate) } + + // MARK: Chapter Images + + func getChapterImage(maxWidth: Int) -> [URL] { + guard let chapters = chapters, !chapters.isEmpty else { return [] } + + var chapterImageURLs: [URL] = [] + + for chapterIndex in 0 ..< chapters.count { + let urlString = ImageAPI.getItemImageWithRequestBuilder(itemId: id ?? "", + imageType: .chapter, + maxWidth: maxWidth, + imageIndex: chapterIndex).URLString + chapterImageURLs.append(URL(string: urlString)!) + } + + return chapterImageURLs + } } diff --git a/Shared/Extensions/JellyfinAPIExtensions/ChapterInfoExtensions.swift b/Shared/Extensions/JellyfinAPIExtensions/ChapterInfoExtensions.swift new file mode 100644 index 00000000..5d5fb50e --- /dev/null +++ b/Shared/Extensions/JellyfinAPIExtensions/ChapterInfoExtensions.swift @@ -0,0 +1,44 @@ +// +// 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 Foundation +import JellyfinAPI + +extension ChapterInfo { + + var timestampLabel: String { + let seconds = (startPositionTicks ?? 0) / 10_000_000 + return seconds.toReadableString() + } +} + +extension Int64 { + + func toReadableString() -> String { + + let s = Int(self) % 60 + let mn = (Int(self) / 60) % 60 + let hr = (Int(self) / 3600) + + var final = "" + + if hr != 0 { + final += "\(hr):" + } + + if mn != 0 { + final += String(format: "%0.2d:", mn) + } else { + final += "00:" + } + + final += String(format: "%0.2d", s) + + return final + } +} diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index e62b4404..63840d3d 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -50,6 +50,8 @@ internal enum L10n { internal static let changeServer = L10n.tr("Localizable", "changeServer") /// Channels internal static let channels = L10n.tr("Localizable", "channels") + /// Chapters + internal static let chapters = L10n.tr("Localizable", "chapters") /// Cinematic Views internal static let cinematicViews = L10n.tr("Localizable", "cinematicViews") /// Closed Captions diff --git a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift index c23d6fe0..1556f745 100644 --- a/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift +++ b/Shared/ViewModels/VideoPlayerViewModel/VideoPlayerViewModel.swift @@ -6,6 +6,7 @@ // Copyright (c) 2022 Jellyfin & Jellyfin Contributors // +import Algorithms import Combine import Defaults import Foundation @@ -110,6 +111,7 @@ final class VideoPlayerViewModel: ViewModel { let transcodedStreamURL: URL? let audioStreams: [MediaStream] let subtitleStreams: [MediaStream] + let chapters: [ChapterInfo] let overlayType: OverlayType let jumpGesturesEnabled: Bool let resumeOffset: Bool @@ -155,6 +157,22 @@ final class VideoPlayerViewModel: ViewModel { subtitleStreams.first(where: { $0.index == selectedSubtitleStreamIndex }) } + var currentChapter: ChapterInfo? { + + let chapterPairs = chapters.adjacentPairs().map { ($0, $1) } + let chapterRanges = chapterPairs.map { ($0.startPositionTicks ?? 0, ($1.startPositionTicks ?? 1) - 1) } + + for chapterRangeIndex in 0 ..< chapterRanges.count { + if chapterRanges[chapterRangeIndex].0 <= currentSecondTicks && + currentSecondTicks < chapterRanges[chapterRangeIndex].1 + { + return chapterPairs[chapterRangeIndex].0 + } + } + + return nil + } + // Necessary PassthroughSubject to capture manual scrubbing from sliders let sliderScrubbingSubject = PassthroughSubject() @@ -174,6 +192,7 @@ final class VideoPlayerViewModel: ViewModel { response: PlaybackInfoResponse, audioStreams: [MediaStream], subtitleStreams: [MediaStream], + chapters: [ChapterInfo], selectedAudioStreamIndex: Int, selectedSubtitleStreamIndex: Int, subtitlesEnabled: Bool, @@ -195,6 +214,7 @@ final class VideoPlayerViewModel: ViewModel { self.response = response self.audioStreams = audioStreams self.subtitleStreams = subtitleStreams + self.chapters = chapters self.selectedAudioStreamIndex = selectedAudioStreamIndex self.selectedSubtitleStreamIndex = selectedSubtitleStreamIndex self.subtitlesEnabled = subtitlesEnabled diff --git a/Swiftfin tvOS/Views/VideoPlayer/tvOSOverlay/ConfirmCloseOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift similarity index 100% rename from Swiftfin tvOS/Views/VideoPlayer/tvOSOverlay/ConfirmCloseOverlay.swift rename to Swiftfin tvOS/Views/VideoPlayer/Overlays/ConfirmCloseOverlay.swift diff --git a/Swiftfin tvOS/Views/VideoPlayer/tvOSOverlay/SmallMenuOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift similarity index 100% rename from Swiftfin tvOS/Views/VideoPlayer/tvOSOverlay/SmallMenuOverlay.swift rename to Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift diff --git a/Swiftfin tvOS/Views/VideoPlayer/tvOSOverlay/tvOSVLCOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift similarity index 100% rename from Swiftfin tvOS/Views/VideoPlayer/tvOSOverlay/tvOSVLCOverlay.swift rename to Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index 827d3b35..b7273063 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -239,6 +239,10 @@ C4E5081B2703F82A0045C9AB /* LibraryListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E508172703E8190045C9AB /* LibraryListView.swift */; }; C4E5081D2703F8370045C9AB /* LibrarySearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */; }; C4E52305272CE68800654268 /* LiveTVChannelItemElement.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */; }; + E1002B5F2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */; }; + E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; }; + E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; }; + E1002B682793CFBA00E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B672793CFBA00E47059 /* Algorithms */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */; }; E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */; }; @@ -648,6 +652,8 @@ C4E508172703E8190045C9AB /* LibraryListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryListView.swift; sourceTree = ""; }; C4E5081C2703F8370045C9AB /* LibrarySearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibrarySearchView.swift; sourceTree = ""; }; C4E52304272CE68800654268 /* LiveTVChannelItemElement.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTVChannelItemElement.swift; sourceTree = ""; }; + E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VLCPlayerChapterOverlayView.swift; sourceTree = ""; }; + E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterInfoExtensions.swift; sourceTree = ""; }; E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPlayButtonRowView.swift; sourceTree = ""; }; E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UICinematicBackgroundView.swift; sourceTree = ""; }; E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CinematicBackgroundView.swift; sourceTree = ""; }; @@ -798,6 +804,7 @@ E13DD3D327168E65009D4DAF /* Defaults in Frameworks */, E1361DA7278FA7A300BEC523 /* NukeUI in Frameworks */, 53649AAD269CFAEA00A2D8B7 /* Puppy in Frameworks */, + E1002B682793CFBA00E47059 /* Algorithms in Frameworks */, E10EAA4D277BB716000269ED /* Sliders in Frameworks */, 62C29E9C26D0FE4200C1D2E7 /* Stinsen in Frameworks */, E1A99999271A3429008E78C0 /* SwiftUICollection in Frameworks */, @@ -845,7 +852,7 @@ children = ( E1C812C6277AE40900918266 /* PlayerOverlayDelegate.swift */, E178859C2780F5300094FBCF /* tvOSSLider */, - E17885A7278130690094FBCF /* tvOSOverlay */, + E17885A7278130690094FBCF /* Overlays */, E1C812C8277AE40900918266 /* VideoPlayerView.swift */, E1384943278036C70024FB48 /* VLCPlayerViewController.swift */, ); @@ -1343,6 +1350,15 @@ path = Pods; sourceTree = ""; }; + E1002B692793E12E00E47059 /* Overlays */ = { + isa = PBXGroup; + children = ( + E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */, + E1002B5E2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift */, + ); + path = Overlays; + sourceTree = ""; + }; E103A6A1278A7EB500820EC7 /* HomeCinematicView */ = { isa = PBXGroup; children = ( @@ -1500,14 +1516,14 @@ path = tvOSSLider; sourceTree = ""; }; - E17885A7278130690094FBCF /* tvOSOverlay */ = { + E17885A7278130690094FBCF /* Overlays */ = { isa = PBXGroup; children = ( E1E5D552278419D900692DFE /* ConfirmCloseOverlay.swift */, E1FA2F7327818A8800B4C270 /* SmallMenuOverlay.swift */, E178859F2780F55C0094FBCF /* tvOSVLCOverlay.swift */, ); - path = tvOSOverlay; + path = Overlays; sourceTree = ""; }; E18845FA26DEACBE00B0C5B7 /* Portrait */ = { @@ -1543,7 +1559,7 @@ isa = PBXGroup; children = ( E1C812B5277A8E5D00918266 /* PlayerOverlayDelegate.swift */, - E1C812BB277A8E5D00918266 /* VLCPlayerOverlayView.swift */, + E1002B692793E12E00E47059 /* Overlays */, E1C812B8277A8E5D00918266 /* VLCPlayerView.swift */, E1C812B6277A8E5D00918266 /* VLCPlayerViewController.swift */, ); @@ -1567,6 +1583,7 @@ E10EAA52277BBD17000269ED /* BaseItemDto+VideoPlayerViewModel.swift */, E1AD104C26D96CE3003E4A08 /* BaseItemDtoExtensions.swift */, 5364F454266CA0DC0026ECBA /* BaseItemPersonExtensions.swift */, + E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */, E11B1B6B2718CD68006DA3E8 /* JellyfinAPIError.swift */, E122A9122788EAAD0060FA63 /* MediaStreamExtension.swift */, E1AD105E26D9ADDD003E4A08 /* NameGUIDPairExtensions.swift */, @@ -1754,6 +1771,7 @@ E10EAA4C277BB716000269ED /* Sliders */, E1AE8E7B2789135A00FBDDAA /* Nuke */, E1361DA6278FA7A300BEC523 /* NukeUI */, + E1002B672793CFBA00E47059 /* Algorithms */, ); productName = JellyfinPlayer; productReference = 5377CBF1263B596A003A4E83 /* Swiftfin iOS.app */; @@ -1847,14 +1865,15 @@ E10EAA4B277BB716000269ED /* XCRemoteSwiftPackageReference "swiftui-sliders" */, E1AE8E7A2789135A00FBDDAA /* XCRemoteSwiftPackageReference "Nuke" */, E1361DA5278FA7A300BEC523 /* XCRemoteSwiftPackageReference "NukeUI" */, + E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */, ); productRefGroup = 5377CBF2263B596A003A4E83 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 5377CBF0263B596A003A4E83 /* Swiftfin iOS */, - 5358705F2669D21600D05A09 /* Swiftfin tvOS */, 628B951F2670CABD0091AF3B /* Swiftfin Widget */, + 5358705F2669D21600D05A09 /* Swiftfin tvOS */, ); }; /* End PBXProject section */ @@ -2138,6 +2157,7 @@ E178859E2780F53B0094FBCF /* SliderView.swift in Sources */, 536D3D7F267BDF100004248C /* LatestMediaView.swift in Sources */, E1384944278036C70024FB48 /* VLCPlayerViewController.swift in Sources */, + E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, 091B5A8E268315D400D78B61 /* UDPBroadCastConnection.swift in Sources */, E103A6A5278A82E500820EC7 /* HomeCinematicView.swift in Sources */, E10EAA50277BBCC4000269ED /* CGSizeExtensions.swift in Sources */, @@ -2339,6 +2359,7 @@ E1C812CE277AE43100918266 /* VideoPlayerViewModel.swift in Sources */, E10D87DA2784E4F100BD264C /* ItemViewDetailsView.swift in Sources */, E1C812C3277A8E5D00918266 /* VLCPlayerOverlayView.swift in Sources */, + E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */, E188460026DECB9E00B0C5B7 /* ItemLandscapeTopBarView.swift in Sources */, 091B5A8B2683142E00D78B61 /* UDPBroadCastConnection.swift in Sources */, 6267B3D626710B8900A7371D /* CollectionExtensions.swift in Sources */, @@ -2358,6 +2379,7 @@ E1D4BF842719D25A00A11E64 /* TrackLanguage.swift in Sources */, E14F7D0726DB36EF007C3AE6 /* ItemPortraitMainView.swift in Sources */, E1AD106226D9B7CD003E4A08 /* ItemPortraitHeaderOverlayView.swift in Sources */, + E1002B5F2793C3BE00E47059 /* VLCPlayerChapterOverlayView.swift in Sources */, 53E4E649263F725B00F67C6B /* MultiSelectorView.swift in Sources */, 6220D0C626D62D8700B8E046 /* iOSVideoPlayerCoordinator.swift in Sources */, E11B1B6C2718CD68006DA3E8 /* JellyfinAPIError.swift in Sources */, @@ -2792,7 +2814,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2829,7 +2851,7 @@ CURRENT_PROJECT_VERSION = 66; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2860,7 +2882,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2887,7 +2909,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = TY84JMYEFE; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2995,6 +3017,14 @@ kind = branch; }; }; + E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/apple/swift-algorithms.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/jellyfin/jellyfin-sdk-swift"; @@ -3117,6 +3147,11 @@ package = 62C29E9A26D0FE4100C1D2E7 /* XCRemoteSwiftPackageReference "stinsen" */; productName = Stinsen; }; + E1002B672793CFBA00E47059 /* Algorithms */ = { + isa = XCSwiftPackageProductDependency; + package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */; + productName = Algorithms; + }; E10EAA44277BB646000269ED /* JellyfinAPI */ = { isa = XCSwiftPackageProductDependency; package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; diff --git a/Swiftfin.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Swiftfin.xcworkspace/xcshareddata/swiftpm/Package.resolved index 00182b81..8e52e799 100644 --- a/Swiftfin.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Swiftfin.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -109,6 +109,15 @@ "version": "2.0.3" } }, + { + "package": "swift-algorithms", + "repositoryURL": "https://github.com/apple/swift-algorithms.git", + "state": { + "branch": null, + "revision": "b14b7f4c528c942f121c8b860b9410b2bf57825e", + "version": "1.0.0" + } + }, { "package": "swift-log", "repositoryURL": "https://github.com/apple/swift-log.git", @@ -118,6 +127,15 @@ "version": "1.4.2" } }, + { + "package": "swift-numerics", + "repositoryURL": "https://github.com/apple/swift-numerics", + "state": { + "branch": null, + "revision": "0a5bc04095a675662cf24757cc0640aa2204253b", + "version": "1.0.2" + } + }, { "package": "Introspect", "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect", diff --git a/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerChapterOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerChapterOverlayView.swift new file mode 100644 index 00000000..f0887f6b --- /dev/null +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerChapterOverlayView.swift @@ -0,0 +1,103 @@ +// +// 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 JellyfinAPI +import SwiftUI + +struct VLCPlayerChapterOverlayView: View { + + @ObservedObject + var viewModel: VideoPlayerViewModel + private let chapterImages: [URL] + + init(viewModel: VideoPlayerViewModel) { + self.viewModel = viewModel + self.chapterImages = viewModel.item.getChapterImage(maxWidth: 500) + } + + @ViewBuilder + private var mainBody: some View { + ZStack(alignment: .bottom) { + + LinearGradient(gradient: Gradient(colors: [.clear, .black.opacity(0.8), .black]), + startPoint: .top, + endPoint: .bottom) + .ignoresSafeArea() + .frame(height: 300) + + VStack { + Spacer() + + VStack(alignment: .leading, spacing: 0) { + + L10n.chapters.text + .font(.title3) + .fontWeight(.bold) + .padding(.leading) + + ScrollView(.horizontal, showsIndicators: false) { + ScrollViewReader { reader in + HStack { + ForEach(0 ..< viewModel.chapters.count) { chapterIndex in + VStack(alignment: .leading) { + Button { + viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex]) + } label: { + ImageView(src: chapterImages[chapterIndex]) + .cornerRadius(10) + .frame(width: 150, height: 100) + .overlay { + if viewModel.chapters[chapterIndex] == viewModel.currentChapter { + RoundedRectangle(cornerRadius: 6) + .stroke(Color.jellyfinPurple, lineWidth: 4) + } + } + } + + VStack(alignment: .leading, spacing: 5) { + + Text(viewModel.chapters[chapterIndex].name ?? L10n.noTitle) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.white) + + Text(viewModel.chapters[chapterIndex].timestampLabel) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(Color(UIColor.systemBlue)) + .padding(.vertical, 2) + .padding(.horizontal, 4) + .background { + Color(UIColor.darkGray).opacity(0.2).cornerRadius(4) + } + } + } + .id(viewModel.chapters[chapterIndex]) + } + } + .padding(.top) + .onAppear { + reader.scrollTo(viewModel.currentChapter) + } + } + } + } + .padding(.bottom) + } + } + } + + var body: some View { + mainBody + .edgesIgnoringSafeArea(.bottom) + .contentShape(Rectangle()) + .onTapGesture { + viewModel.playerOverlayDelegate?.didSelectChapters() + } + } +} diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerOverlayView.swift b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift similarity index 96% rename from Swiftfin/Views/VideoPlayer/VLCPlayerOverlayView.swift rename to Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift index 9a969078..2925ea77 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerOverlayView.swift +++ b/Swiftfin/Views/VideoPlayer/Overlays/VLCPlayerOverlayView.swift @@ -76,7 +76,8 @@ struct VLCPlayerOverlayView: View { } Text(viewModel.title) - .font(.system(size: 28, weight: .regular, design: .default)) + .font(.title3) + .fontWeight(.bold) .alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in context[.leading] } @@ -193,6 +194,17 @@ struct VLCPlayerOverlayView: View { } } + if !viewModel.chapters.isEmpty { + Button { + viewModel.playerOverlayDelegate?.didSelectChapters() + } label: { + HStack { + Image(systemName: "list.dash") + L10n.chapters.text + } + } + } + if viewModel.shouldShowJumpButtonsInOverlayMenu { Menu { ForEach(VideoPlayerJumpLength.allCases, id: \.self) { forwardLength in @@ -247,7 +259,7 @@ struct VLCPlayerOverlayView: View { .alignmentGuide(.EpisodeSeriesAlignmentGuide) { context in context[.leading] } - .offset(y: -10) + .offset(y: -20) } } } @@ -389,6 +401,7 @@ struct VLCPlayerCompactOverlayView_Previews: PreviewProvider { response: PlaybackInfoResponse(), audioStreams: [MediaStream(displayTitle: "English", index: -1)], subtitleStreams: [MediaStream(displayTitle: "None", index: -1)], + chapters: [], selectedAudioStreamIndex: -1, selectedSubtitleStreamIndex: -1, subtitlesEnabled: true, diff --git a/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift b/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift index 6ea4bc5b..f0e49c37 100644 --- a/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift +++ b/Swiftfin/Views/VideoPlayer/PlayerOverlayDelegate.swift @@ -7,6 +7,7 @@ // import Foundation +import JellyfinAPI protocol PlayerOverlayDelegate { @@ -28,4 +29,7 @@ protocol PlayerOverlayDelegate { func didSelectPlayPreviousItem() func didSelectPlayNextItem() + + func didSelectChapters() + func didSelectChapter(_ chapter: ChapterInfo) } diff --git a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift index 5af087db..dfcf1611 100644 --- a/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin/Views/VideoPlayer/VLCPlayerViewController.swift @@ -37,9 +37,14 @@ class VLCPlayerViewController: UIViewController { currentOverlayHostingController?.view.alpha ?? 0 > 0 } + private var displayingChapterOverlay: Bool { + currentChapterOverlayHostingController?.view.alpha ?? 0 > 0 + } + private lazy var videoContentView = makeVideoContentView() private lazy var mainGestureView = makeTapGestureView() private var currentOverlayHostingController: UIHostingController? + private var currentChapterOverlayHostingController: UIHostingController? private var currentJumpBackwardOverlayView: UIImageView? private var currentJumpForwardOverlayView: UIImageView? @@ -119,6 +124,8 @@ class VLCPlayerViewController: UIViewController { @objc private func appWillResignActive() { + hideChaptersOverlay() + showOverlay() stopOverlayDismissTimer() @@ -225,6 +232,38 @@ class VLCPlayerViewController: UIViewController { self.currentOverlayHostingController = newOverlayHostingController + if let currentChapterOverlayHostingController = currentChapterOverlayHostingController { + UIView.animate(withDuration: 0.5) { + currentChapterOverlayHostingController.view.alpha = 0 + } completion: { _ in + currentChapterOverlayHostingController.view.isHidden = true + + currentChapterOverlayHostingController.view.removeFromSuperview() + currentChapterOverlayHostingController.removeFromParent() + } + } + + let newChapterOverlayView = VLCPlayerChapterOverlayView(viewModel: viewModel) + let newChapterOverlayHostingController = UIHostingController(rootView: newChapterOverlayView) + + newChapterOverlayHostingController.view.translatesAutoresizingMaskIntoConstraints = false + newChapterOverlayHostingController.view.backgroundColor = UIColor.clear + + newChapterOverlayHostingController.view.alpha = 0 + + addChild(newChapterOverlayHostingController) + view.addSubview(newChapterOverlayHostingController.view) + newChapterOverlayHostingController.didMove(toParent: self) + + NSLayoutConstraint.activate([ + newChapterOverlayHostingController.view.topAnchor.constraint(equalTo: videoContentView.topAnchor), + newChapterOverlayHostingController.view.bottomAnchor.constraint(equalTo: videoContentView.bottomAnchor), + newChapterOverlayHostingController.view.leftAnchor.constraint(equalTo: videoContentView.leftAnchor), + newChapterOverlayHostingController.view.rightAnchor.constraint(equalTo: videoContentView.rightAnchor), + ]) + + self.currentChapterOverlayHostingController = newChapterOverlayHostingController + // There is a weird behavior when after setting the new overlays that the navigation bar pops up, re-hide it self.navigationController?.isNavigationBarHidden = true } @@ -514,6 +553,31 @@ extension VLCPlayerViewController { } } +// MARK: Hide/Show Chapters + +extension VLCPlayerViewController { + + private func showChaptersOverlay() { + guard let overlayHostingController = currentChapterOverlayHostingController else { return } + + guard overlayHostingController.view.alpha != 1 else { return } + + UIView.animate(withDuration: 0.2) { + overlayHostingController.view.alpha = 1 + } + } + + private func hideChaptersOverlay() { + guard let overlayHostingController = currentChapterOverlayHostingController else { return } + + guard overlayHostingController.view.alpha != 0 else { return } + + UIView.animate(withDuration: 0.2) { + overlayHostingController.view.alpha = 0 + } + } +} + // MARK: OverlayTimer extension VLCPlayerViewController { @@ -724,4 +788,27 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { startPlayback() } } + + func didSelectChapters() { + if displayingChapterOverlay { + hideChaptersOverlay() + } else { + hideOverlay() + showChaptersOverlay() + } + } + + func didSelectChapter(_ chapter: ChapterInfo) { + let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000) + let chapterSeconds = Double((chapter.startPositionTicks ?? 0) / 10_000_000) + let newPositionOffset = chapterSeconds - videoPosition + + if newPositionOffset > 0 { + vlcMediaPlayer.jumpForward(Int32(newPositionOffset)) + } else { + vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset))) + } + + viewModel.sendProgressReport() + } } diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index cc09b41e36cdfbc25e4b2280cd27dce5b40fde0f..efcdd6f71412734f36604fc6a36540f4a447de06 100644 GIT binary patch delta 44 vcmdlG`6zP31|3c%hGd2ehD3&f$%U-KV$KjYLkUAFLlHwUgA#)^0~Z4TB~A)! delta 7 OcmaD9xgm1H1|0wpy93bx From 4a58ba21298231f0f450bc5bf09710444a674334 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sat, 15 Jan 2022 22:25:17 -0700 Subject: [PATCH 2/5] Update project.pbxproj --- Swiftfin.xcodeproj/project.pbxproj | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index b7273063..cd468747 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -243,6 +243,7 @@ E1002B642793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; }; E1002B652793CEE800E47059 /* ChapterInfoExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1002B632793CEE700E47059 /* ChapterInfoExtensions.swift */; }; E1002B682793CFBA00E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B672793CFBA00E47059 /* Algorithms */; }; + E1002B6B2793E36600E47059 /* Algorithms in Frameworks */ = {isa = PBXBuildFile; productRef = E1002B6A2793E36600E47059 /* Algorithms */; }; E100720726BDABC100CE3E31 /* MediaPlayButtonRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E100720626BDABC100CE3E31 /* MediaPlayButtonRowView.swift */; }; E103A6A0278A7E4500820EC7 /* UICinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A69F278A7E4500820EC7 /* UICinematicBackgroundView.swift */; }; E103A6A3278A7EC400820EC7 /* CinematicBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E103A6A2278A7EC400820EC7 /* CinematicBackgroundView.swift */; }; @@ -783,6 +784,7 @@ 53649AAF269CFAF600A2D8B7 /* Puppy in Frameworks */, E11D83AF278FA998006E9776 /* NukeUI in Frameworks */, E1218C9E271A2CD600EA0737 /* CombineExt in Frameworks */, + E1002B6B2793E36600E47059 /* Algorithms in Frameworks */, 6220D0C926D63F3700B8E046 /* Stinsen in Frameworks */, 535870912669D7A800D05A09 /* Introspect in Frameworks */, 536D3D84267BEA550004248C /* ParallaxView in Frameworks */, @@ -1732,6 +1734,7 @@ E178857C278037FD0094FBCF /* JellyfinAPI */, E1AE8E7D2789136D00FBDDAA /* Nuke */, E11D83AE278FA998006E9776 /* NukeUI */, + E1002B6A2793E36600E47059 /* Algorithms */, ); productName = "JellyfinPlayer tvOS"; productReference = 535870602669D21600D05A09 /* Swiftfin tvOS.app */; @@ -3152,6 +3155,11 @@ package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */; productName = Algorithms; }; + E1002B6A2793E36600E47059 /* Algorithms */ = { + isa = XCSwiftPackageProductDependency; + package = E1002B662793CFBA00E47059 /* XCRemoteSwiftPackageReference "swift-algorithms" */; + productName = Algorithms; + }; E10EAA44277BB646000269ED /* JellyfinAPI */ = { isa = XCSwiftPackageProductDependency; package = E10EAA43277BB646000269ED /* XCRemoteSwiftPackageReference "jellyfin-sdk-swift" */; From aa5bdca9176ea5f00e11ca8568ed00ec5fbe5290 Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sat, 15 Jan 2022 22:50:20 -0700 Subject: [PATCH 3/5] tvos chapters --- .../Overlays/SmallMenuOverlay.swift | 238 +++++++++++++----- .../VideoPlayer/Overlays/tvOSVLCOverlay.swift | 1 + .../VideoPlayer/PlayerOverlayDelegate.swift | 3 + .../VideoPlayer/VLCPlayerViewController.swift | 14 ++ Swiftfin.xcodeproj/project.pbxproj | 8 +- 5 files changed, 194 insertions(+), 70 deletions(-) diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift index 0797f0e1..010cf779 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/SmallMenuOverlay.swift @@ -16,6 +16,7 @@ struct SmallMediaStreamSelectionView: View { case subtitles case audio case playbackSpeed + case chapters } enum MediaSection: Hashable { @@ -25,9 +26,12 @@ struct SmallMediaStreamSelectionView: View { @ObservedObject var viewModel: VideoPlayerViewModel + private let chapterImages: [URL] @State private var updateFocusedLayer: Layer = .subtitles + @State + private var lastFocusedLayer: Layer = .subtitles @FocusState private var subtitlesFocused: Bool @@ -36,6 +40,8 @@ struct SmallMediaStreamSelectionView: View { @FocusState private var playbackSpeedFocused: Bool @FocusState + private var chaptersFocused: Bool + @FocusState private var focusedSection: MediaSection? @FocusState private var focusedLayer: Layer? { @@ -48,8 +54,10 @@ struct SmallMediaStreamSelectionView: View { } } - @State - private var lastFocusedLayer: Layer = .subtitles + init(viewModel: VideoPlayerViewModel) { + self.viewModel = viewModel + self.chapterImages = viewModel.item.getChapterImage(maxWidth: 500) + } var body: some View { ZStack(alignment: .bottom) { @@ -161,6 +169,40 @@ struct SmallMediaStreamSelectionView: View { } } + // MARK: Chapters Header + + if !viewModel.chapters.isEmpty { + Button { + updateFocusedLayer = .chapters + focusedLayer = .chapters + } label: { + if updateFocusedLayer == .chapters { + HStack(spacing: 15) { + Image(systemName: "list.dash") + L10n.chapters.text + } + .padding() + .background(Color.white) + .foregroundColor(.black) + } else { + HStack(spacing: 15) { + Image(systemName: "list.dash") + L10n.chapters.text + } + .padding() + } + } + .buttonStyle(PlainButtonStyle()) + .background(Color.clear) + .focused($focusedLayer, equals: .chapters) + .focused($chaptersFocused) + .onChange(of: chaptersFocused) { isFocused in + if isFocused { + focusedLayer = .chapters + } + } + } + Spacer() } .padding() @@ -181,80 +223,144 @@ struct SmallMediaStreamSelectionView: View { if updateFocusedLayer == .subtitles && lastFocusedLayer == .subtitles { // MARK: Subtitles - ScrollView(.horizontal) { - HStack { - if viewModel.subtitleStreams.isEmpty { - Button {} label: { - L10n.none.text - } - } else { - ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in - Button { - viewModel.selectedSubtitleStreamIndex = subtitleStream.index ?? -1 - } label: { - if subtitleStream.index == viewModel.selectedSubtitleStreamIndex { - Label(subtitleStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") - } else { - Text(subtitleStream.displayTitle ?? L10n.noTitle) - } - } - } - } - } - .padding(.vertical) - .focusSection() - .focused($focusedSection, equals: .items) - } + subtitleMenuView } else if updateFocusedLayer == .audio && lastFocusedLayer == .audio { // MARK: Audio - ScrollView(.horizontal) { - HStack { - if viewModel.audioStreams.isEmpty { - Button {} label: { - Text("None") - } - } else { - ForEach(viewModel.audioStreams, id: \.self) { audioStream in - Button { - viewModel.selectedAudioStreamIndex = audioStream.index ?? -1 - } label: { - if audioStream.index == viewModel.selectedAudioStreamIndex { - Label(audioStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") - } else { - Text(audioStream.displayTitle ?? L10n.noTitle) - } - } - } - } - } - .padding(.vertical) - .focusSection() - .focused($focusedSection, equals: .items) - } + audioMenuView } else if updateFocusedLayer == .playbackSpeed && lastFocusedLayer == .playbackSpeed { - // MARK: Rates + // MARK: Playback Speed - ScrollView(.horizontal) { - HStack { - ForEach(PlaybackSpeed.allCases, id: \.self) { playbackSpeed in - Button { - viewModel.playbackSpeed = playbackSpeed - } label: { - if playbackSpeed == viewModel.playbackSpeed { - Label(playbackSpeed.displayTitle, systemImage: "checkmark") - } else { - Text(playbackSpeed.displayTitle) - } - } + playbackSpeedMenuView + } else if updateFocusedLayer == .chapters && lastFocusedLayer == .chapters { + // MARK: Chapters + + chaptersMenuView + } + } + } + } + + @ViewBuilder + private var subtitleMenuView: some View { + ScrollView(.horizontal) { + HStack { + if viewModel.subtitleStreams.isEmpty { + Button {} label: { + L10n.none.text + } + } else { + ForEach(viewModel.subtitleStreams, id: \.self) { subtitleStream in + Button { + viewModel.selectedSubtitleStreamIndex = subtitleStream.index ?? -1 + } label: { + if subtitleStream.index == viewModel.selectedSubtitleStreamIndex { + Label(subtitleStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") + } else { + Text(subtitleStream.displayTitle ?? L10n.noTitle) } } - .padding(.vertical) - .focusSection() - .focused($focusedSection, equals: .items) } } } + .padding(.vertical) + .focusSection() + .focused($focusedSection, equals: .items) + } + } + + @ViewBuilder + private var audioMenuView: some View { + ScrollView(.horizontal) { + HStack { + if viewModel.audioStreams.isEmpty { + Button {} label: { + Text("None") + } + } else { + ForEach(viewModel.audioStreams, id: \.self) { audioStream in + Button { + viewModel.selectedAudioStreamIndex = audioStream.index ?? -1 + } label: { + if audioStream.index == viewModel.selectedAudioStreamIndex { + Label(audioStream.displayTitle ?? L10n.noTitle, systemImage: "checkmark") + } else { + Text(audioStream.displayTitle ?? L10n.noTitle) + } + } + } + } + } + .padding(.vertical) + .focusSection() + .focused($focusedSection, equals: .items) + } + } + + @ViewBuilder + private var playbackSpeedMenuView: some View { + ScrollView(.horizontal) { + HStack { + ForEach(PlaybackSpeed.allCases, id: \.self) { playbackSpeed in + Button { + viewModel.playbackSpeed = playbackSpeed + } label: { + if playbackSpeed == viewModel.playbackSpeed { + Label(playbackSpeed.displayTitle, systemImage: "checkmark") + } else { + Text(playbackSpeed.displayTitle) + } + } + } + } + .padding(.vertical) + .focusSection() + .focused($focusedSection, equals: .items) + } + } + + @ViewBuilder + private var chaptersMenuView: some View { + ScrollView(.horizontal, showsIndicators: false) { + ScrollViewReader { reader in + HStack { + ForEach(0 ..< viewModel.chapters.count) { chapterIndex in + VStack(alignment: .leading) { + Button { + viewModel.playerOverlayDelegate?.didSelectChapter(viewModel.chapters[chapterIndex]) + } label: { + ImageView(src: chapterImages[chapterIndex]) + .cornerRadius(10) + .frame(width: 350, height: 210) + } + .buttonStyle(CardButtonStyle()) + + VStack(alignment: .leading, spacing: 5) { + + Text(viewModel.chapters[chapterIndex].name ?? L10n.noTitle) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(.white) + + Text(viewModel.chapters[chapterIndex].timestampLabel) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(Color(UIColor.systemBlue)) + .padding(.vertical, 2) + .padding(.horizontal, 4) + .background { + Color(UIColor.darkGray).opacity(0.2).cornerRadius(4) + } + } + } + .id(viewModel.chapters[chapterIndex]) + } + } + .padding(.top) + .onAppear { + reader.scrollTo(viewModel.currentChapter) + } + } } } } diff --git a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift index 64eac230..12d83836 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/Overlays/tvOSVLCOverlay.swift @@ -147,6 +147,7 @@ struct tvOSVLCOverlay_Previews: PreviewProvider { response: PlaybackInfoResponse(), audioStreams: [MediaStream(displayTitle: "English", index: -1)], subtitleStreams: [MediaStream(displayTitle: "None", index: -1)], + chapters: [], selectedAudioStreamIndex: -1, selectedSubtitleStreamIndex: -1, subtitlesEnabled: true, diff --git a/Swiftfin tvOS/Views/VideoPlayer/PlayerOverlayDelegate.swift b/Swiftfin tvOS/Views/VideoPlayer/PlayerOverlayDelegate.swift index 4ed7ec35..ac507f02 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/PlayerOverlayDelegate.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/PlayerOverlayDelegate.swift @@ -7,6 +7,7 @@ // import Foundation +import JellyfinAPI protocol PlayerOverlayDelegate { @@ -27,4 +28,6 @@ protocol PlayerOverlayDelegate { func didSelectPlayPreviousItem() func didSelectPlayNextItem() + + func didSelectChapter(_ chapter: ChapterInfo) } diff --git a/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift b/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift index 07a82b2a..1ba5b04a 100644 --- a/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift +++ b/Swiftfin tvOS/Views/VideoPlayer/VLCPlayerViewController.swift @@ -881,4 +881,18 @@ extension VLCPlayerViewController: PlayerOverlayDelegate { startPlayback() } } + + func didSelectChapter(_ chapter: ChapterInfo) { + let videoPosition = Double(vlcMediaPlayer.time.intValue / 1000) + let chapterSeconds = Double((chapter.startPositionTicks ?? 0) / 10_000_000) + let newPositionOffset = chapterSeconds - videoPosition + + if newPositionOffset > 0 { + vlcMediaPlayer.jumpForward(Int32(newPositionOffset)) + } else { + vlcMediaPlayer.jumpBackward(Int32(abs(newPositionOffset))) + } + + viewModel.sendProgressReport() + } } diff --git a/Swiftfin.xcodeproj/project.pbxproj b/Swiftfin.xcodeproj/project.pbxproj index cd468747..ba3fb503 100644 --- a/Swiftfin.xcodeproj/project.pbxproj +++ b/Swiftfin.xcodeproj/project.pbxproj @@ -2817,7 +2817,7 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2854,7 +2854,7 @@ CURRENT_PROJECT_VERSION = 66; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; ENABLE_BITCODE = NO; ENABLE_PREVIEWS = YES; EXCLUDED_ARCHS = ""; @@ -2885,7 +2885,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( @@ -2912,7 +2912,7 @@ ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 66; - DEVELOPMENT_TEAM = TY84JMYEFE; + DEVELOPMENT_TEAM = ""; INFOPLIST_FILE = WidgetExtension/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( From 4704a54d895c0e11bb0bfa17c98c95614016999c Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sat, 15 Jan 2022 22:55:23 -0700 Subject: [PATCH 4/5] Update Strings.swift --- Shared/Generated/Strings.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Shared/Generated/Strings.swift b/Shared/Generated/Strings.swift index e7a02002..efa04c1b 100644 --- a/Shared/Generated/Strings.swift +++ b/Shared/Generated/Strings.swift @@ -48,10 +48,10 @@ internal enum L10n { internal static var castAndCrew: String { return L10n.tr("Localizable", "castAndCrew") } /// Change Server internal static var changeServer: String { return L10n.tr("Localizable", "changeServer") } - /// Chapters - internal static let chaptersString { return L10n.tr("Localizable", "chapters") } /// Channels internal static var channels: String { return L10n.tr("Localizable", "channels") } + /// Chapters + internal static var chapters: String { return L10n.tr("Localizable", "chapters") } /// Cinematic Views internal static var cinematicViews: String { return L10n.tr("Localizable", "cinematicViews") } /// Closed Captions From df304613fafee941ca5b9a1181e783842c1c835a Mon Sep 17 00:00:00 2001 From: Ethan Pippin Date: Sun, 16 Jan 2022 19:15:45 -0700 Subject: [PATCH 5/5] Update Localizable.strings --- Translations/en.lproj/Localizable.strings | Bin 11526 -> 11576 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/Translations/en.lproj/Localizable.strings b/Translations/en.lproj/Localizable.strings index a1457c89db41a0995365734b460f4acc8c2524ba..7a7a17a81feb66e1dc6d43576b92cd0601eb777a 100644 GIT binary patch delta 44 vcmZpR+7Y#ZO_x)NA(