mirror of
https://github.com/CloverHackyColor/CloverBootloader.git
synced 2024-12-02 13:03:28 +01:00
a4c90606d1
HelmoHass themes repository (by LAbyOne) can be selected directly in the UI. Now the theme manager knows when a theme is out dated and show a dedicate icon when this happens. Now Clover.app can encode the git sha1 ... always with out git. Searching themes in the relative fields is now even more easy by tipying the name partially in case insensitive mode. builme now close the Terminal window when exit is selected (thanks to LAbyOne). Fixed a bug in the theme manager engine switching between repositories.
816 lines
29 KiB
Swift
816 lines
29 KiB
Swift
//
|
|
// ThemeManager.swift
|
|
// ThemeManager
|
|
//
|
|
// Created by vector sigma on 04/01/2020.
|
|
// Copyright © 2020 vectorsigma. All rights reserved.
|
|
//
|
|
|
|
import Cocoa
|
|
import CommonCrypto
|
|
|
|
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"
|
|
}
|
|
|
|
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 gitInitCount : Int32 = 0
|
|
|
|
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?) -> ()) {
|
|
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 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 {
|
|
completion(nil)
|
|
}
|
|
})
|
|
} else {
|
|
completion(nil)
|
|
}
|
|
} else {
|
|
completion(nil)
|
|
}
|
|
}
|
|
} else {
|
|
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
|
|
}
|
|
|
|
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] {
|
|
// 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
|
|
}
|
|
|
|
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)
|
|
|
|
while let file = enumerator?.nextObject() as? String {
|
|
if file.fileExtension == "png" || file.fileExtension == "icns" {
|
|
images.append(file)
|
|
}
|
|
}
|
|
|
|
for file in images {
|
|
let fullPath = path.addPath(file)
|
|
do {
|
|
let image = try ThemeImage(themeImageAtPath: fullPath)
|
|
let size = image.size
|
|
let fileName = fullPath.lastPath
|
|
if file.hasPrefix("icons/") || file.hasPrefix("alternative_icons/") {
|
|
if (fileName.hasPrefix("os_") || fileName.hasPrefix("vol_")) { // 128x128 pixels
|
|
if size.width != 128 || size.height != 128 {
|
|
image.size = NSMakeSize(128, 128)
|
|
}
|
|
} else if (fileName.hasPrefix("func_") ||
|
|
fileName.hasPrefix("tool_") ||
|
|
fileName == "pointer.png") { // 32x32 pixels
|
|
if size.width != 32 || size.height != 32 {
|
|
image.size = NSMakeSize(32, 32)
|
|
}
|
|
}
|
|
} else {
|
|
if file == logo { // logo 128x128 pixels
|
|
if size.width != 128 || size.height != 128 {
|
|
image.size = NSMakeSize(128, 128)
|
|
}
|
|
} else if file == Selection_big { // selection_big 144x144 pixels
|
|
if size.width != 144 || size.height != 144 {
|
|
image.size = NSMakeSize(144, 144)
|
|
}
|
|
} else if file == Selection_small { // selection_small 64x64 pixels
|
|
if size.width != 64 || size.height != 64 {
|
|
image.size = NSMakeSize(64, 64)
|
|
}
|
|
} else if (file == "radio_button" ||
|
|
file == "radio_button_selected" ||
|
|
file == "checkbox" ||
|
|
file == "checkbox_checked") { // 15x15 pixels
|
|
if size.width != 15 || size.height != 15 {
|
|
image.size = NSMakeSize(15, 15)
|
|
}
|
|
}
|
|
}
|
|
try image.pngData.write(to: URL(fileURLWithPath: fullPath))
|
|
} catch {
|
|
completion(error)
|
|
break
|
|
}
|
|
|
|
if file == images.last {
|
|
completion(nil)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|