implement remove from resume and play from beginning

This commit is contained in:
Ethan Pippin 2022-01-12 13:14:39 -07:00
parent 46273cf6a5
commit b38d788e34
15 changed files with 156 additions and 68 deletions

View File

@ -222,6 +222,8 @@ internal enum L10n {
internal static let playbackSettings = L10n.tr("Localizable", "playbackSettings")
/// Playback Speed
internal static let playbackSpeed = L10n.tr("Localizable", "playbackSpeed")
/// Play From Beginning
internal static let playFromBeginning = L10n.tr("Localizable", "playFromBeginning")
/// Play Next
internal static let playNext = L10n.tr("Localizable", "playNext")
/// Play Next Item
@ -250,6 +252,8 @@ internal enum L10n {
internal static let remove = L10n.tr("Localizable", "remove")
/// Remove All Users
internal static let removeAllUsers = L10n.tr("Localizable", "removeAllUsers")
/// Remove From Resume
internal static let removeFromResume = L10n.tr("Localizable", "removeFromResume")
/// Reset
internal static let reset = L10n.tr("Localizable", "reset")
/// Reset App Settings

View File

@ -70,19 +70,3 @@ extension EpisodesRowManager {
}
}
}
final class SingleSeasonEpisodesRowViewModel: ViewModel {
// TODO: Protocol these viewmodels for generalization instead of Season
@ObservedObject
var seasonItemViewModel: SeasonItemViewModel
@Published
var episodes: [BaseItemDto]
init(seasonItemViewModel: SeasonItemViewModel) {
self.seasonItemViewModel = seasonItemViewModel
self.episodes = seasonItemViewModel.episodes
super.init()
}
}

View File

@ -184,6 +184,20 @@ final class HomeViewModel: ViewModel {
})
.store(in: &cancellables)
}
func removeItemFromResume(_ item: BaseItemDto) {
guard let itemID = item.id, resumeItems.contains(where: { $0.id == itemID }) else { return }
PlaystateAPI.markUnplayedItem(userId: SessionManager.main.currentLogin.user.id,
itemId: item.id!)
.sink(receiveCompletion: { [weak self] completion in
self?.handleAPIRequestError(completion: completion)
}, receiveValue: { _ in
self.refreshResumeItems()
self.refreshNextUpItems()
})
.store(in: &cancellables)
}
// MARK: Next Up Items

View File

