Library category view (#390)

* category view

* build number

* LibraryCategoryView

* fix

* FaviconDownloadService

* remove old code

* build number
This commit is contained in:
ChrisLi 2021-08-25 17:27:48 -04:00 committed by GitHub
parent 98ca3b6473
commit c865da5515
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 106 additions and 58 deletions

View File

@ -38,6 +38,7 @@
9768C23B1F4B7F6300FD499B /* Preference.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9768C23A1F4B7F6300FD499B /* Preference.swift */; };
976A65B32659489F009A97C6 /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976A65B22659489F009A97C6 /* SearchResult.swift */; };
976A65B42659489F009A97C6 /* SearchResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 976A65B22659489F009A97C6 /* SearchResult.swift */; };
977959C026D494C500D48E4A /* FaviconDownloadService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977959BF26D494C500D48E4A /* FaviconDownloadService.swift */; };
9779A5B22456793600F6F6FF /* LibraryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9779A59C2456793500F6F6FF /* LibraryService.swift */; };
9779A5B42456793600F6F6FF /* BookmarkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9779A59D2456793500F6F6FF /* BookmarkService.swift */; };
9779A5B52456793600F6F6FF /* BookmarkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9779A59D2456793500F6F6FF /* BookmarkService.swift */; };
@ -234,6 +235,7 @@
9768C2341F4B7BAC00FD499B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
9768C23A1F4B7F6300FD499B /* Preference.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preference.swift; sourceTree = "<group>"; };
976A65B22659489F009A97C6 /* SearchResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResult.swift; sourceTree = "<group>"; };
977959BF26D494C500D48E4A /* FaviconDownloadService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconDownloadService.swift; sourceTree = "<group>"; };
9779A59C2456793500F6F6FF /* LibraryService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryService.swift; sourceTree = "<group>"; };
9779A59D2456793500F6F6FF /* BookmarkService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BookmarkService.swift; sourceTree = "<group>"; };
9779A59F2456793500F6F6FF /* LibraryScanOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LibraryScanOperation.swift; sourceTree = "<group>"; };
@ -526,6 +528,7 @@
children = (
9779A5A62456793500F6F6FF /* ZimFileService */,
9779A5D52456796A00F6F6FF /* DownloadService.swift */,
977959BF26D494C500D48E4A /* FaviconDownloadService.swift */,
9779A59C2456793500F6F6FF /* LibraryService.swift */,
9779A59D2456793500F6F6FF /* BookmarkService.swift */,
9793548A257D86370076E94A /* Queries.swift */,
@ -1086,6 +1089,7 @@
9779A7AF2456796B00F6F6FF /* Settings.swift in Sources */,
97225A982618B2A200D8CB32 /* LibraryLanguageView.swift in Sources */,
972521012009616B00B60A80 /* EmptyContentView.swift in Sources */,
977959C026D494C500D48E4A /* FaviconDownloadService.swift in Sources */,
9797435C257BF33600D30F03 /* WebViewController.swift in Sources */,
97A36C571F8D5FCD0079B452 /* LibraryController.swift in Sources */,
97A36C321F8C21210079B452 /* AppDelegate.swift in Sources */,
@ -1424,7 +1428,7 @@
CODE_SIGN_ENTITLEMENTS = iOS/Support/Kiwix.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 66;
CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = L7HWM3SP3L;
ENABLE_BITCODE = YES;
GCC_C_LANGUAGE_STANDARD = c11;
@ -1464,7 +1468,7 @@
CODE_SIGN_ENTITLEMENTS = iOS/Support/Kiwix.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 66;
CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = L7HWM3SP3L;
ENABLE_BITCODE = YES;
GCC_C_LANGUAGE_STANDARD = c11;
@ -1556,7 +1560,7 @@
CODE_SIGN_ENTITLEMENTS = iOS/BookmarksWidget/Bookmarks.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 66;
CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = L7HWM3SP3L;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = iOS/BookmarksWidget/Info.plist;
@ -1588,7 +1592,7 @@
CODE_SIGN_ENTITLEMENTS = iOS/BookmarksWidget/Bookmarks.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 66;
CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = L7HWM3SP3L;
GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = iOS/BookmarksWidget/Info.plist;

