mirror of
https://github.com/kiwix/kiwix-apple.git
synced 2025-09-28 06:25:04 -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.favIcon.image = UIImage(data: book.favIcon ?? NSData())
|
||||
|
||||
guard let progress = Network.shared.operations[id]?.progress else {return}
|
||||
cell.progressLabel.text = progress.fractionCompletedDescription
|
||||
cell.progressView.setProgress(Float(progress.fractionCompleted), animated: animated)
|
||||
cell.detailLabel.text = {
|
||||
let string = progress.progressAndSpeedDescription
|
||||
if string.containsString(" — ") {
|
||||
return string.stringByReplacingOccurrencesOfString(" — ", withString: "\n")
|
||||
} else {
|
||||
return string + "\n" + NSLocalizedString("Estimating Speed and Remaining Time", comment: "")
|
||||
}
|
||||
}()
|
||||
if let progress = Network.shared.operations[id]?.progress {
|
||||
cell.progressLabel.text = progress.fractionCompletedDescription
|
||||
cell.progressView.setProgress(Float(progress.fractionCompleted), animated: animated)
|
||||
cell.detailLabel.text = {
|
||||
let string = progress.progressAndSpeedDescription
|
||||
if downloadTask.state == .Downloading {
|
||||
if string.containsString(" — ") {
|
||||
return string.stringByReplacingOccurrencesOfString(" — ", withString: "\n")
|
||||
} else {
|
||||
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
|
||||
@ -159,27 +174,67 @@ class DownloadTasksController: UITableViewController, NSFetchedResultsController
|
||||
override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {}
|
||||
|
||||
override func tableView(tableView: UITableView, editActionsForRowAtIndexPath indexPath: NSIndexPath) -> [UITableViewRowAction]? {
|
||||
let pause = UITableViewRowAction(style: .Normal, title: "Pause") { (action, indexPath) in
|
||||
guard let downloadTask = self.fetchedResultController.objectAtIndexPath(indexPath) as? DownloadTask else {return}
|
||||
self.managedObjectContext.performBlock({
|
||||
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
|
||||
downloadTask.state = .Paused
|
||||
})
|
||||
guard let bookID = downloadTask.book?.id else {return}
|
||||
Network.shared.operations[bookID]?.cancel(produceResumeData: true)
|
||||
|
||||
guard let bookID = downloadTask.book?.id else {return}
|
||||
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
|
||||
guard let downloadTask = self.fetchedResultController.objectAtIndexPath(indexPath) as? DownloadTask else {return}
|
||||
if let bookID = downloadTask.book?.id {
|
||||
// Cancel the download operation, did cancel observer will do the rest
|
||||
Network.shared.operations[bookID]?.cancel(produceResumeData: false)
|
||||
if let operation = Network.shared.operations[bookID] {
|
||||
// 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 {
|
||||
// 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
|
||||
|
@ -49,7 +49,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.7.1525</string>
|
||||
<string>1.7.1576</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
@ -21,7 +21,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.7.1892</string>
|
||||
<string>1.7.1983</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionMainStoryboard</key>
|
||||
|
@ -19,5 +19,21 @@
|
||||
landmarkType = "5">
|
||||
</BreakpointContent>
|
||||
</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>
|
||||
</Bucket>
|
||||
|
@ -74,6 +74,16 @@ class Book: NSManagedObject {
|
||||
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
|
||||
|
||||
class func fetchAll(context: NSManagedObjectContext) -> [Book] {
|
||||
@ -103,10 +113,9 @@ class Book: NSManagedObject {
|
||||
|
||||
// MARK: - Manage
|
||||
|
||||
func removeCache() {
|
||||
guard let bookID = id,
|
||||
let cacheFileURL = NSFileManager.cacheDirURL.URLByAppendingPathComponent(bookID) else {return}
|
||||
_ = try? NSFileManager.defaultManager().removeItemAtURL(cacheFileURL)
|
||||
func removeResumeData() {
|
||||
guard let id = id else {return}
|
||||
Preference.resumeData[id] = nil
|
||||
}
|
||||
|
||||
// MARK: - Properties Description
|
||||
@ -120,7 +129,7 @@ class Book: NSManagedObject {
|
||||
return formatter.stringFromDate(date)
|
||||
}
|
||||
|
||||
var fileSizeDescription: String? {
|
||||
var fileSizeDescription: String {
|
||||
return NSByteCountFormatter.stringFromByteCount(fileSize, countStyle: .File)
|
||||
}
|
||||
|
||||
@ -152,7 +161,7 @@ class Book: NSManagedObject {
|
||||
var detailedDescription: String? {
|
||||
var descriptions = [String]()
|
||||
if let dateDescription = dateDescription {descriptions.append(dateDescription)}
|
||||
if let fileSizeDescription = fileSizeDescription {descriptions.append(fileSizeDescription)}
|
||||
descriptions.append(fileSizeDescription)
|
||||
if let articleCountDescription = articleCountDescription {descriptions.append(articleCountDescription)}
|
||||
|
||||
guard descriptions.count != 0 else {return nil}
|
||||
|
@ -40,6 +40,16 @@ class DownloadTask: NSManagedObject {
|
||||
stateRaw = Int16(newValue.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
static let percentFormatter: NSNumberFormatter = {
|
||||
let formatter = NSNumberFormatter()
|
||||
formatter.numberStyle = .PercentStyle
|
||||
formatter.minimumFractionDigits = 1
|
||||
formatter.maximumIntegerDigits = 3
|
||||
formatter.minimumFractionDigits = 2
|
||||
formatter.maximumIntegerDigits = 2
|
||||
return formatter
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,9 @@ import Operations
|
||||
class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSessionDownloadDelegate, OperationQueueDelegate {
|
||||
static let shared = Network()
|
||||
let queue = OperationQueue()
|
||||
let context = NSManagedObjectContext.mainQueueContext
|
||||
private(set) var operations = [String: DownloadBookOperation]()
|
||||
var data: NSData?
|
||||
|
||||
private override init() {
|
||||
super.init()
|
||||
@ -49,9 +51,30 @@ class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSe
|
||||
// MARK: - NSURLSessionTaskDelegate
|
||||
|
||||
func URLSession(session: NSURLSession, task: NSURLSessionTask, didCompleteWithError error: NSError?) {
|
||||
print(error?.code)
|
||||
print(error?.localizedDescription)
|
||||
print(error?.userInfo.keys)
|
||||
guard let error = error, let bookID = task.taskDescription else {return}
|
||||
self.context.performBlockAndWait {
|
||||
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
|
||||
@ -60,6 +83,11 @@ class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSe
|
||||
guard let bookID = downloadTask.taskDescription,
|
||||
let operation = operations[bookID] else {return}
|
||||
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) {
|
||||
@ -73,12 +101,11 @@ class Network: NSObject, NSURLSessionDelegate, NSURLSessionTaskDelegate, NSURLSe
|
||||
_ = try? NSFileManager.defaultManager().moveItemAtURL(location, toURL: destination)
|
||||
|
||||
// Perform clean up (remove cache and delete download task)
|
||||
let context = NSManagedObjectContext.mainQueueContext
|
||||
context.performBlock {
|
||||
guard let book = Book.fetch(bookID, context: context) else {return}
|
||||
book.removeCache()
|
||||
guard let book = Book.fetch(bookID, context: self.context) else {return}
|
||||
book.removeResumeData()
|
||||
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 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) {
|
||||
super.init(parent: nil, userInfo: [NSProgressFileOperationKindKey: NSProgressFileOperationKindDownloading])
|
||||
self.kind = NSProgressKindFile
|
||||
@ -40,7 +30,7 @@ class DownloadProgress: NSProgress {
|
||||
// MARK: - Descriptions
|
||||
|
||||
var fractionCompletedDescription: String? {
|
||||
return percentFormatter.stringFromNumber(NSNumber(double: fractionCompleted))
|
||||
return DownloadTask.percentFormatter.stringFromNumber(NSNumber(double: fractionCompleted))
|
||||
}
|
||||
|
||||
var progressAndSpeedDescription: String! {
|
||||
|
@ -91,6 +91,12 @@ class Preference {
|
||||
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 {
|
||||
@ -108,4 +114,6 @@ extension DefaultsKeys {
|
||||
static let libraryHasShownPreferredLanguagePrompt = DefaultsKey<Bool>("libraryHasShownPreferredLanguagePrompt")
|
||||
static let langFilterSortByAlphabeticalAsc = DefaultsKey<Bool>("langFilterSortByAlphabeticalAsc")
|
||||
static let langFilterNameDisplayInOriginalLocale = DefaultsKey<Bool>("langFilterNameDisplayInOriginalLocale")
|
||||
|
||||
static let resumeData = DefaultsKey<[String: AnyObject]>("resumeData")
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user