mirror of
https://github.com/CloverHackyColor/CloverBootloader.git
synced 2024-11-27 12:15:19 +01:00
0a5b4bd8bf
Support for recently added drivers. Clover.app has some fixes for the UI.
1034 lines
38 KiB
Swift
1034 lines
38 KiB
Swift
//
|
|
// ThemeManager.swift
|
|
// ThemeManager
|
|
//
|
|
// Created by vector sigma on 04/01/2020.
|
|
// Copyright © 2020 vectorsigma. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import CommonCrypto
|
|
|
|
let kThemeInfoFile = ".CTv1_i"
|
|
let kThemeUserKey = "themeUser"
|
|
let kThemeRepoKey = "themeRepo"
|
|
|
|
let kDefaultThemeUser = "CloverHackyColor" // CloverHackyColor
|
|
let kDefaultThemeRepo = "CloverThemes"
|
|
|
|
enum ThemeDownload {
|
|
case indexOnly
|
|
case thumbnail
|
|
case complete
|
|
}
|
|
|
|
enum GitProtocol : String {
|
|
case https = "https"
|
|
case git = "git"
|
|
}
|
|
|
|
struct ThemeInfo {
|
|
var user: String
|
|
var repo: String
|
|
var optimized : Bool
|
|
|
|
func archive() -> Data? {
|
|
var dict = [String : String]()
|
|
dict["user"] = self.user
|
|
dict["repo"] = self.repo
|
|
dict["optimized"] = self.optimized ? "1" : "0"
|
|
|
|
return try? JSONEncoder().encode(dict)
|
|
}
|
|
|
|
static func unarchive(at path: String) -> ThemeInfo? {
|
|
if let data = try? Data(contentsOf: URL(fileURLWithPath: path)) {
|
|
|
|
if let dict = try? JSONSerialization.jsonObject(with: data, options: .mutableContainers) as? [String : String] {
|
|
|
|
let new : ThemeInfo = ThemeInfo(user: dict["user"] ?? "",
|
|
repo: dict["repo"] ?? "",
|
|
optimized: (dict["optimized"] == "1") ? true : false)
|
|
/*
|
|
print(new.user)
|
|
print(new.repo)
|
|
print(new.optimized)
|
|
*/
|
|
return new
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
let kCloverThemeAttributeKey = "org.cloverTheme.sha"
|
|
|
|
final class ThemeManager: NSObject, URLSessionDataDelegate {
|
|
private let errorDomain : String = "org.slice.Clover.ThemeManager.Error"
|
|
var statusError : Error? = nil
|
|
var delegate : ThemeManagerVC?
|
|
var user : String
|
|
var repo : String
|
|
var basePath : String
|
|
private var urlBaseStr : String
|
|
var themeManagerIndexDir : String
|
|
private var indexing : Bool = false
|
|
|
|
let userAgent = "Clover"
|
|
|
|
required init(user: String, repo: String,
|
|
basePath: String,
|
|
indexDir : String,
|
|
delegate: ThemeManagerVC?) {
|
|
self.user = user
|
|
self.repo = repo
|
|
self.basePath = basePath
|
|
self.urlBaseStr = "\(GitProtocol.https.rawValue)://api.github.com/repos/\(user)/\(repo)/git/trees/master?recursive=1"
|
|
self.themeManagerIndexDir = indexDir
|
|
self.delegate = delegate
|
|
if !fm.fileExists(atPath: self.themeManagerIndexDir) {
|
|
try? fm.createDirectory(atPath: self.themeManagerIndexDir,
|
|
withIntermediateDirectories: true,
|
|
attributes: nil)
|
|
}
|
|
}
|
|
|
|
public func getIndexedThemesForAllRepositories() -> [String] {
|
|
self.statusError = nil
|
|
var themes = [String]()
|
|
let tip = NSHomeDirectory().addPath("Library/Application Support/CloverApp/Themeindex")
|
|
|
|
if let repos = try? fm.contentsOfDirectory(atPath: tip) {
|
|
for repo in repos {
|
|
let repoThemesDir = tip.addPath(repo).addPath("Themes")
|
|
let repoShaFilePath = tip.addPath(repo).addPath("sha")
|
|
if fm.fileExists(atPath: repoShaFilePath) && fm.fileExists(atPath: repoThemesDir) {
|
|
if let files : [String] = try? fm.contentsOfDirectory(atPath: repoThemesDir) {
|
|
for f in files {
|
|
let theme : String = f.deletingFileExtension
|
|
let ext : String = f.fileExtension
|
|
if ext == "plist" && !themes.contains(theme) {
|
|
themes.append(theme)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return themes.sorted()
|
|
}
|
|
|
|
public func getIndexedThemes() -> [String] {
|
|
self.statusError = nil
|
|
var themes = [String]()
|
|
if self.getSha() != nil {
|
|
let themesIndexPath = self.themeManagerIndexDir.addPath("Themes")
|
|
if let files : [String] = try? fm.contentsOfDirectory(atPath: themesIndexPath) {
|
|
for f in files {
|
|
let theme : String = f.deletingFileExtension
|
|
let ext : String = f.fileExtension
|
|
if ext == "plist" {
|
|
themes.append(theme)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return themes.sorted()
|
|
}
|
|
|
|
public func getThemes(completion: @escaping ([String]) -> ()) {
|
|
self.statusError = nil
|
|
var themes : [String] = [String]()
|
|
let themesIndexPath : String = self.themeManagerIndexDir.addPath("Themes")
|
|
|
|
self.getInfo(urlString: self.urlBaseStr) { (success) in
|
|
|
|
do {
|
|
let files : [String] = try fm.contentsOfDirectory(atPath: themesIndexPath)
|
|
for f in files {
|
|
let theme : String = f.deletingFileExtension
|
|
let ext : String = f.fileExtension
|
|
if ext == "plist" {
|
|
themes.append(theme)
|
|
}
|
|
}
|
|
completion(themes)
|
|
} catch {
|
|
completion(themes)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
/// Return the sha string only if the sha exist, and only if Themes directory exists.
|
|
func getSha() -> String? {
|
|
var sha : String? = nil
|
|
let themesPath : String = "\(self.themeManagerIndexDir)/Themes"
|
|
let shaPath : String = "\(self.themeManagerIndexDir)/sha"
|
|
|
|
if fm.fileExists(atPath: themesPath) && fm.fileExists(atPath: shaPath) {
|
|
if let data : Data = try? Data(contentsOf: URL(fileURLWithPath: shaPath)) {
|
|
sha = String(data: data, encoding: .utf8)
|
|
if ((sha != nil) && (sha!.count < 40)) {
|
|
sha = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return sha
|
|
}
|
|
|
|
private func normalize(_ url: String) -> String {
|
|
if (url.range(of: " ") != nil) {
|
|
return url.replacingOccurrences(of: " ", with: "%20")
|
|
}
|
|
return url
|
|
}
|
|
|
|
private func downloadFile(at url: String, dst: String,
|
|
completion: @escaping (Bool) -> ()) {
|
|
|
|
if let validURL : URL = URL(string: self.normalize(url)) {
|
|
let upperDir : String = dst.deletingLastPath
|
|
if !fm.fileExists(atPath: upperDir) {
|
|
do {
|
|
try fm.createDirectory(atPath: upperDir,
|
|
withIntermediateDirectories: true,
|
|
attributes: nil)
|
|
} catch {
|
|
let errStr = "DF0, \(error.localizedDescription)."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 1000,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
}
|
|
|
|
var request : URLRequest = URLRequest(url: validURL)
|
|
request.httpMethod = "GET"
|
|
request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent")
|
|
|
|
let config = URLSessionConfiguration.ephemeral
|
|
let session = URLSession(configuration: config)
|
|
|
|
let task = session.dataTask(with: request, completionHandler: {(d, r, e) in
|
|
if let response = r as? HTTPURLResponse {
|
|
switch response.statusCode {
|
|
case 200:
|
|
break
|
|
default:
|
|
let errStr = "DF1, Error: request for '\(validURL)' response with status code \(response.statusCode) (\(gHTTPInfo(for: response.statusCode)))."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 1001,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
} else {
|
|
let errStr = "DF2, Error: empty response for '\(validURL)'."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 1002,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
if (e != nil) {
|
|
let errStr = "DF3, \(e!.localizedDescription)."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 1003,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
guard let data = d else {
|
|
let errStr = "DF4, Error: no datas."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 1004,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
do {
|
|
try data.write(to: URL(fileURLWithPath: dst))
|
|
completion(true)
|
|
} catch {
|
|
let errStr = "DF5, \(error.localizedDescription)"
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 1005,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
self.statusError = se
|
|
completion(false)
|
|
}
|
|
})
|
|
|
|
task.resume()
|
|
} else {
|
|
let errStr = "DF6, Error: invalid url '\(url)'."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 1006,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
}
|
|
}
|
|
|
|
private func getInfo(urlString: String, completion: @escaping (Bool) -> ()) {
|
|
if let url = URL(string: self.normalize(urlString)) {
|
|
var request : URLRequest = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
request.setValue("application/json; charset=utf-8", forHTTPHeaderField: "Content-Type")
|
|
request.setValue(self.userAgent, forHTTPHeaderField: "User-Agent")
|
|
let config = URLSessionConfiguration.ephemeral
|
|
let session = URLSession(configuration: config)
|
|
|
|
let task = session.dataTask(with: request, completionHandler: {(d, r, e) in
|
|
if let reponse = r as? HTTPURLResponse {
|
|
if reponse.statusCode != 200 {
|
|
let errStr = "GI0, Error: request for '\(url)' reponse with status code \(reponse.statusCode) (\(gHTTPInfo(for: reponse.statusCode)))."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2000,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
}
|
|
|
|
if (e != nil) {
|
|
let errStr = "GI1, \(e!.localizedDescription)"
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2001,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
guard let data = d else {
|
|
let errStr = "GI2, no data"
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2002,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
guard let utf8 = String(decoding: data, as: UTF8.self).data(using: .utf8) else {
|
|
let errStr = "GI3, data is not utf8."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2003,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
do {
|
|
let json = try JSONSerialization.jsonObject(with: utf8, options: .mutableContainers)
|
|
|
|
if let jdict = json as? [String: Any] {
|
|
guard let truncated = jdict["truncated"] as? Bool else {
|
|
let errStr = "GI4, Error: 'truncated' key not found"
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2004,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
if truncated == true {
|
|
let errStr = "GI5, Error: json has truncated list."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2005,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
guard let sha = jdict["sha"] as? String else {
|
|
let errStr = "GI6, Error: 'sha' key not found"
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2006,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
if sha == self.getSha() {
|
|
// we're done because we already have all the files needed
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
guard let tree = jdict["tree"] as? [[String: Any]] else {
|
|
let errStr = "GI7, Error: 'tree' key not found, or not an array."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2007,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
let shaPath : String = "\(self.themeManagerIndexDir)/\(sha)"
|
|
|
|
do {
|
|
if !fm.fileExists(atPath: shaPath) {
|
|
try fm.createDirectory(atPath: shaPath, withIntermediateDirectories: true, attributes: nil)
|
|
}
|
|
|
|
} catch {
|
|
let errStr = "GI8, Error: can't write sha commit."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2008,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
for obj in tree {
|
|
/*
|
|
"path": ".gitignore",
|
|
"mode": "100644",
|
|
"type": "blob",
|
|
"sha": "e43b0f988953ae3a84b00331d0ccf5f7d51cb3cf",
|
|
"size": 10,
|
|
"url": "https://api.github.com/repos/vectorsigma72/CloverThemes/git/blobs/e43b0f988953ae3a84b00331d0ccf5f7d51cb3cf"
|
|
*/
|
|
|
|
guard let type = obj["type"] as? String else {
|
|
let errStr = "GI9, Error: 'type' key not found."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2009,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
break
|
|
}
|
|
|
|
if let path = obj["path"] as? String {
|
|
if path.componentsPath.count == 1 && type != "tree" {
|
|
// skip every things is not a directory in the root of the repository (like README.md)
|
|
continue
|
|
}
|
|
|
|
guard let fileSha = obj["sha"] as? String else {
|
|
let errStr = "GI16, Error: 'sha' key not found for \(path)."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2016,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
break
|
|
}
|
|
|
|
// skip every hidden files like .gitignore, .DS_Store, etc.
|
|
if !path.hasPrefix(".") && type == "blob" {
|
|
let themeName : String = path.components(separatedBy: "/")[0]
|
|
let plistPath : String = "\(self.themeManagerIndexDir)/\(sha)/\(themeName).plist"
|
|
let theme : NSMutableDictionary = NSMutableDictionary(contentsOfFile: plistPath) ?? NSMutableDictionary()
|
|
if !(theme.allKeys as! [String]).contains(path) {
|
|
theme.setObject(fileSha, forKey: path as NSString)
|
|
}
|
|
|
|
|
|
if !theme.write(toFile: plistPath, atomically: false) {
|
|
let errStr = "GI10, Error: can't write \(plistPath)"
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2010,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
break
|
|
}
|
|
}
|
|
} else {
|
|
let errStr = "GI11, Error: 'path' key not found."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2011,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
break
|
|
}
|
|
}
|
|
|
|
// move temp files in standard position
|
|
do {
|
|
try sha.write(toFile: "\(self.themeManagerIndexDir)/sha", atomically: true, encoding: .utf8)
|
|
if fm.fileExists(atPath: "\(self.themeManagerIndexDir)/Themes") {
|
|
try fm.removeItem(atPath: "\(self.themeManagerIndexDir)/Themes")
|
|
}
|
|
try fm.moveItem(atPath: shaPath, toPath: "\(self.themeManagerIndexDir)/Themes")
|
|
self.removeOld(new: sha)
|
|
completion(true)
|
|
} catch {
|
|
let errStr = "GI12, \(error.localizedDescription)"
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2012,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
return
|
|
}
|
|
|
|
//remove old themes (old sha)
|
|
} else {
|
|
let errStr = "GI13, json is not a dictionary (API change?)."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2013,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
}
|
|
|
|
} catch {
|
|
let errStr = "GI14, \(error.localizedDescription)"
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2014,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
}
|
|
})
|
|
|
|
task.resume()
|
|
} else {
|
|
let errStr = "GI15, \(urlString) is invalid."
|
|
let se : Error = NSError(domain: self.errorDomain,
|
|
code: 2015,
|
|
userInfo: [NSLocalizedDescriptionKey: errStr])
|
|
|
|
self.statusError = se
|
|
completion(false)
|
|
}
|
|
}
|
|
|
|
/// Remove old thumbnails from old sha commits
|
|
private func removeOld(new sha: String) {
|
|
do {
|
|
let contents : [String] = try fm.contentsOfDirectory(atPath: self.themeManagerIndexDir)
|
|
for c in contents {
|
|
if c != "sha" && c != "Themes" && c != sha {
|
|
try fm.removeItem(atPath: "\(self.themeManagerIndexDir)/\(c)")
|
|
}
|
|
}
|
|
} catch { }
|
|
}
|
|
|
|
private func thumbnailExist(at path: String) -> Bool {
|
|
if fm.fileExists(atPath: path.addPath("theme.svg")) {
|
|
return true
|
|
} else {
|
|
if fm.fileExists(atPath: path.addPath("theme.plist")) &&
|
|
fm.fileExists(atPath: path.addPath("screenshot.png")){
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Return the path for a given theme, if the download succeded
|
|
public func download(theme: String, down: ThemeDownload, completion: @escaping (String?) -> ()) {
|
|
let advice = "Try to refresh the list by pressing the Refresh button below."
|
|
self.statusError = nil
|
|
if let sha = self.getSha() {
|
|
let shaPath : String = self.basePath.addPath(sha)
|
|
let themeDest : String = (down == .complete)
|
|
? self.themeManagerIndexDir.addPath("Downloads").addPath(theme)
|
|
: shaPath.addPath(theme)
|
|
if (down != .complete) && self.thumbnailExist(at: themeDest) {
|
|
completion(themeDest)
|
|
} else {
|
|
if !fm.fileExists(atPath: themeDest) {
|
|
do {
|
|
try fm.createDirectory(atPath: themeDest,
|
|
withIntermediateDirectories: true,
|
|
attributes: nil)
|
|
} catch {
|
|
let desc = "Unexpected error: '\(error)'"
|
|
let e = NSError(domain: "org.slice.Clover.Download.Error",
|
|
code: 3000,
|
|
userInfo :[NSLocalizedDescriptionKey : desc])
|
|
|
|
self.statusError = e
|
|
completion(nil)
|
|
return
|
|
}
|
|
}
|
|
|
|
let plistPath : String = "\(themeManagerIndexDir)/Themes/\(theme).plist"
|
|
let themePlist = NSDictionary(contentsOfFile: plistPath)
|
|
if let files : [String] = themePlist?.allKeys as? [String] {
|
|
let fc : Int = files.count
|
|
if fc > 0 {
|
|
var succeded : Bool = true
|
|
let dispatchGroup = DispatchGroup()
|
|
for i in 0..<fc {
|
|
let file : String = files[i]
|
|
// build the url
|
|
let furl : String = "\(GitProtocol.https.rawValue)://raw.githubusercontent.com/\(self.user)/\(self.repo)/master/\(file)"
|
|
|
|
if down == .thumbnail {
|
|
if file != theme.addPath("screenshot.png")
|
|
&& file != theme.addPath("theme.svg")
|
|
&& file != theme.addPath("theme.plist") {
|
|
continue
|
|
}
|
|
}
|
|
|
|
let filedest : String = (down == .complete)
|
|
? themeDest.deletingLastPath.addPath(file)
|
|
: shaPath.addPath(file)
|
|
|
|
if !fm.fileExists(atPath: filedest) {
|
|
dispatchGroup.enter()
|
|
self.downloadFile(at: self.normalize(furl), dst: filedest) { (success) in
|
|
succeded = success
|
|
dispatchGroup.leave()
|
|
}
|
|
if !succeded {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
dispatchGroup.notify(queue: DispatchQueue.main, execute: {
|
|
if succeded {
|
|
completion(themeDest)
|
|
} else {
|
|
if self.statusError == nil { // downloadFile() generate a specific error
|
|
let desc = "Unknown error downloading '\(theme)' theme."
|
|
let e = NSError(domain: "org.slice.Clover.Download.Error",
|
|
code: 3001,
|
|
userInfo :[NSLocalizedDescriptionKey : desc])
|
|
|
|
self.statusError = e
|
|
}
|
|
completion(nil)
|
|
}
|
|
})
|
|
} else {
|
|
let desc = "'\(theme)' index file contains no file to download.\n\n\(advice)"
|
|
let e = NSError(domain: "org.slice.Clover.Download.Error",
|
|
code: 3002,
|
|
userInfo :[NSLocalizedDescriptionKey : desc])
|
|
|
|
self.statusError = e
|
|
completion(nil)
|
|
}
|
|
} else {
|
|
let desc = "Unable to load '\(theme)' index file (not found or unreadable).\n\n\(advice)"
|
|
let e = NSError(domain: "org.slice.Clover.Download.Error",
|
|
code: 3003,
|
|
userInfo :[NSLocalizedDescriptionKey : desc])
|
|
|
|
self.statusError = e
|
|
completion(nil)
|
|
}
|
|
}
|
|
} else {
|
|
let desc = "sha1 directory not found.\n\n\(advice)"
|
|
let e = NSError(domain: "org.slice.Clover.Download.Error",
|
|
code: 3004,
|
|
userInfo :[NSLocalizedDescriptionKey : desc])
|
|
|
|
self.statusError = e
|
|
completion(nil)
|
|
}
|
|
}
|
|
|
|
public func getImageUrl(for theme: String, completion: @escaping (String?) -> ()) {
|
|
/*
|
|
We are going to load an image in a web view,
|
|
so no matter if the url will be a file on the local
|
|
filesystem (downloaded theme) or just on the online repository.
|
|
*/
|
|
if let sha : String = self.getSha() {
|
|
let localTheme : String = "\(basePath)/\(sha)/\(theme)"
|
|
let png : String = "\(localTheme)/screenshot.png"
|
|
let svg : String = "\(localTheme)/theme.svg"
|
|
if fm.fileExists(atPath: png) {
|
|
completion(png)
|
|
return
|
|
} else if fm.fileExists(atPath: svg) {
|
|
completion(svg)
|
|
return
|
|
}
|
|
}
|
|
|
|
// theme not found?? Downloading...
|
|
self.download(theme: theme, down: .thumbnail) { (path) in
|
|
if let localTheme : String = path {
|
|
let png : String = "\(localTheme)/screenshot.png"
|
|
let svg : String = "\(localTheme)/theme.svg"
|
|
if fm.fileExists(atPath: png) {
|
|
completion(png)
|
|
} else if fm.fileExists(atPath: svg) {
|
|
completion(svg)
|
|
}
|
|
} else {
|
|
completion(nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func signTheme(at path: String) { // unused function
|
|
if let sha : String = self.getSha() {
|
|
let fileURL : URL = URL(fileURLWithPath: path)
|
|
let data : Data? = sha.data(using: .utf8)
|
|
|
|
// remove all attributes
|
|
do {
|
|
let list = try fileURL.listExtendedAttributes()
|
|
for attr in list {
|
|
try fileURL.removeExtendedAttribute(forName: attr)
|
|
}
|
|
} catch {
|
|
print(error.localizedDescription)
|
|
}
|
|
|
|
// set attribute
|
|
do {
|
|
try fileURL.setExtendedAttribute(data: data!, forName: kCloverThemeAttributeKey)
|
|
/* nine test
|
|
let adata = try fileURL.extendedAttribute(forName: attr)
|
|
print(String(data: adata, encoding: .utf8)!)
|
|
*/
|
|
} catch let error {
|
|
print(error.localizedDescription)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Check if a theme exist (in the current repository)
|
|
public func exist(theme: String) -> Bool {
|
|
return fm.fileExists(atPath: "\(self.themeManagerIndexDir)/Themes/\(theme).plist")
|
|
}
|
|
|
|
/// Check if a theme at the given path is up to date in the current repository. True if doesn't exists.
|
|
public func isThemeUpToDate(at path : String) -> Bool {
|
|
let theme = path.lastPath
|
|
if !self.exist(theme: theme) {
|
|
// if theme doesn't exist in the repository is up to date since is not part of it!
|
|
return true
|
|
}
|
|
|
|
/*
|
|
if the repo is not the current one return true
|
|
*/
|
|
|
|
var isOptimized = false
|
|
|
|
if let info = ThemeInfo.unarchive(at: path.addPath(kThemeInfoFile)) {
|
|
if info.user != self.user || info.repo != self.repo {
|
|
return true
|
|
}
|
|
isOptimized = info.optimized
|
|
}
|
|
|
|
let plistPath = "\(self.themeManagerIndexDir)/Themes/\(theme).plist"
|
|
guard var plist = NSMutableDictionary(contentsOfFile: plistPath) as? [String : String] else {
|
|
// We cannot load our generated file? :-(
|
|
print("Error: Theme Manager can't load \(plistPath)")
|
|
return true
|
|
}
|
|
|
|
let enumerator = fm.enumerator(atPath: path)
|
|
|
|
while let file = enumerator?.nextObject() as? String {
|
|
let fp = path.addPath(file)
|
|
var isDir : ObjCBool = false
|
|
fm.fileExists(atPath: fp, isDirectory: &isDir)
|
|
|
|
// don't check hidden files, like .DS_Store ;-)
|
|
if !file.lastPath.hasPrefix(".") {
|
|
/*
|
|
Check only files.
|
|
If extra directories exists inside the user theme it's not our business...
|
|
*/
|
|
|
|
if !isDir.boolValue {
|
|
// ..it is, if a file doesn't exist
|
|
let key = theme.addPath(file)
|
|
|
|
if let githubSha = plist[key] {
|
|
/*
|
|
if the theme is optimized check only file existence.
|
|
otherwise compare the sha1
|
|
*/
|
|
if !isOptimized {
|
|
// ok compare the sha1
|
|
if let fileData = try? Data(contentsOf: URL(fileURLWithPath: fp)) {
|
|
var gitdata = Data()
|
|
let encoding : String.Encoding = .utf8
|
|
gitdata.append("blob ".data(using: encoding)!)
|
|
gitdata.append("\(fileData.count)".data(using: encoding)!)
|
|
gitdata.append(0x00)
|
|
gitdata.append(fileData)
|
|
|
|
let gitsha1 = gitdata.sha1
|
|
if gitsha1 != githubSha {
|
|
// sha doesn't match, this is a different file
|
|
return false
|
|
}
|
|
} else {
|
|
print("Error: Theme Manager can't load \(fp)")
|
|
return false
|
|
}
|
|
}
|
|
|
|
plist[key] = nil // no longer needed
|
|
} else {
|
|
// file doesn't exist in the theme at repository
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
if We are here is because sha1 has been compared successfully
|
|
for each file on the installed theme.
|
|
If plist var no longer contains keys, it also means that all
|
|
the files are existing on the installed theme.
|
|
Otherwise fail!
|
|
*/
|
|
return plist.keys.count == 0
|
|
}
|
|
|
|
private func getIconSize(configPlistPath: String) -> Int {
|
|
let config = NSDictionary(contentsOfFile: configPlistPath) as? [String : Any]
|
|
|
|
let minScreenWidth : Int = 1280
|
|
var screenWidth : Int = minScreenWidth
|
|
let ScreenResolution = ((config?["GUI"] as? [String : Any])?["ScreenResolution"] as? String) ?? ""
|
|
|
|
if ScreenResolution.count > 0 {
|
|
screenWidth = Int(ScreenResolution.components(separatedBy: "x")[0]) ?? 0
|
|
} else {
|
|
if let mainScreenFrame = NSScreen.main?.frame {
|
|
screenWidth = Int(mainScreenFrame.size.width)
|
|
}
|
|
}
|
|
|
|
if screenWidth < minScreenWidth {
|
|
screenWidth = minScreenWidth // default
|
|
}
|
|
|
|
// determine the icon size
|
|
var iconSize : Int = 128 // default
|
|
switch screenWidth {
|
|
case 0...2048:
|
|
iconSize = 128
|
|
case 2049...2560:
|
|
iconSize = 192
|
|
case 2561...3840:
|
|
iconSize = 256
|
|
case 3841...7680:
|
|
iconSize = 400
|
|
case 7681...12000:
|
|
iconSize = 512
|
|
default:
|
|
break
|
|
}
|
|
|
|
return iconSize
|
|
}
|
|
|
|
public func optimizeTheme(at path: String, completion: @escaping (Error?) -> ()) {
|
|
DispatchQueue.global(priority: .background).async(execute: { () -> Void in
|
|
let plist = NSDictionary(contentsOfFile: path.addPath("theme.plist")) as? [String : Any]
|
|
let theme = plist?["Theme"] as? [String : Any]
|
|
let Selection = plist?["Selection"] as? [String : Any]
|
|
|
|
//logo 128x128 pixels Theme->Banner
|
|
let logo : String = (theme?["Banner"] as? String) ?? "logo.png"
|
|
|
|
// selection_big 64x64 pixels Theme->Selection->Big
|
|
let Selection_big : String = (Selection?["Big"] as? String) ?? "Selection_big.png"
|
|
|
|
// selection_small 144x144 pixels Theme->Selection->Small
|
|
let Selection_small : String = (Selection?["Small"] as? String) ?? "Selection_small.png"
|
|
|
|
var images : [String] = [String]()
|
|
let enumerator = fm.enumerator(atPath: path)
|
|
|
|
if var info = ThemeInfo.unarchive(at: path.addPath(kThemeInfoFile)) {
|
|
info.optimized = true
|
|
// write back
|
|
try? info.archive()?.write(to: URL(fileURLWithPath: path.addPath(kThemeInfoFile)))
|
|
}
|
|
|
|
var minIconSizeWidth : CGFloat? = nil
|
|
var iconsNeedsResize : Bool = false
|
|
let cloverDir = path.deletingLastPath.deletingLastPath
|
|
let suggestedSize = self.getIconSize(configPlistPath: cloverDir.addPath("config.plist"))
|
|
|
|
while let file = enumerator?.nextObject() as? String {
|
|
if file.fileExtension == "png" || file.fileExtension == "icns" {
|
|
images.append(file)
|
|
// load icons to see if they have the same size
|
|
if file.hasPrefix("icons/os_") || file.hasPrefix("icons/vol_") {
|
|
if let image = NSImage(byReferencingFile: path.addPath(file)) {
|
|
// ensure width and height are equal
|
|
if image.size.width != image.size.height {
|
|
iconsNeedsResize = true
|
|
}
|
|
if minIconSizeWidth != nil {
|
|
// size is same as in previous icon?
|
|
if minIconSizeWidth != image.size.width {
|
|
iconsNeedsResize = true
|
|
}
|
|
|
|
// is this image smaller?
|
|
if image.size.width < minIconSizeWidth! {
|
|
minIconSizeWidth = image.size.width
|
|
}
|
|
} else {
|
|
minIconSizeWidth = image.size.width
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if iconsNeedsResize {
|
|
// is minIconSizeWidth resonable from suggestedSize?
|
|
if minIconSizeWidth != nil && minIconSizeWidth! <= CGFloat(suggestedSize) {
|
|
minIconSizeWidth = CGFloat(suggestedSize)
|
|
}
|
|
}
|
|
|
|
for file in images {
|
|
let fullPath = path.addPath(file)
|
|
if var image = NSImage(byReferencingFile: fullPath) {
|
|
do {
|
|
let size = image.size
|
|
let fileName = fullPath.lastPath
|
|
if file.hasPrefix("icons/") {
|
|
if (fileName.hasPrefix("os_") || fileName.hasPrefix("vol_")) {
|
|
if iconsNeedsResize {
|
|
image = image.resize(to: NSMakeSize(minIconSizeWidth!, minIconSizeWidth!))
|
|
}
|
|
} else if (fileName.hasPrefix("func_") ||
|
|
fileName.hasPrefix("tool_") ||
|
|
fileName.hasPrefix("pointer.")) { // 32x32 pixels
|
|
if size.width != 32 || size.height != 32 {
|
|
image = image.resize(to: NSMakeSize(32, 32))
|
|
}
|
|
}
|
|
} else if file.hasPrefix("scrollbar/") {
|
|
if fileName.hasPrefix("bar_end.") || fileName.hasPrefix("bar_start.") {
|
|
image = image.resize(to: NSMakeSize(16, 5))
|
|
} else if fileName.hasPrefix("bar_fill.") || fileName.hasPrefix("scroll_fill.") {
|
|
image = image.resize(to: NSMakeSize(16, 1))
|
|
} else if fileName.hasPrefix("down_button.") || fileName.hasPrefix("up_button.") {
|
|
image = image.resize(to: NSMakeSize(16, 20))
|
|
} else if fileName.hasPrefix("scroll_end.") || fileName.hasPrefix("scroll_start.") {
|
|
image = image.resize(to: NSMakeSize(16, 7))
|
|
}
|
|
} else {
|
|
if file == logo { // logo 128x128 pixels
|
|
if size.width != 128 || size.height != 128 {
|
|
image = image.resize(to: NSMakeSize(128, 128))
|
|
}
|
|
} else if file == Selection_big { // selection_big 144x144 pixels
|
|
if size.width != 144 || size.height != 144 {
|
|
image = image.resize(to: NSMakeSize(144, 144))
|
|
}
|
|
} else if file == Selection_small { // selection_small 64x64 pixels
|
|
if size.width != 64 || size.height != 64 {
|
|
image = image.resize(to: NSMakeSize(64, 64))
|
|
}
|
|
} else if (fileName.hasPrefix("radio_button.") ||
|
|
fileName.hasPrefix("radio_button_selected.") ||
|
|
fileName.hasPrefix("checkbox.") ||
|
|
fileName.hasPrefix("checkbox_checked.")) { // 15x15 pixels
|
|
if size.width != 15 || size.height != 15 {
|
|
image = image.resize(to: NSMakeSize(15, 15))
|
|
}
|
|
}
|
|
}
|
|
|
|
if fullPath.fileExtension == "icns" {
|
|
try fm.removeItem(atPath: fullPath)
|
|
}
|
|
|
|
// everythings has now png extensions
|
|
// optimize
|
|
var success = false
|
|
if let tr = image.tiffRepresentation {
|
|
if let bitmapImage = NSBitmapImageRep(data: tr) {
|
|
if let data = bitmapImage.representation(using: .png, properties: [:]) {
|
|
var err : NSError? = nil
|
|
let optimizedData = ThemeImage(data: data, error: &err, atPath: fullPath)?.pngData
|
|
try optimizedData?.write(to: URL(fileURLWithPath: "\(fullPath.deletingFileExtension).png"))
|
|
if (err != nil) {
|
|
completion(err)
|
|
break
|
|
}
|
|
success = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if !success {
|
|
let desc = "Unable to optimize \(fullPath) data."
|
|
let e = NSError(domain: "org.slice.Clover.NSImage.Error",
|
|
code: 1001,
|
|
userInfo :[NSLocalizedDescriptionKey : desc])
|
|
completion(e)
|
|
break
|
|
}
|
|
} catch {
|
|
completion(error)
|
|
break
|
|
}
|
|
} else {
|
|
let desc = "Unable to load \(fullPath)."
|
|
let e = NSError(domain: "org.slice.Clover.NSImage.Error",
|
|
code: 1000,
|
|
userInfo :[NSLocalizedDescriptionKey : desc])
|
|
completion(e)
|
|
break
|
|
}
|
|
|
|
|
|
if file == images.last {
|
|
completion(nil)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|