View File

@ -6,7 +6,6 @@
// Copyright © 2020 Chris Li. All rights reserved.
//
import CoreData
import os
import Defaults
import RealmSwift

View File

@ -0,0 +1,77 @@
//
// FaviconDownloadService.swift
// Kiwix
//
// Created by Chris Li on 8/23/21.
// Copyright © 2021 Chris Li. All rights reserved.
//
import os
import RealmSwift
class FaviconDownloadService: NSObject, URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate {
static let shared = FaviconDownloadService()
private let queue = DispatchQueue(label: "org.kiwix.faviconDownload")
private var cache = [String: Data]()
private var retryCounter = [String: Int]()
private lazy var session: URLSession = {
var configuration = URLSessionConfiguration.default
configuration.httpMaximumConnectionsPerHost = 1
let operationQueue = OperationQueue()
operationQueue.underlyingQueue = queue
return URLSession(configuration: configuration, delegate: self, delegateQueue: operationQueue)
}()
private override init() { }
func download(zimFile: ZimFile) {
guard let faviconURL = zimFile.faviconURL, let url = URL(string: faviconURL) else { return }
let task = session.dataTask(with: url)
task.taskDescription = zimFile.fileID
task.resume()
}
private func retry(zimFileID: String) {
guard retryCounter[zimFileID, default: 0] < 3 else { return }
os_log("Retry downloading favicon data: %@.", log: Log.FaviconDownloadService, type: .info, zimFileID)
retryCounter[zimFileID, default: 0] += 1
queue.asyncAfter(deadline: DispatchTime.now() + 5) {
guard let database = try? Realm(),
let zimFile = database.object(ofType: ZimFile.self, forPrimaryKey: zimFileID) else { return }
self.download(zimFile: zimFile)
}
}
// MARK: - Delegates
func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let zimFileID = dataTask.taskDescription else { return }
var faviconData = cache[zimFileID, default: Data()]
faviconData.append(data)
cache[zimFileID] = faviconData
}
func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let zimFileID = task.taskDescription else { return }
defer { cache[zimFileID] = nil }
// retry download later if request was unsuccessful
guard let response = task.response as? HTTPURLResponse, response.statusCode == 200 else {
retry(zimFileID: zimFileID)
return
}
// save favicon data to database
do {
let database = try Realm()
try database.write {
guard let zimFile = database.object(ofType: ZimFile.self, forPrimaryKey: zimFileID) else { return }
zimFile.faviconData = cache[zimFileID]
}
} catch {
os_log("Failed to save favicon data.", log: Log.FaviconDownloadService, type: .error)
}
}
}

View File

@ -88,31 +88,4 @@ class LibraryService {
}
} catch {}
}
// MARK: - Favicon Download
/// Download and save favicon data of a zim file.
/// - Parameters:
/// - zimFileID: ID of a zim file
/// - url: URL of the favicon data
@available(iOS 13.0, *)
func downloadFavicons(zimFiles: [ZimFile]) {
let urls = Set(zimFiles.compactMap { $0.faviconURL }.compactMap { URL(string: $0) })
let publishers = urls.map { URLSession.shared.dataTaskPublisher(for: $0) }
faviconDownloadPipeline = Combine.Publishers.MergeMany(publishers)
.collect(5)
.sink(receiveCompletion: { _ in }, receiveValue: { results in
do {
let database = try Realm()
try database.write {
for result in results {
guard let url = result.response.url else { continue }
database.objects(ZimFile.self)
.filter("faviconURL = %@", url.absoluteString)
.forEach { zimFile in zimFile.faviconData = result.data }
}
}
} catch {}
})
}
}

View File