@ -24,7 +24,7 @@ final class CollectionItemViewModel: ItemViewModel {
private func getCollectionItems() {
ItemsAPI.getItems(userId: SessionManager.main.currentLogin.user.id,
parentId: item.id,
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
fields: [.primaryImageAspectRatio, .seriesPrimaryImage, .seasonUserData, .overview, .genres, .people])
.trackActivity(loading)
.sink { [weak self] completion in
self?.handleAPIRequestError(completion: completion)

View File

@ -100,7 +100,7 @@ final class VideoPlayerViewModel: ViewModel {
// MARK: General
let item: BaseItemDto
private(set) var item: BaseItemDto
let title: String
let subtitle: String?
let streamURL: URL
@ -235,6 +235,22 @@ final class VideoPlayerViewModel: ViewModel {
}
}
// MARK: Injected Values
extension VideoPlayerViewModel {
// Injects custom values that override certain settings
func injectCustomValues(startFromBeginning: Bool = false) {
if startFromBeginning {
item.userData?.playbackPositionTicks = 0
item.userData?.playedPercentage = 0
sliderPercentage = 0
sliderPercentageChanged(newValue: 0)
}
}
}
// MARK: Adjacent Items
extension VideoPlayerViewModel {

View File

@ -17,45 +17,47 @@ struct EpisodeRowCard: View {
let episode: BaseItemDto
var body: some View {
Button {
itemRouter.route(to: \.item, episode)
} label: {
HStack(alignment: .top) {
VStack(alignment: .leading) {
VStack {
Button {
itemRouter.route(to: \.item, episode)
} label: {
ImageView(src: episode.getBackdropImage(maxWidth: 550),
bh: episode.getBackdropImageBlurHash())
.mask(Rectangle().frame(width: 550, height: 308))
.frame(width: 550, height: 308)
}
.buttonStyle(CardButtonStyle())
VStack(alignment: .leading) {
ImageView(src: episode.getBackdropImage(maxWidth: 500),
bh: episode.getBackdropImageBlurHash())
.mask(Rectangle().frame(width: 500, height: 280))
.frame(width: 500, height: 280)
VStack(alignment: .leading) {
Text(episode.getEpisodeLocator() ?? "")
.font(.caption)
.foregroundColor(.secondary)
Text(episode.name ?? "")
.font(.footnote)
.padding(.bottom, 1)
VStack(alignment: .leading) {
Text(episode.getEpisodeLocator() ?? "")
.font(.caption)
.foregroundColor(.secondary)
Text(episode.name ?? "")
.font(.footnote)
.padding(.bottom, 1)
if episode.unaired {
Text(episode.airDateLabel ?? L10n.noOverviewAvailable)
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
.lineLimit(3)
} else {
Text(episode.overview ?? "")
.font(.caption)
.fontWeight(.light)
.lineLimit(4)
.fixedSize(horizontal: false, vertical: true)
}
}
if episode.unaired {
Text(episode.airDateLabel ?? L10n.noOverviewAvailable)
.font(.caption)
.foregroundColor(.secondary)
.fontWeight(.light)
.lineLimit(3)
} else {
Text(episode.overview ?? "")
.font(.caption)
.fontWeight(.light)
.lineLimit(4)
}
}
.padding(.horizontal)
Spacer()
}
.frame(width: 500)
}
}
.buttonStyle(PlainButtonStyle())
Spacer()
}
.padding()
.frame(width: 550)
}
.focusSection()
}
}

View File

@ -13,6 +13,8 @@ struct CinematicResumeCardView: View {
@EnvironmentObject
var homeRouter: HomeCoordinator.Router
@ObservedObject
var viewModel: HomeViewModel
let item: BaseItemDto
var body: some View {
@ -53,8 +55,15 @@ struct CinematicResumeCardView: View {
}
.frame(width: 350, height: 210)
}
.buttonStyle(CardButtonStyle())
.padding(.top)
.buttonStyle(CardButtonStyle())
.padding(.top)
.contextMenu {
Button(role: .destructive) {
viewModel.removeItemFromResume(item)
} label: {
L10n.removeFromResume.text
}
}
}
.padding(.vertical)
}

View File

@ -33,6 +33,8 @@ struct HomeCinematicView: View {
@FocusState
var selectedItem: BaseItemDto?
@ObservedObject
var viewModel: HomeViewModel
@State
private var updatedSelectedItem: BaseItemDto?
@State
@ -41,7 +43,8 @@ struct HomeCinematicView: View {
private let items: [HomeCinematicViewItem]
private let backgroundViewModel = DynamicCinematicBackgroundViewModel()
init(items: [HomeCinematicViewItem], forcedItemSubtitle: String? = nil) {
init(viewModel: HomeViewModel, items: [HomeCinematicViewItem], forcedItemSubtitle: String? = nil) {
self.viewModel = viewModel
self.items = items
self.forcedItemSubtitle = forcedItemSubtitle
}
@ -99,7 +102,7 @@ struct HomeCinematicView: View {
CinematicNextUpCardView(item: item.item, showOverlay: true)
.focused($selectedItem, equals: item.item)
case .resume:
CinematicResumeCardView(item: item.item)
CinematicResumeCardView(viewModel: viewModel, item: item.item)
.focused($selectedItem, equals: item.item)
case .plain:
CinematicNextUpCardView(item: item.item, showOverlay: false)

View File

@ -15,7 +15,7 @@ struct HomeView: View {
@EnvironmentObject
var homeRouter: HomeCoordinator.Router
@ObservedObject
@StateObject
var viewModel = HomeViewModel()
@Default(.showPosterLabels)
var showPosterLabels
@ -32,7 +32,8 @@ struct HomeView: View {
LazyVStack(alignment: .leading) {
if viewModel.resumeItems.isEmpty {
HomeCinematicView(items: viewModel.latestAddedItems.map { .init(item: $0, type: .plain) },
HomeCinematicView(viewModel: viewModel,
items: viewModel.latestAddedItems.map { .init(item: $0, type: .plain) },
forcedItemSubtitle: L10n.recentlyAdded)
if !viewModel.nextUpItems.isEmpty {
@ -40,7 +41,8 @@ struct HomeView: View {
.focusSection()
}
} else {
HomeCinematicView(items: viewModel.resumeItems.map { .init(item: $0, type: .resume) })
HomeCinematicView(viewModel: viewModel,
items: viewModel.resumeItems.map { .init(item: $0, type: .resume) })
if !viewModel.nextUpItems.isEmpty {
NextUpView(items: viewModel.nextUpItems)

View File

@ -78,6 +78,20 @@ struct CinematicItemViewTopRow: View {
.cornerRadius(10)
}
.buttonStyle(CardButtonStyle())
.contextMenu {
if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 {
Button {
if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel {
itemVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
itemRouter.route(to: \.videoPlayer, itemVideoPlayerViewModel)
} else {
LogManager.shared.log.error("Attempted to play item but no playback information available")
}
} label: {
Label(L10n.playFromBeginning, systemImage: "gobackward")
}
}
}
}
}

View File

@ -2797,7 +2797,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 = "";
@ -2834,7 +2834,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 = "";
@ -2865,7 +2865,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 = (
@ -2892,7 +2892,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 = (

View File

@ -81,6 +81,13 @@ struct ContinueWatchingView: View {
}
}
}
.contextMenu {
Button(role: .destructive) {
viewModel.removeItemFromResume(item)
} label: {
L10n.removeFromResume.text
}
}
}
}
.padding(.horizontal)

View File

@ -50,6 +50,20 @@ struct ItemLandscapeMainView: View {
.cornerRadius(10)
}
.disabled(viewModel.playButtonItem == nil || viewModel.itemVideoPlayerViewModel == nil)
.contextMenu {
if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 {
Button {
if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel {
itemVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
itemRouter.route(to: \.videoPlayer, itemVideoPlayerViewModel)
} else {
LogManager.shared.log.error("Attempted to play item but no playback information available")
}
} label: {
Label(L10n.playFromBeginning, systemImage: "gobackward")
}
}
}
Spacer()
}

View File

@ -90,7 +90,11 @@ struct PortraitHeaderOverlayView: View {
// MARK: Play
Button {
self.itemRouter.route(to: \.videoPlayer, viewModel.itemVideoPlayerViewModel!)
if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel {
itemRouter.route(to: \.videoPlayer, itemVideoPlayerViewModel)
} else {
LogManager.shared.log.error("Attempted to play item but no playback information available")
}
} label: {
HStack {
Image(systemName: "play.fill")
@ -104,7 +108,22 @@ struct PortraitHeaderOverlayView: View {
.frame(width: 130, height: 40)
.background(viewModel.playButtonItem == nil ? Color(UIColor.secondarySystemFill) : Color.jellyfinPurple)
.cornerRadius(10)
}.disabled(viewModel.playButtonItem == nil)
}
.disabled(viewModel.playButtonItem == nil)
.contextMenu {
if viewModel.playButtonItem != nil, viewModel.item.userData?.playbackPositionTicks ?? 0 > 0 {
Button {
if let itemVideoPlayerViewModel = viewModel.itemVideoPlayerViewModel {
itemVideoPlayerViewModel.injectCustomValues(startFromBeginning: true)
itemRouter.route(to: \.videoPlayer, itemVideoPlayerViewModel)
} else {
LogManager.shared.log.error("Attempted to play item but no playback information available")
}
} label: {
Label(L10n.playFromBeginning, systemImage: "gobackward")
}
}
}
Spacer()