Skip to content
This repository has been archived by the owner on Nov 14, 2024. It is now read-only.

Use Context Menu in iOS 13+ when selecting episodes in AnimeViewController #241

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 93 additions & 23 deletions NineAnimator/Controllers/Player Scene/AnimeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1021,7 +1021,66 @@ extension AnimeViewController {

// MARK: - Actions for Episodes
extension AnimeViewController {
/// Context menu item that can cast to both UIAction and UIMenuItem
///
/// Used to support iOS 13+ and below
private struct BackwardsCompatibleContextMenuItem {
var title: String
var image: UIImage?
var action: Selector
weak var parent: AnimeViewController?

@available(iOS 13.0, *)
func toUIAction() -> UIAction {
.init(
title: title,
image: image,
handler: { _ in parent?.perform(action) }
)
}

func toUIMenuItem() -> UIMenuItem {
.init(title: title, action: action)
}
}

@available(iOS 13.0, *)
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let targetCell = tableView.cellForRow(at: indexPath) else { return nil }

// Obtain the episode link
var targetEpisodeLink: EpisodeLink?

if let episodeCell = targetCell as? EpisodeTableViewCell {
targetEpisodeLink = episodeCell.episodeLink
} else if let episodeCell = targetCell as? DetailedEpisodeTableViewCell {
targetEpisodeLink = episodeCell.episodeLink
}

guard let unwrappedEpisodeLink = targetEpisodeLink else { return nil }

contextMenuSelectedEpisode = unwrappedEpisodeLink
contextMenuSelectedIndexPath = indexPath

return .init(
identifier: nil,
previewProvider: nil
) { [weak self] _ in
let availableUIAction = self?.generateMenuActions(for: unwrappedEpisodeLink)
.map { $0.toUIAction() }

return UIMenu(
title: "Episode: \(unwrappedEpisodeLink.name)",
identifier: nil,
options: [],
children: availableUIAction ?? []
)
}
}

@IBAction private func onLongPress(_ recognizer: UILongPressGestureRecognizer) {
// Only used as fallback in < iOS 13 devices
if #available(iOS 13.0, *) { return }
if recognizer.state == .began {
// Obtain the touch position, indexPath, and cell
let targetTouchPosition = recognizer.location(in: tableView)
Expand Down Expand Up @@ -1053,43 +1112,54 @@ extension AnimeViewController {

let targetRect = tableView.convert(sourceView.frame, to: view)
let editMenu = UIMenuController.shared
var availableMenuItems = [UIMenuItem]()

let availableMenuItems = generateMenuActions(for: episodeLink)
.map { $0.toUIMenuItem() }

// Save the available actions
editMenu.menuItems = availableMenuItems

if #available(iOS 13.0, *) {
editMenu.showMenu(from: view, rect: targetRect)
} else {
// Fallback on earlier versions
editMenu.setTargetRect(targetRect, in: view)
editMenu.setMenuVisible(true, animated: true)
}
}

private func generateMenuActions(for episodeLink: EpisodeLink) -> [BackwardsCompatibleContextMenuItem] {
var availableMenuItems = [BackwardsCompatibleContextMenuItem]()

switch episodeLink.playbackProgress {
case 0..<0.05:
availableMenuItems.append(UIMenuItem(
availableMenuItems.append(.init(
title: "Mark as Completed",
action: #selector(contextMenu(markAsWatched:))
action: #selector(contextMenuMarkAsWatched),
parent: self
))
case 0.05..<0.8:
availableMenuItems.append(UIMenuItem(
availableMenuItems.append(.init(
title: "Unwatch",
action: #selector(contextMenu(markAsUnwatched:))
action: #selector(contextMenuMarkAsUnwatched),
parent: self
))
availableMenuItems.append(UIMenuItem(
availableMenuItems.append(.init(
title: "Finished",
action: #selector(contextMenu(markAsWatched:))
action: #selector(contextMenuMarkAsWatched),
parent: self
))
default:
availableMenuItems.append(UIMenuItem(
availableMenuItems.append(.init(
title: "Unwatch",
action: #selector(contextMenu(markAsUnwatched:))
action: #selector(contextMenuMarkAsUnwatched),
parent: self
))
}

// Save the available actions
editMenu.menuItems = availableMenuItems

if #available(iOS 13.0, *) {
editMenu.showMenu(from: view, rect: targetRect)
} else {
// Fallback on earlier versions
editMenu.setTargetRect(targetRect, in: view)
editMenu.setMenuVisible(true, animated: true)
}
return availableMenuItems
}

@objc private func contextMenu(markAsWatched sender: UIMenuController) {
@objc func contextMenuMarkAsWatched() {
guard let selectedEpisodeLink = self.contextMenuSelectedEpisode,
let selectedIndexPath = self.contextMenuSelectedIndexPath,
let selectedEpisodeCell = tableView.cellForRow(at: selectedIndexPath),
Expand Down Expand Up @@ -1158,7 +1228,7 @@ extension AnimeViewController {
present(alertMessage, animated: true)
}

@objc private func contextMenu(markAsUnwatched sender: UIMenuController) {
@objc func contextMenuMarkAsUnwatched() {
if let episodeLink = contextMenuSelectedEpisode, let tracker = anime?.trackingContext {
tracker.update(progress: 0.0, forEpisodeLink: episodeLink)
}
Expand All @@ -1169,7 +1239,7 @@ extension AnimeViewController {
/// - Parameters:
/// - batchUpdatePerformed: Boolean indicating if more than 1 episode's progress has been updated.
private func concludeContextMenu(batchUpdatePerformed: Bool = false) {
/// Do not reload tableView if batch update has occured.
/// Do not reload tableView if batch update has occurred.
/// `onBatchPlaybackProgressDidUpdate(:_)` will handle reloading the tableView
if let targetIndexPath = contextMenuSelectedIndexPath, !batchUpdatePerformed {
tableView.performBatchUpdates({
Expand Down