Merge pull request #289 from LePips/remove-from-resume
Remove from Resume and Play from Beginning
This commit is contained in:
commit
dba82fdb17
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -185,6 +185,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
|
||||
|
||||
private func refreshNextUpItems() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
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) {
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
Text(episode.getEpisodeLocator() ?? "")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
Text(episode.name ?? "")
|
||||
.font(.footnote)
|
||||
.padding(.bottom, 1)
|
||||
|
||||
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() ?? "")
|
||||
if episode.unaired {
|
||||
Text(episode.airDateLabel ?? L10n.noOverviewAvailable)
|
||||
.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)
|
||||
}
|
||||
.fontWeight(.light)
|
||||
.lineLimit(3)
|
||||
} else {
|
||||
Text(episode.overview ?? "")
|
||||
.font(.caption)
|
||||
.fontWeight(.light)
|
||||
.lineLimit(4)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.frame(width: 500)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
.frame(width: 550)
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
.focusSection()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ struct CinematicResumeCardView: View {
|
|||
|
||||
@EnvironmentObject
|
||||
var homeRouter: HomeCoordinator.Router
|
||||
@ObservedObject
|
||||
var viewModel: HomeViewModel
|
||||
let item: BaseItemDto
|
||||
|
||||
var body: some View {
|
||||
|
@ -55,6 +57,13 @@ struct CinematicResumeCardView: View {
|
|||
}
|
||||
.buttonStyle(CardButtonStyle())
|
||||
.padding(.top)
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
viewModel.removeItemFromResume(item)
|
||||
} label: {
|
||||
L10n.removeFromResume.text
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -78,6 +78,24 @@ 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")
|
||||
}
|
||||
|
||||
Button(role: .cancel) {} label: {
|
||||
L10n.cancel.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -81,6 +81,13 @@ struct ContinueWatchingView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.contextMenu {
|
||||
Button(role: .destructive) {
|
||||
viewModel.removeItemFromResume(item)
|
||||
} label: {
|
||||
L10n.removeFromResume.text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
Binary file not shown.
Loading…
Reference in New Issue