@ -12,6 +12,7 @@ private let subsystem = "org.kiwix.kiwix"
struct Log {
static let DownloadService = OSLog(subsystem: subsystem, category: "DownloadService")
static let FaviconDownloadService = OSLog(subsystem: subsystem, category: "FaviconDownloadService")
static let LibraryService = OSLog(subsystem: subsystem, category: "LibraryService")
static let OPDS = OSLog(subsystem: subsystem, category: "OPDS")
static let URLSchemeHandler = OSLog(subsystem: subsystem, category: "URLSchemeHandler")

View File

@ -64,26 +64,33 @@ struct LibraryCategoryView: View {
@Published private(set) var zimFiles = [String: [ZimFile]]()
let category: ZimFile.Category
private let database = try? Realm()
private let queue = DispatchQueue(label: "org.kiwix.library.category", qos: .userInitiated)
private var defaultsSubscriber: AnyCancellable?
private var languageCodeObserver: Defaults.Observation?
private var collectionSubscriber: AnyCancellable?
init(category: ZimFile.Category) {
self.category = category
defaultsSubscriber = Defaults.publisher(.libraryLanguageCodes)
.sink(receiveValue: { languageCodes in
self.loadData(languageCodes: languageCodes.newValue)
self.downloadFavicon(languageCodes: languageCodes.newValue)
})
languageCodeObserver = Defaults.observe(.libraryLanguageCodes) { languageCodes in
self.loadData(languageCodes: languageCodes.newValue)
self.database?.objects(ZimFile.self)
.filter(NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "categoryRaw = %@", category.rawValue),
NSPredicate(format: "languageCode IN %@", languageCodes.newValue),
NSPredicate(format: "faviconData = nil"),
NSPredicate(format: "faviconURL != nil"),
]))
.forEach { FaviconDownloadService.shared.download(zimFile: $0) }
}
}
private func loadData(languageCodes: [String]) {
let database = try? Realm()
var predicates = [NSPredicate(format: "categoryRaw = %@", category.rawValue)]
if !languageCodes.isEmpty {
predicates.append(NSPredicate(format: "languageCode IN %@", languageCodes))
}
collectionSubscriber = database?.objects(ZimFile.self)
.filter(NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "categoryRaw = %@", category.rawValue),
NSPredicate(format: "languageCode IN %@", languageCodes),
]))
.filter(NSCompoundPredicate(andPredicateWithSubpredicates: predicates))
.sorted(by: [
SortDescriptor(keyPath: "title", ascending: true),
SortDescriptor(keyPath: "size", ascending: false)
@ -91,6 +98,7 @@ struct LibraryCategoryView: View {
.collectionPublisher
.subscribe(on: queue)
.freeze()
.throttle(for: 0.2, scheduler: queue, latest: true)
.map { zimFiles in
var results = [String: [ZimFile]]()
for zimFile in zimFiles {
@ -109,19 +117,5 @@ struct LibraryCategoryView: View {
}
})
}
private func downloadFavicon(languageCodes: [String]) {
do {
let database = try Realm()
let zimFiles = database.objects(ZimFile.self)
.filter(NSCompoundPredicate(andPredicateWithSubpredicates: [
NSPredicate(format: "categoryRaw = %@", category.rawValue),
NSPredicate(format: "languageCode IN %@", languageCodes),
NSPredicate(format: "faviconData = nil"),
NSPredicate(format: "faviconURL != nil"),
]))
LibraryService.shared.downloadFavicons(zimFiles: Array(zimFiles))
} catch {}
}
}
}

View File

@ -39,6 +39,6 @@ struct LibrarySearchResultView: View {
NSPredicate(format: "title CONTAINS[cd] %@", searchText),
NSPredicate(format: "languageCode IN %@", Defaults[.libraryLanguageCodes]),
])
LibraryService.shared.downloadFavicons(zimFiles: zimFiles.filter { $0.faviconData == nil })
// LibraryService.shared.downloadFavicons(zimFiles: zimFiles.filter { $0.faviconData == nil })
}
}