This commit is contained in:
Chris Li 2016-12-24 16:19:03 -05:00
parent 73a720194f
commit 3bbb2b1f23
10 changed files with 8 additions and 531 deletions

View File

@ -34,6 +34,8 @@ class Buttons: LPTBarButtonItemDelegate {
delegate?.didTapForwardButton()
case toc:
delegate?.didTapTOCButton()
case library:
delegate?.didTapLibraryButton()
default:
return
}

View File

@ -49,7 +49,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>1.8.3669</string>
<string>1.8.3683</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>

View File

@ -21,7 +21,7 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.8.3687</string>
<string>1.8.3701</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionMainStoryboard</key>

View File

@ -214,7 +214,6 @@
973DD4271D36E3E4009D45DB /* SettingDetailController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SettingDetailController.swift; path = "Kiwix-iOS/Controller/Setting/SettingDetailController.swift"; sourceTree = SOURCE_ROOT; };
974C49621DA307FF00E276E1 /* injection.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = injection.js; sourceTree = "<group>"; };
974C49671DA4266200E276E1 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; };
974F33C51D99CAD700879D35 /* BookmarkOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkOperation.swift; sourceTree = "<group>"; };
975227CA1D0227E8001D1DDE /* Main.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Main.storyboard; path = "Kiwix-iOS/Storyboard/Main.storyboard"; sourceTree = SOURCE_ROOT; };
975227CB1D0227E8001D1DDE /* Setting.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = Setting.storyboard; path = "Kiwix-iOS/Storyboard/Setting.storyboard"; sourceTree = SOURCE_ROOT; };
975227CF1D022814001D1DDE /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = LaunchScreen.storyboard; path = "Kiwix-iOS/Storyboard/LaunchScreen.storyboard"; sourceTree = SOURCE_ROOT; };
@ -273,7 +272,6 @@
97D4D64E1D874E6E00C1B065 /* SearchOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchOperation.swift; sourceTree = "<group>"; };
97D6811A1D6E2A7100E5FA99 /* DownloadTasksController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadTasksController.swift; sourceTree = "<group>"; };
97D6811C1D6F70AC00E5FA99 /* GlobalQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GlobalQueue.swift; sourceTree = "<group>"; };
97D6811D1D6F70AC00E5FA99 /* RefreshLibraryOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshLibraryOperation.swift; sourceTree = "<group>"; };
97D6811E1D6F70AC00E5FA99 /* ScanLocalBook.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScanLocalBook.swift; sourceTree = "<group>"; };
97D681211D6F70AC00E5FA99 /* UpdateWidgetDataSourceOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateWidgetDataSourceOperation.swift; sourceTree = "<group>"; };
97D681221D6F70AC00E5FA99 /* URLSessionDownloadTaskOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSessionDownloadTaskOperation.swift; sourceTree = "<group>"; };
@ -291,7 +289,6 @@
97D6813D1D6F712800E5FA99 /* DownloadTask+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "DownloadTask+CoreDataProperties.swift"; path = "Classes/DownloadTask+CoreDataProperties.swift"; sourceTree = "<group>"; };
97D6813E1D6F712800E5FA99 /* Language+CoreDataProperties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Language+CoreDataProperties.swift"; path = "Classes/Language+CoreDataProperties.swift"; sourceTree = "<group>"; };
97DB65D91D4576B600A2CC42 /* BookmarkWidgetCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkWidgetCell.swift; sourceTree = "<group>"; };
97DF259B1D6F7612001648A3 /* BookOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookOperation.swift; sourceTree = "<group>"; };
97DF259F1D6F996B001648A3 /* Network.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Network.swift; sourceTree = "<group>"; };
97E609F01D103DED00EBCB9D /* NotificationCenter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NotificationCenter.framework; path = System/Library/Frameworks/NotificationCenter.framework; sourceTree = SDKROOT; };
97E60A011D10423A00EBCB9D /* Others.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Others.swift; sourceTree = "<group>"; };
@ -372,16 +369,6 @@
name = Frameworks;
sourceTree = "<group>";
};
970A2A201DD562B60078BB7C /* old */ = {
isa = PBXGroup;
children = (
974F33C51D99CAD700879D35 /* BookmarkOperation.swift */,
97DF259B1D6F7612001648A3 /* BookOperation.swift */,
97D6811D1D6F70AC00E5FA99 /* RefreshLibraryOperation.swift */,
);
name = old;
sourceTree = "<group>";
};
971187051CEB426E00B9909D /* libkiwix */ = {
isa = PBXGroup;
children = (
@ -784,7 +771,6 @@
97E5712A1CA0525300FF4F1D /* Operation */ = {
isa = PBXGroup;
children = (
970A2A201DD562B60078BB7C /* old */,
97D6811C1D6F70AC00E5FA99 /* GlobalQueue.swift */,
9764CBD21D8083AA00072D6A /* ArticleOperation.swift */,
970A2A211DD562CB0078BB7C /* BookOperations.swift */,

View File

@ -2,16 +2,4 @@
<Bucket
type = "0"
version = "2.0">
<Breakpoints>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.ExceptionBreakpoint">
<BreakpointContent
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
scope = "0"
stopOnStyle = "0">
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View File

@ -1,218 +0,0 @@
//
// DownloadBookOperation.swift
// Kiwix
//
// Created by Chris Li on 8/25/16.
// Copyright © 2016 Chris Li. All rights reserved.
//
import UIKit
import CoreData
import ProcedureKit
class DownloadBookOperation: URLSessionDownloadTaskOperation {
let bookID: String?
let progress: DownloadProgress
override init(downloadTask: URLSessionDownloadTask) {
progress = DownloadProgress(completedUnitCount: downloadTask.countOfBytesReceived, totalUnitCount: downloadTask.countOfBytesExpectedToReceive)
bookID = downloadTask.taskDescription
super.init(downloadTask: downloadTask)
name = downloadTask.taskDescription
if UIApplication.shared.applicationState == .active,
let url = downloadTask.originalRequest?.url {
add(ReachabilityCondition(url: url, connectivity: .ViaWiFi))
}
// Update Coredata
let context = NSManagedObjectContext.mainQueueContext
context.performAndWait {
guard let bookID = self.bookID,
let book = Book.fetch(bookID, context: context),
let downloadTask = DownloadTask.fetch(book, context: context) else {return}
book.state = .downloading
downloadTask.state = .queued
// Overwrite progress
self.progress.completedUnitCount = book.downloadTask?.totalBytesWritten ?? 0
self.progress.totalUnitCount = book.fileSize
}
}
convenience init?(bookID: String, resumeData: Data) {
if #available(iOS 10.0, *) {
guard let data = DownloadBookOperation.correctFuckingResumeData(resumeData) else {return nil}
let downloadTask = Network.shared.session.downloadTask(withResumeData: data)
downloadTask.taskDescription = bookID
self.init(downloadTask: downloadTask)
} else {
let downloadTask = Network.shared.session.downloadTask(withResumeData: resumeData)
downloadTask.taskDescription = bookID
self.init(downloadTask: downloadTask)
}
}
convenience init?(bookID: String) {
let context = NSManagedObjectContext.mainQueueContext
guard let book = Book.fetch(bookID, context: context),
let url = book.url else { return nil }
let task = Network.shared.session.downloadTask(with: url)
task.taskDescription = bookID
self.init(downloadTask: task)
}
override func operationDidCancel() {
// Not Reachable
if let error = errors.first as? Operations.ReachabilityCondition.Error, error == .NotReachable {
return
}
// Update Core Data
if produceResumeData {
let context = NSManagedObjectContext.mainQueueContext
context.performAndWait({
guard let bookID = self.bookID,
let book = Book.fetch(bookID, context: context) else {return}
book.state = .downloading
})
} else {
let context = NSManagedObjectContext.mainQueueContext
context.performAndWait({
guard let bookID = self.bookID,
let book = Book.fetch(bookID, context: context) else {return}
book.state = .cloud
guard let downloadTask = book.downloadTask else {return}
context.delete(downloadTask)
})
}
// URLSessionDelegate save resume data and update downloadTask
}
// MARK: - Helper
fileprivate class func correctFuckingResumeData(_ data: Data?) -> Data? {
let kResumeCurrentRequest = "NSURLSessionResumeCurrentRequest"
let kResumeOriginalRequest = "NSURLSessionResumeOriginalRequest"
guard let data = data, let resumeDictionary = (try? PropertyListSerialization.propertyList(from: data, options: [.mutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else {
return nil
}
resumeDictionary[kResumeCurrentRequest] = correctFuckingRequestData(resumeDictionary[kResumeCurrentRequest] as? Data)
resumeDictionary[kResumeOriginalRequest] = correctFuckingRequestData(resumeDictionary[kResumeOriginalRequest] as? Data)
let result = try? PropertyListSerialization.data(fromPropertyList: resumeDictionary, format: PropertyListSerialization.PropertyListFormat.xml, options: PropertyListSerialization.WriteOptions())
return result
}
fileprivate class func correctFuckingRequestData(_ data: Data?) -> Data? {
guard let data = data else {
return nil
}
if NSKeyedUnarchiver.unarchiveObject(with: data) != nil {
return data
}
guard let archive = (try? PropertyListSerialization.propertyList(from: data, options: [.mutableContainersAndLeaves], format: nil)) as? NSMutableDictionary else {
return nil
}
// Rectify weird __nsurlrequest_proto_props objects to $number pattern
var k = 0
while archive["$objects"]?[1].object(forKey: "$\(k)") != nil {
k += 1
}
var i = 0
while archive["$objects"]?[1].object(forKey: "__nsurlrequest_proto_prop_obj_\(i)") != nil {
let arr = archive["$objects"] as? NSMutableArray
if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_prop_obj_\(i)"] {
dic.setObject(obj, forKey: "$\(i + k)" as NSCopying)
dic.removeObject(forKey: "__nsurlrequest_proto_prop_obj_\(i)")
arr?[1] = dic
archive["$objects"] = arr
}
i += 1
}
if archive["$objects"]?[1]["__nsurlrequest_proto_props"] != nil {
let arr = archive["$objects"] as? NSMutableArray
if let dic = arr?[1] as? NSMutableDictionary, let obj = dic["__nsurlrequest_proto_props"] {
dic.setObject(obj, forKey: "$\(i + k)" as NSCopying)
dic.removeObject(forKey: "__nsurlrequest_proto_props")
arr?[1] = dic
archive["$objects"] = arr
}
}
// Rectify weird "NSKeyedArchiveRootObjectKey" top key to NSKeyedArchiveRootObjectKey = "root"
if archive["$top"]?["NSKeyedArchiveRootObjectKey"] != nil {
(archive["$top"]? as AnyObject).set(archive["$top"]?["NSKeyedArchiveRootObjectKey"], forKey: NSKeyedArchiveRootObjectKey)
(archive["$top"]? as AnyObject).removeObject(forKey: "NSKeyedArchiveRootObjectKey")
}
// Re-encode archived object
let result = try? PropertyListSerialization.data(fromPropertyList: archive, format: PropertyListSerialization.PropertyListFormat.binary, options: PropertyListSerialization.WriteOptions())
return result
}
}
class RemoveBookOperation: Procedure {
let bookID: String
init(bookID: String) {
self.bookID = bookID
super.init()
}
override func execute() {
let context = NSManagedObjectContext.mainQueueContext
context.performAndWait {
guard let zimFileURL = ZimMultiReader.shared.readers[self.bookID]?.fileURL else {return}
_ = try? FileManager.default.removeItem(at: zimFileURL)
// Core data is updated by scan book operation
// Article removal is handled by cascade relationship
guard let idxFolderURL = ZimMultiReader.shared.readers[self.bookID]?.idxFolderURL else {return}
_ = try? FileManager.default.removeItem(at: idxFolderURL)
}
finish()
}
}
class PauseBookDwonloadOperation: Procedure {
let bookID: String
init(bookID: String) {
self.bookID = bookID
super.init()
}
override func execute() {
Network.shared.operations[bookID]?.cancel(produceResumeData: true)
finish()
}
}
class ResumeBookDwonloadOperation: Procedure {
let bookID: String
init(bookID: String) {
self.bookID = bookID
super.init()
name = "Resume Book Dwonload Operation, bookID = \(bookID)"
}
override func execute() {
guard let data: Data = Preference.resumeData[bookID] as Data?,
let operation = DownloadBookOperation(bookID: bookID, resumeData: data) else {
if let operation = DownloadBookOperation(bookID: bookID) {
produce(operation)
}
finish()
return
}
Network.shared.queue.addOperation(operation)
finish()
}
}

View File

@ -1,150 +0,0 @@
//
// BookmarkMigrationOperation.swift
// Kiwix
//
// Created by Chris Li on 9/26/16.
// Copyright © 2016 Chris Li. All rights reserved.
//
import CoreData
import CloudKit
import ProcedureKit
class BookmarkMigrationOperation: Procedure {
private let context: NSManagedObjectContext
override init() {
self.context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = NSManagedObjectContext.mainQueueContext
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
super.init()
add(condition: MutuallyExclusive<GlobalQueue>())
name = String(describing: self)
}
override func execute() {
context.performAndWait {
let pids = Book.fetchLocal(self.context).flatMap({$1.pid})
for pid in pids {
var books = Book.fetch(pid: pid, context: self.context)
let latestBook = books.removeFirst()
for book in books {
book.articles.forEach({$0.book = latestBook})
}
}
if self.context.hasChanges {_ = try? self.context.save()}
}
finish()
}
}
class BookmarkTrashOperation: Procedure {
private let context: NSManagedObjectContext
private let articles: [Article]
init(articles: [Article]) {
self.context = NSManagedObjectContext.mainQueueContext
self.articles = articles
super.init()
// add(condition: MutuallyExclusive<BookmarkController>())
name = String(describing: self)
}
override func execute() {
context.perform {
self.articles.forEach() {
$0.isBookmarked = false
$0.bookmarkDate = nil
}
// Get books whose zim file removed, but are retain by bookmarks, and whose bookmarks are all removed
let books = Set(self.articles.flatMap({$0.book}))
.filter({Article.fetchBookmarked(in: $0, with: self.context).count == 0 && $0.state == .retained})
books.forEach({ (book) in
if let _ = book.meta4URL {
book.state = .cloud
} else {
self.context.delete(book)
}
})
if self.context.hasChanges {_ = try? self.context.save()}
}
if articles.count > 0 {
// produce(UpdateWidgetDataSourceOperation())
}
finish()
}
}
class BookmarkCloudKitOperation: Procedure {
let article: Article
init(article: Article) {
self.article = article
super.init()
name = String(describing: self)
}
override func execute() {
// guard let bookID = article.book?.id else {finish(); return}
// let container = CKContainer(identifier: "iCloud.org.kiwix")
// container.accountStatusWithCompletionHandler { (status, error) in
// guard status == .Available else {self.finish(); return}
//
// container.fetchUserRecordIDWithCompletionHandler({ (recordID, error) in
// guard let ownerName = recordID?.recordName else {self.finish(); return}
// let database = container.privateCloudDatabase
// let zoneID = CKRecordZoneID(zoneName: bookID, ownerName: ownerName)
// database.fetchRecordZoneWithID(zoneID, completionHandler: { (zone, error) in
// if let zone = zone {
//
// } else {
// database.
// }
// })
// })
// }
//
//
//
// guard let bookID = article.book?.id else {finish(); return}
//
// let recordID = CKRecordID(recordName: bookID + "|" + article.path)
// let database = CKContainer(identifier: "iCloud.org.kiwix").privateCloudDatabase
//
// database.fetchRecordWithID(recordID) { (record, error) in
// if let record = record {
// if self.article.isBookmarked {
// self.populate(record, with: self.article)
// database.saveRecord(record, completionHandler: { (record, error) in
// self.finish()
// })
// } else {
// database.deleteRecordWithID(recordID, completionHandler: { (recordID, error) in
// self.finish()
// })
// }
// } else {
// guard self.article.isBookmarked else {self.finish(); return}
// let record = CKRecord(recordType: "Article", recordID: recordID)
// self.populate(record, with: self.article)
// database.saveRecord(record, completionHandler: { (record, error) in
// self.finish()
// })
// }
// }
}
func populate(_ record: CKRecord, with article: Article) {
record["path"] = self.article.path as CKRecordValue?
record["title"] = self.article.title as CKRecordValue?
record["snippet"] = self.article.snippet as CKRecordValue?
}
}

View File

@ -67,9 +67,10 @@ fileprivate class Process: Procedure, InputProcedure, XMLParserDelegate {
let toBeDeleted = storeBookIDs.subtracting(memoryBookIDs)
hasUpdate = toBeDeleted.count > 0
context.performAndWait {
toBeDeleted.forEach({ (id) in
})
for id in toBeDeleted {
guard let book = Book.fetch(id, context: self.context) else {continue}
self.context.delete(book)
}
}
if context.hasChanges { try? context.save() }

View File

@ -1,131 +0,0 @@
//
// RefreshLibraryOperation.swift
// Kiwix
//
// Created by Chris Li on 2/7/16.
// Copyright © 2016 Chris Li. All rights reserved.
//
import CoreData
import ProcedureKit
class RefreshLibraryOperation: GroupProcedure {
private(set) var hasUpdate = false
private(set) var firstTime = Preference.libraryLastRefreshTime == nil
init(invokedByUser: Bool = false) {
let retrieve = Retrieve()
let process = Process()
process.injectResultFromDependency(retrieve)
super.init(operations: [retrieve, process])
addObserver(NetworkObserver())
if UIApplication.sharedApplication().applicationState == .Active {
add(ReachabilityCondition(url: Retrieve.url))
}
addObserver(WillExecuteObserver { _ in
(UIApplication.sharedApplication().delegate as! AppDelegate).registerNotification()
})
process.addObserver(DidFinishObserver { [unowned self] (operation, errors) in
guard let operation = operation as? Process else {return}
self.hasUpdate = operation.hasUpdate
self.firstTime = operation.firstTime
})
}
}
private class Retrieve: Procedure, ResultInjection {
private static let url = URL(string: "https://download.kiwix.org/library/library.xml")!
private var result: Data?
override init() {
super.init()
name = "Library Retrieve"
}
fileprivate override func execute() {
guard !isCancelled else {return}
let task = URLSession.shared.dataTask(with: Retrieve.url, completionHandler: { (data, response, error) in
self.result = data
self.finish()
})
task.resume()
}
}
private class Process: Procedure, XMLParserDelegate, AutomaticInjectionOperationType {
var requirement: Data?
fileprivate(set) var hasUpdate = false
fileprivate(set) var firstTime = false
private let context: NSManagedObjectContext
private var oldBookIDs = Set<String>()
private var newBookIDs = Set<String>()
override init() {
self.context = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
context.parent = NSManagedObjectContext.mainQueueContext
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
super.init()
name = "Library Process"
}
override fileprivate func execute() {
guard let data = requirement else {finish(); return}
let xmlParser = XMLParser(data: data)
xmlParser.delegate = self
xmlParser.parse()
finish()
}
fileprivate func saveManagedObjectContexts() {
context.performAndWait { self.context.saveIfNeeded() }
context.parent?.performAndWait { self.context.parent?.saveIfNeeded() }
}
// MARK: NSXMLParser Delegate
@objc fileprivate func parserDidStartDocument(_ parser: XMLParser) {
context.performAndWait { () -> Void in
self.oldBookIDs = Set(Book.fetchAll(self.context).map({ $0.id }))
}
}
@objc fileprivate func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String]) {
guard elementName == "book",
let id = attributeDict["id"] else {return}
if !oldBookIDs.contains(id) {
hasUpdate = true
context.performAndWait({ () -> Void in
Book.add(attributeDict, context: self.context)
})
}
newBookIDs.insert(id)
}
@objc fileprivate func parserDidEndDocument(_ parser: XMLParser) {
let idsToDelete = oldBookIDs.subtracting(newBookIDs)
context.performAndWait({ () -> Void in
idsToDelete.forEach({ (id) in
guard let book = Book.fetch(id, context: self.context) else {return}
// Delete Book object only if book is online, i.e., is not associated with a download task or is not local
guard book.state == .cloud else {return}
self.context.delete(book)
self.hasUpdate = true
})
})
saveManagedObjectContexts()
firstTime = Preference.libraryLastRefreshTime == nil
Preference.libraryLastRefreshTime = Date()
}
@objc fileprivate func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) {
saveManagedObjectContexts()
}
}

View File

@ -1,4 +1,3 @@
//
// ZimReader.m
// KiwixTest