mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-29 06:56:46 -04:00
Library pause / resume
This commit is contained in:
parent
1aeff3dbd0
commit
9879fae22a
@ -107,17 +107,32 @@ class DownloadTasksController: UITableViewController, NSFetchedResultsController
|
|||||||
cell.titleLabel.text = book.title
|
cell.titleLabel.text = book.title
|
||||||
cell.favIcon.image = UIImage(data: book.favIcon ?? NSData())
|
cell.favIcon.image = UIImage(data: book.favIcon ?? NSData())
|
||||||
|
|
||||||
guard let progress = Network.shared.operations[id]?.progress else {return}
|
if let progress = Network.shared.operations[id]?.progress {
|
||||||
cell.progressLabel.text = progress.fractionCompletedDescription
|
cell.progressLabel.text = progress.fractionCompletedDescription
|
||||||
cell.progressView.setProgress(Float(progress.fractionCompleted), animated: animated)
|
cell.progressView.setProgress(Float(progress.fractionCompleted), animated: animated)
|
||||||
cell.detailLabel.text = {
|
cell.detailLabel.text = {
|
||||||
let string = progress.progressAndSpeedDescription
|
let string = progress.progressAndSpeedDescription
|
||||||
|
if downloadTask.state == .Downloading {
|
||||||
if string.containsString(" — ") {
|
if string.containsString(" — ") {
|
||||||
return string.stringByReplacingOccurrencesOfString(" — ", withString: "\n")
|
return string.stringByReplacingOccurrencesOfString(" — ", withString: "\n")
|
||||||
} else {
|
} else {
|
||||||
return string + "\n" + NSLocalizedString("Estimating Speed and Remaining Time", comment: "")
|
return string + "\n" + "Estimating"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return string + "\n" + String(downloadTask.state)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
} else {
|
||||||
|
let progress = Double(downloadTask.totalBytesWritten) / Double(book.fileSize)
|
||||||
|
cell.progressLabel.text = DownloadTask.percentFormatter.stringFromNumber(NSNumber(double: progress))
|
||||||
|
cell.progressView.setProgress(Float(progress), animated: animated)
|
||||||
|
cell.detailLabel.text = {
|
||||||
|
let downloadedSize = NSByteCountFormatter.stringFromByteCount(downloadTask.totalBytesWritten, countStyle: .File)
|
||||||
|
let fileSize = book.fileSizeDescription
|
||||||
|
return String(format: "%@ of %@ completed", downloadedSize, fileSize) + "\n" + String(downloadTask.state)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Other Data Source
|
// MARK: Other Data Source
|
||||||
@ -159,27 +174,67 @@ class DownloadTasksController: UITableViewController, NSFetchedResultsController
|
|||||||
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {}
|
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {}
|
||||||
|
|
||||||
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
|
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
|
||||||
|
guard let downloadTask = self.fetchedResultController.objectAtIndexPath(indexPath) as? DownloadTask else {return nil}
|
||||||
|
|
||||||
|
var actions = [UITableViewRowAction]()
|
||||||
|
switch downloadTask.state {
|
||||||
|
case .Downloading:
|
||||||
let pause = UITableViewRowAction(style: .Normal, title: "Pause") { (action, indexPath) in
|
let pause = UITableViewRowAction(style: .Normal, title: "Pause") { (action, indexPath) in
|
||||||
guard let downloadTask = self.fetchedResultController.objectAtIndexPath(indexPath) as? DownloadTask else {return}
|
|
||||||
self.managedObjectContext.performBlock({
|
|
||||||
downloadTask.state = .Paused
|
downloadTask.state = .Paused
|
||||||
})
|
|
||||||
guard let bookID = downloadTask.book?.id else {return}
|
guard let bookID = downloadTask.book?.id else {return}
|
||||||
Network.shared.operations[bookID]?.cancel(produceResumeData: true)
|
Network.shared.operations[bookID]?.cancel(produceResumeData: true)
|
||||||
|
tableView.setEditing(false, animated: true)
|
||||||
}
|
}
|
||||||
|
actions.insert(pause, atIndex: 0)
|
||||||
|
case .Paused:
|
||||||
|
|
||||||
|
if let book = downloadTask.book,
|
||||||
|
let resumeData = Network.shared.data {
|
||||||
|
let resume = UITableViewRowAction(style: .Normal, title: "Resume") { (action, indexPath) in
|
||||||
|
let task = Network.shared.session.downloadTaskWithResumeData(resumeData)
|
||||||
|
let operation = DownloadBookOperation(downloadTask: task)
|
||||||
|
Network.shared.queue.addOperation(operation)
|
||||||
|
tableView.setEditing(false, animated: true)
|
||||||
|
}
|
||||||
|
actions.insert(resume, atIndex: 0)
|
||||||
|
} else {
|
||||||
|
let restart = UITableViewRowAction(style: .Normal, title: "Restart") { (action, indexPath) in
|
||||||
|
guard let bookID = downloadTask.book?.id,
|
||||||
|
let operation = DownloadBookOperation(bookID: bookID) else {return}
|
||||||
|
Network.shared.queue.addOperation(operation)
|
||||||
|
tableView.setEditing(false, animated: true)
|
||||||
|
}
|
||||||
|
actions.insert(restart, atIndex: 0)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
let cancel = UITableViewRowAction(style: .Destructive, title: LocalizedStrings.Common.cancel) { (action, indexPath) -> Void in
|
let cancel = UITableViewRowAction(style: .Destructive, title: LocalizedStrings.Common.cancel) { (action, indexPath) -> Void in
|
||||||
guard let downloadTask = self.fetchedResultController.objectAtIndexPath(indexPath) as? DownloadTask else {return}
|
|
||||||
if let bookID = downloadTask.book?.id {
|
if let bookID = downloadTask.book?.id {
|
||||||
// Cancel the download operation, did cancel observer will do the rest
|
if let operation = Network.shared.operations[bookID] {
|
||||||
Network.shared.operations[bookID]?.cancel(produceResumeData: false)
|
// When download is ongoing
|
||||||
|
// Cancel the download operation
|
||||||
|
// URLSessionTaskDelegate will update coredata and do clean up
|
||||||
|
operation.cancel(produceResumeData: false)
|
||||||
|
} else {
|
||||||
|
// When download is paused
|
||||||
|
// Remove resume data
|
||||||
|
// Delete downloadTask object and set book to not local
|
||||||
|
downloadTask.book?.removeResumeData()
|
||||||
|
downloadTask.book?.isLocal = NSNumber(bool: false)
|
||||||
|
self.managedObjectContext.deleteObject(downloadTask)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// In case of something goes wrong, and cannot find the book related to a download task, allow user to delete the row
|
// In case of something goes wrong, and cannot find the book related to a download task, allow user to delete the row
|
||||||
self.managedObjectContext.performBlock({
|
|
||||||
self.managedObjectContext.deleteObject(downloadTask)
|
self.managedObjectContext.deleteObject(downloadTask)
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
tableView.setEditing(false, animated: true)
|
||||||
}
|
}
|
||||||
return [cancel, pause]
|
actions.insert(cancel, atIndex: 0)
|
||||||
|
|
||||||
|
return actions
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Fetched Results Controller
|
// MARK: - Fetched Results Controller
|
||||||
|
@ -49,7 +49,7 @@
|
|||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.7.1525</string>
|
<string>1.7.1576</string>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
@ -21,7 +21,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>1.7.1892</string>
|
<string>1.7.1983</string>
|
||||||
<key>NSExtension</key>
|
<key>NSExtension</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSExtensionMainStoryboard</key>
|
<key>NSExtensionMainStoryboard</key>
|
||||||
|
@ -19,5 +19,21 @@
|
|||||||
landmarkType = "5">
|
landmarkType = "5">
|
||||||
</BreakpointContent>
|
</BreakpointContent>
|
||||||
</BreakpointProxy>
|
</BreakpointProxy>
|
||||||
|
<BreakpointProxy
|
||||||
|
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||||
|
<BreakpointContent
|
||||||
|
shouldBeEnabled = "Yes"
|
||||||
|
ignoreCount = "0"
|
||||||
|
continueAfterRunningActions = "No"
|
||||||
|
filePath = "Kiwix-iOS/Controller/Library/DownloadTasksController.swift"
|
||||||
|
timestampString = "494191501.485551"
|
||||||
|
startingColumnNumber = "9223372036854775807"
|
||||||
|
endingColumnNumber = "9223372036854775807"
|
||||||
|
startingLineNumber = "195"
|
||||||
|
endingLineNumber = "195"
|
||||||
|
landmarkName = "tableView(_:editActionsForRowAtIndexPath:)"
|
||||||
|
landmarkType = "7">
|
||||||
|
</BreakpointContent>
|
||||||
|
</BreakpointProxy>
|
||||||
</Breakpoints>
|
</Breakpoints>
|
||||||
</Bucket>
|
</Bucket>
|
||||||
|
@ -74,6 +74,16 @@ class Book: NSManagedObject {
|
|||||||
return urlComponents?.URL
|
return urlComponents?.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resumeDataURL: NSURL? {
|
||||||
|
guard let id = id,
|
||||||
|
let folderURL = NSURL(fileURLWithPath: NSFileManager.libDirURL.path!).URLByAppendingPathComponent("DownloadTemp", isDirectory: true),
|
||||||
|
let folderPath = folderURL.path else {return nil}
|
||||||
|
if !NSFileManager.defaultManager().fileExistsAtPath(folderPath) {
|
||||||
|
_ = try? NSFileManager.defaultManager().createDirectoryAtURL(folderURL, withIntermediateDirectories: true, attributes: [NSURLIsExcludedFromBackupKey: true])
|
||||||
|
}
|
||||||
|
return folderURL.URLByAppendingPathComponent(id)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Fetch
|
// MARK: - Fetch
|
||||||
|
|
||||||
class func fetchAll(context: NSManagedObjectContext) -> [Book] {
|
class func fetchAll(context: NSManagedObjectContext) -> [Book] {
|
||||||
@ -103,10 +113,9 @@ class Book: NSManagedObject {
|
|||||||
|
|
||||||
// MARK: - Manage
|
// MARK: - Manage
|
||||||
|
|
||||||
func removeCache() {
|
func removeResumeData() {
|
||||||
guard let bookID = id,
|
guard let id = id else {return}
|
||||||
let cacheFileURL = NSFileManager.cacheDirURL.URLByAppendingPathComponent(bookID) else {return}
|
Preference.resumeData[id] = nil
|
||||||
_ = try? NSFileManager.defaultManager().removeItemAtURL(cacheFileURL)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Properties Description
|
// MARK: - Properties Description
|
||||||
@ -120,7 +129,7 @@ class Book: NSManagedObject {
|
|||||||
return formatter.stringFromDate(date)
|
return formatter.stringFromDate(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileSizeDescription: String? {
|
var fileSizeDescription: String {
|
||||||
return NSByteCountFormatter.stringFromByteCount(fileSize, countStyle: .File)
|
return NSByteCountFormatter.stringFromByteCount(fileSize, countStyle: .File)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,7 +161,7 @@ class Book: NSManagedObject {
|
|||||||
var detailedDescription: String? {
|
var detailedDescription: String? {
|
||||||
var descriptions = [String]()
|
var descriptions = [String]()
|
||||||
if let dateDescription = dateDescription {descriptions.append(dateDescription)}
|
if let dateDescription = dateDescription {descriptions.append(dateDescription)}
|
||||||
if let fileSizeDescription = fileSizeDescription {descriptions.append(fileSizeDescription)}
|
descriptions.append(fileSizeDescription)
|
||||||
if let articleCountDescription = articleCountDescription {descriptions.append(articleCountDescription)}
|
if let articleCountDescription = articleCountDescription {descriptions.append(articleCountDescription)}
|
||||||
|
|
||||||
guard descriptions.count != 0 else {return nil}
|
guard descriptions.count != 0 else {return nil}
|
||||||
|
@ -41,6 +41,16 @@ class DownloadTask: NSManagedObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static let percentFormatter: NSNumberFormatter = {
|
||||||
|
let formatter = NSNumberFormatter()
|
||||||
|
formatter.numberStyle = .PercentStyle
|
||||||
|
formatter.minimumFractionDigits = 1
|
||||||
|
formatter.maximumIntegerDigits = 3
|
||||||
|
formatter.minimumFractionDigits = 2
|
||||||
|
formatter.maximumIntegerDigits = 2
|
||||||
|
return formatter
|
||||||
|
}()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DownloadTaskState: Int {
|
enum DownloadTaskState: Int {
|
||||||
|
@ -13,7 +13,9 @@ import Operations
|
|||||||
class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate, OperationQueueDelegate {
|
class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate, OperationQueueDelegate {
|
||||||
static let shared = Network()
|
static let shared = Network()
|
||||||
let queue = OperationQueue()
|
let queue = OperationQueue()
|
||||||
|
let context = NSManagedObjectContext.mainQueueContext
|
||||||
private(set) var operations = [String: DownloadBookOperation]()
|
private(set) var operations = [String: DownloadBookOperation]()
|
||||||
|
var data: NSData?
|
||||||
|
|
||||||
private override init() {
|
private override init() {
|
||||||
super.init()
|
super.init()
|
||||||
@ -49,9 +51,30 @@ class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSe
|
|||||||
// MARK: - NSURLSessionTaskDelegate
|
// MARK: - NSURLSessionTaskDelegate
|
||||||
|
|
||||||
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
|
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
|
||||||
print(error?.code)
|
guard let error = error, let bookID = task.taskDescription else {return}
|
||||||
print(error?.localizedDescription)
|
self.context.performBlockAndWait {
|
||||||
print(error?.userInfo.keys)
|
guard let book = Book.fetch(bookID, context: self.context) else {return}
|
||||||
|
if error.code == NSURLErrorCancelled {
|
||||||
|
// If download task doesnt exist, it must mean download is cancelled by user
|
||||||
|
// DownloadTask object will have been deleted when user tap Cancel button / table row action
|
||||||
|
guard let downloadTask = book.downloadTask else {return}
|
||||||
|
downloadTask.totalBytesWritten = task.countOfBytesReceived
|
||||||
|
downloadTask.state = .Paused
|
||||||
|
|
||||||
|
// Save resue data to disk
|
||||||
|
guard let resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData] as? NSData else {return}
|
||||||
|
let task = Network.shared.session.downloadTaskWithResumeData(resumeData)
|
||||||
|
task.resume()
|
||||||
|
guard let book = downloadTask.book else {return}
|
||||||
|
|
||||||
|
self.data = resumeData
|
||||||
|
// guard let resumeDataPath = book.resumeDataURL?.path,
|
||||||
|
// let resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData] as? NSData else {return}
|
||||||
|
// resumeData.writeToFile(resumeDataPath, atomically: true)
|
||||||
|
} else {
|
||||||
|
// Handle other errors
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - NSURLSessionDownloadDelegate
|
// MARK: - NSURLSessionDownloadDelegate
|
||||||
@ -60,6 +83,11 @@ class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSe
|
|||||||
guard let bookID = downloadTask.taskDescription,
|
guard let bookID = downloadTask.taskDescription,
|
||||||
let operation = operations[bookID] else {return}
|
let operation = operations[bookID] else {return}
|
||||||
operation.progress.completedUnitCount = totalBytesWritten
|
operation.progress.completedUnitCount = totalBytesWritten
|
||||||
|
|
||||||
|
context.performBlock {
|
||||||
|
guard let downloadTask = Book.fetch(bookID, context: self.context)?.downloadTask where downloadTask.state == .Queued else {return}
|
||||||
|
downloadTask.state = .Downloading
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
|
func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {
|
||||||
@ -73,12 +101,11 @@ class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSe
|
|||||||
_ = try? NSFileManager.defaultManager().moveItemAtURL(location, toURL: destination)
|
_ = try? NSFileManager.defaultManager().moveItemAtURL(location, toURL: destination)
|
||||||
|
|
||||||
// Perform clean up (remove cache and delete download task)
|
// Perform clean up (remove cache and delete download task)
|
||||||
let context = NSManagedObjectContext.mainQueueContext
|
|
||||||
context.performBlock {
|
context.performBlock {
|
||||||
guard let book = Book.fetch(bookID, context: context) else {return}
|
guard let book = Book.fetch(bookID, context: self.context) else {return}
|
||||||
book.removeCache()
|
book.removeResumeData()
|
||||||
guard let downloadTask = book.downloadTask else {return}
|
guard let downloadTask = book.downloadTask else {return}
|
||||||
context.deleteObject(downloadTask)
|
self.context.deleteObject(downloadTask)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,16 +14,6 @@ class DownloadProgress: NSProgress {
|
|||||||
private let timePointMinCount: Int = 20
|
private let timePointMinCount: Int = 20
|
||||||
private let timePointMaxCount: Int = 200
|
private let timePointMaxCount: Int = 200
|
||||||
|
|
||||||
private lazy var percentFormatter: NSNumberFormatter = {
|
|
||||||
let formatter = NSNumberFormatter()
|
|
||||||
formatter.numberStyle = .PercentStyle
|
|
||||||
formatter.minimumFractionDigits = 1
|
|
||||||
formatter.maximumIntegerDigits = 3
|
|
||||||
formatter.minimumFractionDigits = 2
|
|
||||||
formatter.maximumIntegerDigits = 2
|
|
||||||
return formatter
|
|
||||||
}()
|
|
||||||
|
|
||||||
init(completedUnitCount: Int64 = 0, totalUnitCount: Int64) {
|
init(completedUnitCount: Int64 = 0, totalUnitCount: Int64) {
|
||||||
super.init(parent: nil, userInfo: [NSProgressFileOperationKindKey: NSProgressFileOperationKindDownloading])
|
super.init(parent: nil, userInfo: [NSProgressFileOperationKindKey: NSProgressFileOperationKindDownloading])
|
||||||
self.kind = NSProgressKindFile
|
self.kind = NSProgressKindFile
|
||||||
@ -40,7 +30,7 @@ class DownloadProgress: NSProgress {
|
|||||||
// MARK: - Descriptions
|
// MARK: - Descriptions
|
||||||
|
|
||||||
var fractionCompletedDescription: String? {
|
var fractionCompletedDescription: String? {
|
||||||
return percentFormatter.stringFromNumber(NSNumber(double: fractionCompleted))
|
return DownloadTask.percentFormatter.stringFromNumber(NSNumber(double: fractionCompleted))
|
||||||
}
|
}
|
||||||
|
|
||||||
var progressAndSpeedDescription: String! {
|
var progressAndSpeedDescription: String! {
|
||||||
|
@ -91,6 +91,12 @@ class Preference {
|
|||||||
set{Defaults[.langFilterNameDisplayInOriginalLocale] = newValue}
|
set{Defaults[.langFilterNameDisplayInOriginalLocale] = newValue}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Resume Data
|
||||||
|
|
||||||
|
class var resumeData: [String: NSData] {
|
||||||
|
get{return Defaults[.resumeData] as? [String: NSData] ?? [String: NSData]()}
|
||||||
|
set{Defaults[.resumeData] = newValue}}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension DefaultsKeys {
|
extension DefaultsKeys {
|
||||||
@ -108,4 +114,6 @@ extension DefaultsKeys {
|
|||||||
static let libraryHasShownPreferredLanguagePrompt = DefaultsKey<Bool>("libraryHasShownPreferredLanguagePrompt")
|
static let libraryHasShownPreferredLanguagePrompt = DefaultsKey<Bool>("libraryHasShownPreferredLanguagePrompt")
|
||||||
static let langFilterSortByAlphabeticalAsc = DefaultsKey<Bool>("langFilterSortByAlphabeticalAsc")
|
static let langFilterSortByAlphabeticalAsc = DefaultsKey<Bool>("langFilterSortByAlphabeticalAsc")
|
||||||
static let langFilterNameDisplayInOriginalLocale = DefaultsKey<Bool>("langFilterNameDisplayInOriginalLocale")
|
static let langFilterNameDisplayInOriginalLocale = DefaultsKey<Bool>("langFilterNameDisplayInOriginalLocale")
|
||||||
|
|
||||||
|
static let resumeData = DefaultsKey<[String: AnyObject]>("resumeData")
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user