diff --git a/NineAnimator.xcodeproj/project.pbxproj b/NineAnimator.xcodeproj/project.pbxproj index 33f4544ba..c82a6fe6b 100644 --- a/NineAnimator.xcodeproj/project.pbxproj +++ b/NineAnimator.xcodeproj/project.pbxproj @@ -476,6 +476,7 @@ BF762C612616A8FB00265DA4 /* Pantsubase+Episode.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF762C602616A8FB00265DA4 /* Pantsubase+Episode.swift */; }; BF762C662616BDBD00265DA4 /* PantsudriveParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF762C652616BDBD00265DA4 /* PantsudriveParser.swift */; }; BF76D9F524EA27C000734C43 /* AnimeHub+Episode.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFCB22AA24DF378800406034 /* AnimeHub+Episode.swift */; }; + BF7D075E26324F5500842CBC /* SourceDescriptionTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7D075D26324F5500842CBC /* SourceDescriptionTableViewCell.swift */; }; BF86795425F99BBC0046773E /* CachedDownloadsToAppSupportDataMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF86795325F99BBC0046773E /* CachedDownloadsToAppSupportDataMigrator.swift */; }; BF9203DC25E07E6E00CB2D91 /* AudioBackgroundController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9203DB25E07E6E00CB2D91 /* AudioBackgroundController.swift */; }; BF95264224F1E06B00924B79 /* AnilistUserRecommendations.graphql in Resources */ = {isa = PBXBuildFile; fileRef = BF95264124F1E06B00924B79 /* AnilistUserRecommendations.graphql */; }; @@ -1088,6 +1089,7 @@ BF762C5B2616A8BA00265DA4 /* Pantsubase+Anime.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Pantsubase+Anime.swift"; sourceTree = ""; }; BF762C602616A8FB00265DA4 /* Pantsubase+Episode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Pantsubase+Episode.swift"; sourceTree = ""; }; BF762C652616BDBD00265DA4 /* PantsudriveParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PantsudriveParser.swift; sourceTree = ""; }; + BF7D075D26324F5500842CBC /* SourceDescriptionTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDescriptionTableViewCell.swift; sourceTree = ""; }; BF86795325F99BBC0046773E /* CachedDownloadsToAppSupportDataMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedDownloadsToAppSupportDataMigrator.swift; sourceTree = ""; }; BF9203DB25E07E6E00CB2D91 /* AudioBackgroundController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioBackgroundController.swift; sourceTree = ""; }; BF95264124F1E06B00924B79 /* AnilistUserRecommendations.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = AnilistUserRecommendations.graphql; sourceTree = ""; }; @@ -2189,6 +2191,7 @@ 2CD9B9A521FE605B00203442 /* AnimeSynopsisCellTableViewCell.swift */, 2C8F6E7F2205E76D0047798B /* AnimePredictedEpisodeTableViewCell.swift */, 2CDA17A422065456009543F9 /* OfflineEpisodeTableViewCell.swift */, + BF7D075D26324F5500842CBC /* SourceDescriptionTableViewCell.swift */, ); path = "Anime Scene"; sourceTree = ""; @@ -2918,6 +2921,7 @@ 2CAEA891235D385400AC2370 /* LibraryHeaderView.swift in Sources */, 2C4FDDAC22482AE90059B7D8 /* UrlFormEncoding.swift in Sources */, 2C86C1EF225D7FB600950ABF /* SetupWhatsNewViewController.swift in Sources */, + BF7D075E26324F5500842CBC /* SourceDescriptionTableViewCell.swift in Sources */, 2CBDBC5B24BCBC0F000B360D /* UserLibrary.xcdatamodeld in Sources */, 2C8374EA24AF808A0009F8D7 /* SettingsRichPresenceController.swift in Sources */, 2C6A390A23A0180B00FC00A9 /* FourAnime+Episode.swift in Sources */, diff --git a/NineAnimator/Base.lproj/AnimePlayer.storyboard b/NineAnimator/Base.lproj/AnimePlayer.storyboard index 50f593c0b..7ffc283ba 100644 --- a/NineAnimator/Base.lproj/AnimePlayer.storyboard +++ b/NineAnimator/Base.lproj/AnimePlayer.storyboard @@ -1,9 +1,9 @@ - + - + @@ -85,7 +85,7 @@ - + + + + + + + + + + + + + + + + + + + + + - + @@ -465,7 +533,7 @@ - + @@ -532,7 +600,7 @@ - + @@ -572,7 +640,7 @@ - + @@ -692,6 +760,20 @@ + + + + + + + + + + + + + + diff --git a/NineAnimator/Controllers/Player Scene/AnimeViewController.swift b/NineAnimator/Controllers/Player Scene/AnimeViewController.swift index 800add9f0..f1fcf26bc 100644 --- a/NineAnimator/Controllers/Player Scene/AnimeViewController.swift +++ b/NineAnimator/Controllers/Player Scene/AnimeViewController.swift @@ -344,7 +344,7 @@ extension AnimeViewController { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { switch Section(rawValue: section)! { - case .suggestion, .synopsis: + case .suggestion, .synopsis, .sourceDescription: return anime == nil ? 0 : 1 case .episodes: return anime?.numberOfEpisodeLinks ?? 0 @@ -368,6 +368,16 @@ extension AnimeViewController { tableView?.endUpdates() } return cell + case .sourceDescription: + let cell = tableView.dequeueReusableCell(withIdentifier: "anime.source.description", for: indexPath) as! SourceDescriptionTableViewCell + + cell.setPresenting( + source: anime!.source, + server: anime!.servers[server!] ?? "No server" + ) { [weak self] cell in + self?.showSelectServerDialog(sourceView: cell) + } + return cell case .episodes: let episode = episodeLink(for: indexPath)! @@ -889,7 +899,9 @@ extension AnimeViewController { if anime != nil { actionSheet.addAction({ let action = UIAlertAction(title: "Select Server", style: .default) { - [weak self] _ in self?.showSelectServerDialog() + [weak self] _ in + guard let self = self else { return } + self.showSelectServerDialog(sourceView: self.moreOptionsButton) } action.image = #imageLiteral(resourceName: "Server") action.textAlignment = .left @@ -942,11 +954,12 @@ extension AnimeViewController { present(actionSheet, animated: true, completion: nil) } - private func showSelectServerDialog() { + private func showSelectServerDialog(sourceView: UIView) { let alertView = UIAlertController(title: "Select Server", message: nil, preferredStyle: .actionSheet) if let popover = alertView.popoverPresentationController { - popover.sourceView = moreOptionsButton + popover.sourceView = sourceView + popover.sourceRect = sourceView.bounds } for server in anime!.servers { @@ -988,7 +1001,7 @@ extension AnimeViewController { self.server = server tableView.reloadSections( - Section.indexSet(.episodes, .suggestion), + Section.indexSet(.episodes, .suggestion, .sourceDescription), with: .automatic ) @@ -1477,7 +1490,9 @@ fileprivate extension AnimeViewController { case synopsis = 1 - case episodes = 2 + case sourceDescription = 2 + + case episodes = 3 subscript(_ item: Int) -> IndexPath { IndexPath(item: item, section: self.rawValue) @@ -1506,5 +1521,5 @@ fileprivate extension AnimeViewController { } fileprivate extension Array where Element == AnimeViewController.Section { - static let all: [AnimeViewController.Section] = [ .suggestion, .synopsis, .episodes ] + static let all: [AnimeViewController.Section] = [ .suggestion, .synopsis, .sourceDescription, .episodes ] } diff --git a/NineAnimator/Utilities/Asynchronous/Promise.swift b/NineAnimator/Utilities/Asynchronous/Promise.swift index 9fd24ee5e..9f95ef833 100644 --- a/NineAnimator/Utilities/Asynchronous/Promise.swift +++ b/NineAnimator/Utilities/Asynchronous/Promise.swift @@ -27,13 +27,13 @@ protocol NineAnimatorPromiseProtocol { /// True if an error had occurred and this promise is rejected var isRejected: Bool { get } - /// Execute the promise immedietly + /// Execute the promise immediately func concludePromise() } /// NineAnimator's implementation of promise /// -/// Since NineAnimator is involved in many chainned networking stuff, +/// Since NineAnimator is involved in many chained networking stuff, /// which, as many can tell, creates numerous "callback hells" in the /// code, and that Marcus couldn't come up with which promise /// framework to use, so he's just going to write one himself. @@ -41,11 +41,24 @@ class NineAnimatorPromise: NineAnimatorAsyncTask, NineAnimatorPromis /// Hold reference to the task private var referenceTask: NineAnimatorAsyncTask? - /// Mark the promise as being resolved (success or failiure). A promise can only be resolved once. - private(set) var isResolved = false + /// Mark the promise as being resolved (success or failure). A promise can only be resolved once. + private var _isResolved = false /// A flag to mark if this promise has been rejected - private(set) var isRejected = true + private var _isRejected = true + + // Public thread-safe accessors. Should not be used internally + var isResolved: Bool { + semaphore.wait() + defer { semaphore.signal() } + return _isResolved + } + + var isRejected: Bool { + semaphore.wait() + defer { semaphore.signal() } + return _isRejected + } /// Storing the result private(set) var result: Result? @@ -69,8 +82,8 @@ class NineAnimatorPromise: NineAnimatorAsyncTask, NineAnimatorPromis private var deferBlock: ((NineAnimatorPromise) -> Void)? { didSet { - // Run the defer block immedietly if the promise has been resolved - if isResolved { deferBlock?(self) } + // Run the defer block immediately if the promise has been resolved + if _isResolved { deferBlock?(self) } } } @@ -81,17 +94,17 @@ class NineAnimatorPromise: NineAnimatorAsyncTask, NineAnimatorPromis /// promises will run in private var queue: DispatchQueue - /// Additional flags to be passed to the execusion of blocks + /// Additional flags to be passed to the execution of blocks private var queueFlags: DispatchWorkItemFlags /// The latest DispatchTime that the resolvers can be set - private var creationDate: DispatchTime = .now() + private let creationDate: DispatchTime = .now() /// The task to perform when the promise concludes setup - private var task: NineAnimatorPromiseInitialTask? + private let task: NineAnimatorPromiseInitialTask? /// Thread safety - private var semaphore: DispatchSemaphore + private let semaphore: DispatchSemaphore typealias NineAnimatorPromiseCallback = NineAnimatorCallback typealias NineAnimatorPromiseInitialTask = (@escaping NineAnimatorPromiseCallback) -> NineAnimatorAsyncTask? @@ -119,7 +132,7 @@ class NineAnimatorPromise: NineAnimatorAsyncTask, NineAnimatorPromis } // Check if the promise has been resolved - guard !isResolved || result != nil else { + guard !_isResolved || result != nil else { Log.error("[NineAnimatorPromise] Attempting to resolve a promise twice.") return } @@ -128,8 +141,8 @@ class NineAnimatorPromise: NineAnimatorAsyncTask, NineAnimatorPromis result = .success(value) defer { - isResolved = true - isRejected = false + _isResolved = true + _isRejected = false } // Run the defer block @@ -153,7 +166,7 @@ class NineAnimatorPromise: NineAnimatorAsyncTask, NineAnimatorPromis defer { releaseAll() } // Check if the promise has been resolved - guard !isResolved || result != nil else { + guard !_isResolved || result != nil else { Log.error("[NineAnimatorPromise] Attempting to resolve a promise twice.") return } @@ -162,8 +175,8 @@ class NineAnimatorPromise: NineAnimatorAsyncTask, NineAnimatorPromis result = .failure(error) defer { - isResolved = true - isRejected = true + _isResolved = false + _isRejected = true } // Run the defer block @@ -311,7 +324,7 @@ class NineAnimatorPromise: NineAnimatorAsyncTask, NineAnimatorPromis chainedErrorCallback = nil chainedReference = nil deferBlock = nil - isResolved = true + _isResolved = true } /// Conclude the setup of promise and start the task @@ -341,7 +354,7 @@ class NineAnimatorPromise: NineAnimatorAsyncTask, NineAnimatorPromis } /// Make a promise with a closure that will be executed asynchronously - /// in the speicified queue + /// in the specified queue static func firstly(queue: DispatchQueue = .global(), _ executingFunction: @escaping () throws -> ResultType?) -> NineAnimatorPromise { NineAnimatorPromise(queue: queue) { callback in @@ -355,7 +368,7 @@ class NineAnimatorPromise: NineAnimatorAsyncTask, NineAnimatorPromis } deinit { - if !isResolved { + if !_isResolved { Log.debug("[NineAnimatorPromise] Losing reference to an unresolved promise. This cancels any executing tasks.") } cancel() diff --git a/NineAnimator/Views/Anime Scene/SourceDescriptionTableViewCell.swift b/NineAnimator/Views/Anime Scene/SourceDescriptionTableViewCell.swift new file mode 100644 index 000000000..37e7be20c --- /dev/null +++ b/NineAnimator/Views/Anime Scene/SourceDescriptionTableViewCell.swift @@ -0,0 +1,41 @@ +// +// This file is part of the NineAnimator project. +// +// Copyright © 2018-2020 Marcus Zhou. All rights reserved. +// +// NineAnimator is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// NineAnimator is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with NineAnimator. If not, see . +// + +import UIKit + +class SourceDescriptionTableViewCell: UITableViewCell { + @IBOutlet private weak var sourceHeaderTitle: UILabel! + @IBOutlet private weak var serverTitleButton: UIButton! + + private var onTapHandler: ((UIButton) -> Void)? + + func setPresenting(source: Source, server: String, handler: @escaping (UIButton) -> Void) { + sourceHeaderTitle.text = "Episodes on \(source.name)" + onTapHandler = handler + serverTitleButton.setTitle(server, for: .normal) + + // Remove separator line for this cell + separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: .greatestFiniteMagnitude) + directionalLayoutMargins = .zero + } + + @IBAction private func onServerTitleClicked(_ sender: Any) { + onTapHandler?(serverTitleButton) + } +}