// // ThemeManagerVC.swift // ThemeManager // // Created by vector sigma on 07/01/2020. // Copyright © 2020 vectorsigma. All rights reserved. // import Cocoa import WebKit final class GradientView : NSView { override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) /* let up = NSColor(red: 48 / 255, green: 35 / 255, blue: 255 / 255, alpha: 0.5) let down = NSColor(red: 150 / 255, green: 158 / 255, blue: 215 / 255, alpha: 0.5) let grad = NSGradient(colors: [down, up]) grad?.draw(in: dirtyRect, angle: 45)*/ } } final class ThemeManagerVC: NSViewController, NSTableViewDelegate, NSTableViewDataSource, WebFrameLoadDelegate, WebUIDelegate, NSComboBoxDelegate, NSComboBoxDataSource { var targetVolume : String? = nil var themes : [String] = [String]() var manager : ThemeManager? @IBOutlet var installedThemesCheckBox : NSButton! @IBOutlet var webView : WebView! @IBOutlet var sidebar : NSTableView! @IBOutlet var nameBox : NSComboBox! @IBOutlet var authorField : NSTextField! @IBOutlet var infoField : NSTextField! @IBOutlet var targetPop : FWPopUpButton! @IBOutlet var spinner : NSProgressIndicator! @IBOutlet var installButton : NSButton! @IBOutlet var unistallButton : NSButton! @IBOutlet var optimizeButton : NSButton! @IBOutlet var refreshButton : NSButton! var isPngTheme : Bool = false var loaded : Bool = false var showInstalled : Bool = false var isBusy : Bool = false override func awakeFromNib() { super.awakeFromNib() if !self.loaded { if #available(OSX 10.10, *) {} else { self.viewDidLoad() } self.loaded = true } } override func viewDidLoad() { if #available(OSX 10.10, *) { super.viewDidLoad() } self.loaded = true self.view.window?.title = self.view.window!.title.locale let settingVC = AppSD.settingsWC?.contentViewController as? SettingsViewController settingVC?.themeUserCBox.isEnabled = false settingVC?.themeRepoField.isEnabled = false self.webView.drawsBackground = false self.webView.uiDelegate = self self.webView.frameLoadDelegate = self self.webView.mainFrame.frameView.allowsScrolling = false localize(view: self.view) AppSD.isInstallerOpen = true settingVC?.disksPopUp.isEnabled = false settingVC?.updateCloverButton.isEnabled = false settingVC?.unmountButton.isEnabled = false self.nameBox.completes = true self.nameBox.delegate = self self.nameBox.dataSource = self self.nameBox.usesDataSource = true self.sidebar.delegate = self self.sidebar.dataSource = self self.sidebar.backgroundColor = NSColor.clear self.sidebar.enclosingScrollView?.contentView.drawsBackground = false if #available(OSX 10.10, *) { self.sidebar.usesStaticContents = true } self.installedThemesCheckBox.isEnabled = false self.installedThemesCheckBox.isHidden = true self.installButton.isEnabled = false self.installButton.isHidden = true self.targetPop.removeAllItems() self.populateTargets() if let bootDevice = findBootPartitionDevice() { if let mountPoint = getMountPoint(from: bootDevice) { for i in self.targetPop.itemArray { if let target = i.representedObject as? String { if bootDevice == target { self.targetPop.select(i) self.targetPopPressed(self.targetPop) self.targetVolume = mountPoint break } } } } } self.reloadThemes() } func reloadThemes() { self.themes.removeAll() self.sidebar.reloadData() self.showIndexing() let themeManagerIndexDir = NSHomeDirectory().addPath("Library/Application Support/CloverApp/Themeindex/\(AppSD.themeUser)_\(AppSD.themeRepo)") self.manager = ThemeManager(user: AppSD.themeUser, repo: AppSD.themeRepo, basePath: themeManagerIndexDir, indexDir: themeManagerIndexDir, delegate: self) /* 1) immediately load indexed themes (if any). Fast if thumbnails already exists. */ for t in self.manager!.getIndexedThemes() { self.add(theme: t) } /* 2) download thumbnails. Slower if thumbnails didn't exist or themes never indexed. */ self.manager?.getThemes { (t) in let sorted = t.sorted() for t in sorted { DispatchQueue.main.async { self.add(theme: t) } } } } func dataSource() -> [String] { if showInstalled { return AppSD.installedThemes } return self.themes } func numberOfItems(in comboBox: NSComboBox) -> Int { return self.dataSource().count } func comboBox(_ comboBox: NSComboBox, indexOfItemWithStringValue string: String) -> Int { return self.dataSource().firstIndex(of: string) ?? -1 } func comboBox(_ comboBox: NSComboBox, objectValueForItemAt index: Int) -> Any? { return self.dataSource()[index] } func comboBox(_ comboBox: NSComboBox, completedString string: String) -> String? { for theme in dataSource() { if theme.lowercased().hasPrefix(string.lowercased()) { return theme } } return nil } @IBAction func refreshIndexedThemesPressed(_ sender: NSButton!) { if let repoDir = self.manager?.themeManagerIndexDir { // delete old index if fm.fileExists(atPath: repoDir) { try? fm.removeItem(atPath: repoDir) } // re index the repository self.reloadThemes() } else { NSSound.beep() } } @IBAction func showInstalledThemes(_ sender: NSButton!) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { let copy = self.manager!.getIndexedThemes().sorted() // in cases of low downloads self.sidebar.noteNumberOfRowsChanged() AppSD.installedThemes.removeAll() self.themes.removeAll() if (self.targetVolume != nil && fm.fileExists(atPath: self.targetVolume!.addPath("EFI/CLOVER/themes"))) { self.installedThemesCheckBox.isEnabled = true self.installedThemesCheckBox.isHidden = false self.installButton.isHidden = false self.installButton.isEnabled = true } else { self.installButton.isHidden = true self.installButton.isEnabled = false self.installedThemesCheckBox.isEnabled = false self.installedThemesCheckBox.isHidden = true } if sender.state == .on { if (self.targetVolume != nil && fm.fileExists(atPath: self.targetVolume!)) { let themeDir = self.targetVolume!.addPath("EFI/CLOVER/themes") var installed = [String]() for theme in copy { if fm.fileExists(atPath: themeDir.addPath(theme)) { installed.append(theme) } } if fm.fileExists(atPath: themeDir) { do { let userTheme = try fm.contentsOfDirectory(atPath: themeDir) for theme in userTheme { if (fm.fileExists(atPath: themeDir.addPath(theme).addPath("theme.plist")) && fm.fileExists(atPath: themeDir.addPath(theme).addPath("screenshot.png"))) { if !installed.contains(theme) { installed.append(theme) } } else if fm.fileExists(atPath: themeDir.addPath(theme).addPath("theme.svg")) { if !installed.contains(theme) { installed.append(theme) } } } } catch { } } AppSD.installedThemes = installed.sorted() if installed.count == 0 { self.showNoThemes() } } self.showInstalled = true } else { self.themes = copy self.showInstalled = false if copy.count == 0 { self.showNoThemes() } } self.sidebar.reloadData() } } @IBAction func optimizeThemePressed(_ sender: NSButton!) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { var beep : Bool = true let sr = self.sidebar.selectedRow if sr >= 0 { if let v = self.sidebar.view(atColumn: 0, row: sr, makeIfNecessary: false) as? ThemeView { if v.isInstalled { if self.targetPop.indexOfSelectedItem > 0 && self.targetVolume == nil || fm.fileExists(atPath: self.targetVolume!) { let themePath = self.targetVolume!.addPath("EFI/CLOVER/themes").addPath(v.name) if fm.fileExists(atPath: themePath) { beep = false self.isBusy = true self.spinner.startAnimation(nil) self.optimizeButton.isEnabled = false self.installButton.isEnabled = false self.unistallButton.isEnabled = false self.targetPop.isEnabled = false self.nameBox.isEnabled = false self.installedThemesCheckBox.isEnabled = false //return self.manager?.optimizeTheme(at: themePath, completion: { (error) in DispatchQueue.main.async { self.isBusy = false self.spinner.stopAnimation(nil) self.optimizeButton.isEnabled = true self.installButton.isEnabled = true self.unistallButton.isEnabled = true self.targetPop.isEnabled = true self.nameBox.isEnabled = true self.installedThemesCheckBox.isEnabled = true } if (error != nil) { DispatchQueue.main.async { NSSound(named: "Basso")?.play() let alert = NSAlert() alert.messageText = "😱" alert.informativeText = error!.localizedDescription alert.alertStyle = .critical alert.addButton(withTitle: "Ok".locale) alert.beginSheetModal(for: self.view.window!) { (reponse) in } } } else { DispatchQueue.main.async { NSSound(named: "Glass")?.play() } } }) } } } } } if beep { NSSound.beep() } } } func showNoThemes() { self.webView.mainFrame.loadHTMLString( """

\("No themes found".locale)

""", baseURL: Bundle.main.bundleURL) } func showIndexing() { self.webView.drawsBackground = true // https://www.w3schools.com/howto/howto_css_loader.asp self.webView.mainFrame.loadHTMLString("""

Indexing..

""", baseURL: Bundle.main.bundleURL) } @IBAction func unInstallPressed(_ sender: NSButton!) { if AppSD.isInstalling || self.isBusy { NSSound.beep() return } if self.targetPop.indexOfSelectedItem == 0 || self.targetVolume == nil || (self.targetVolume != nil && !fm.fileExists(atPath: self.targetVolume!)) { NSSound.beep() return // this should not happen } let theme = self.nameBox.stringValue let themePath = self.targetVolume!.addPath("EFI/CLOVER/themes").addPath(theme) let sr = self.sidebar.selectedRow if sr >= 0 && sr < self.dataSource().count { self.spinner.startAnimation(nil) do { if fm.fileExists(atPath: themePath) { try fm.removeItem(atPath: themePath) self.installedThemesCheckBox.isEnabled = false self.unistallButton.isEnabled = false self.unistallButton.animator().isHidden = true NSSound(contentsOfFile: "/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/finder/empty trash.aif", byReference: false)?.play() } self.spinner.stopAnimation(nil) self.showInstalledThemes(self.installedThemesCheckBox) } catch { NSSound(named: "Basso")?.play() let alert = NSAlert() alert.messageText = "Can't remove the theme".locale alert.informativeText = "\(error.localizedDescription)" alert.addButton(withTitle: "OK") alert.runModal() self.spinner.stopAnimation(nil) self.showInstalledThemes(self.installedThemesCheckBox) } } } @IBAction func InstallPressed(_ sender: NSButton!) { if AppSD.isInstalling || self.isBusy { NSSound.beep() return } if self.targetPop.indexOfSelectedItem == 0 || self.targetVolume == nil || (self.targetVolume != nil && !fm.fileExists(atPath: self.targetVolume!)) { NSSound.beep() DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { self.targetPop.performClick(self.targetPop) } return } let theme = self.nameBox.stringValue let themeDest = self.targetVolume!.addPath("EFI/CLOVER/themes").addPath(theme) let sr = self.sidebar.selectedRow if sr >= 0 && sr < self.dataSource().count { self.isBusy = true self.spinner.startAnimation(nil) self.optimizeButton.isEnabled = false self.installButton.isEnabled = false self.unistallButton.isEnabled = false self.targetPop.isEnabled = false self.nameBox.isEnabled = false self.installedThemesCheckBox.isEnabled = false self.manager?.download(theme: theme, down: .complete, completion: { (path) in if self.manager?.statusError != nil { NSSound(named: "Basso")?.play() DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Installation failed".locale alert.informativeText = self.manager!.statusError!.localizedDescription alert.addButton(withTitle: "OK") alert.beginSheetModal(for: self.view.window!, completionHandler: { (modalResponse) -> Void in }) } } else { if let themePath = path { try? fm.removeItem(atPath: themeDest) do { try fm.moveItem(atPath: themePath, toPath: themeDest) let ti = ThemeInfo(user: self.manager!.user, repo: self.manager!.repo, optimized: false).archive() try ti?.write(to: URL(fileURLWithPath: themeDest.addPath(kThemeInfoFile))) NSSound(named: "Glass")?.play() } catch { NSSound(named: "Basso")?.play() DispatchQueue.main.async { let alert = NSAlert() alert.messageText = "Installation failed".locale alert.informativeText = "\(error.localizedDescription)" alert.addButton(withTitle: "OK") alert.beginSheetModal(for: self.view.window!, completionHandler: { (modalResponse) -> Void in }) } } } else { NSSound(named: "Basso")?.play() DispatchQueue.main.async { let alert = NSAlert() let advice = "Try to refresh the list by pressing the Refresh button below." alert.messageText = "Installation failed".locale alert.informativeText = "Theme \"\(theme)\" cannot be downloaded.\n\n\(advice)" alert.addButton(withTitle: "OK") alert.beginSheetModal(for: self.view.window!, completionHandler: { (modalResponse) -> Void in }) } } } DispatchQueue.main.async { self.isBusy = false AppSD.isInstalling = false self.spinner.stopAnimation(nil) self.targetPop.isEnabled = true self.nameBox.isEnabled = true self.installedThemesCheckBox.isEnabled = true self.onSelection() } }) } } func addRow(for theme: String) { let index = self.themes.count self.themes.append(theme) self.sidebar.beginUpdates() self.sidebar.insertRows(at: IndexSet(integer: index), withAnimation: .slideUp) self.sidebar.endUpdates() } func add(theme: String) { if self.themes.contains(theme) { return } if let sha = self.manager?.getSha() { let themePath = self.manager!.basePath.addPath(sha).addPath(theme) if fm.fileExists(atPath: themePath.addPath("theme.svg")) { self.addRow(for: theme) } else if (fm.fileExists(atPath: themePath.addPath("screenshot.png")) && fm.fileExists(atPath: themePath.addPath("theme.plist"))) { self.addRow(for: theme) } else { self.manager?.download(theme: theme, down: .thumbnail, completion: { (path) in if path != nil { DispatchQueue.main.async { self.addRow(for: theme) } } }) } } } @IBAction func nameBoxSelected(_ sender: NSComboBox!) { let theme = sender.stringValue if theme.count > 0 && self.dataSource().contains(theme) { let index = self.dataSource().firstIndex(of: theme) self.sidebar.selectRowIndexes(IndexSet(integer: index!), byExtendingSelection: false) self.sidebar.scrollRowToVisible(index!) } else { NSSound.beep() } } func tableView(_ tableView: NSTableView, didAdd rowView: NSTableRowView, forRow row: Int) { if self.sidebar.selectedRow < 0 { self.sidebar.selectRowIndexes(IndexSet(integer: 0), byExtendingSelection: false) } } func numberOfRows(in tableView: NSTableView) -> Int { return self.dataSource().count } func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat { return 82 } func tableViewSelectionDidChange(_ notification: Notification) { self.webView.drawsBackground = false if self.isBusy { NSSound.beep() return } self.onSelection() } func onSelection() { self.isPngTheme = false self.optimizeButton.isEnabled = false self.optimizeButton.state = .off let sr = self.sidebar.selectedRow if sr >= 0 { if let v = self.sidebar.view(atColumn: 0, row: sr, makeIfNecessary: false) as? ThemeView { self.installButton.title = v.isUpToDate ? "Install".locale : "Update".locale if v.isInstalled { self.unistallButton.animator().isHidden = false self.unistallButton.isEnabled = true } else { self.unistallButton.animator().isHidden = true self.unistallButton.isEnabled = false } if (self.manager?.exist(theme: v.name))! { self.installButton.isEnabled = true } else { self.installButton.isEnabled = false } if let path = v.imagePath { self.nameBox.stringValue = v.name self.authorField.stringValue = v.author ?? "" self.infoField.stringValue = v.info ?? "" self.webView.mainFrame.loadHTMLString("""
""", baseURL: Bundle.main.bundleURL) if path.hasSuffix(".svg") { let svgStr = try? String(contentsOfFile: path) var author : String? = nil var version : String? = nil var description : String? = nil if let lines = svgStr?.components(separatedBy: .newlines) { /* Version="0.87" Year="2018-2019" Author="Blackosx" Description="Vector version of BGM (Based on Clovy theme file structure)" */ for line in lines { if (line.range(of: "Version=\"") != nil) { version = line.components(separatedBy: "Version=\"")[1].components(separatedBy: "\"")[0] } } for line in lines { if (line.range(of: "Author=\"") != nil) { author = line.components(separatedBy: "Author=\"")[1].components(separatedBy: "\"")[0] } } for line in lines { if (line.range(of: "Description=\"") != nil) { description = line.components(separatedBy: "Description=\"")[1].components(separatedBy: "\"")[0] } } } self.authorField.stringValue = author ?? "" self.infoField.stringValue = "\(description ?? "?"), v\(version ?? "?")" } else if path.hasSuffix(".png") { // get theme.plist self.isPngTheme = true let plistPath = "\((path as NSString).deletingLastPathComponent)/theme.plist" let plist = NSDictionary(contentsOfFile: plistPath) self.authorField.stringValue = (plist?.object(forKey: "Author") as? String) ?? "" self.infoField.stringValue = (plist?.object(forKey: "Description") as? String) ?? "" self.optimizeButton.isEnabled = v.isInstalled && self.isPngTheme } } else { v.load() } } } else { self.installButton.title = "Install".locale } } func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? { if let identifier = tableColumn?.identifier { if identifier.rawValue == "column1" { let count = self.dataSource().count if count == 0 { return nil } if row > (count - 1) { return nil } let tv = ThemeView(manager: self.manager!, name: self.dataSource()[row], row: row) tv.manager.delegate = self return tv } } return nil } func tableView(_ tableView: NSTableView, rowViewForRow row: Int) -> NSTableRowView? { return ThemeTableRowView() } // MARK: find suitable disks func populateTargets() { if AppSD.isInstalling { return } let selected : String? = self.targetPop.selectedItem?.representedObject as? String self.targetPop.removeAllItems() self.targetPop.addItem(withTitle: "Select a disk..".locale) let disks : NSDictionary = getAlldisks() let bootPartition = findBootPartitionDevice() let diskSorted : [String] = (disks.allKeys as! [String]).sorted() for d in diskSorted { let disk : String = d if isWritable(diskOrMtp: disk) && !kBannedMedia.contains(getVolumeName(from: disk) ?? "") { let fs : String = getFS(from: disk)?.lowercased() ?? kNotAvailable.locale let psm : String = getPartitionSchemeMap(from: disk) ?? kNotAvailable.locale let name : String = getVolumeName(from: disk) ?? kNotAvailable.locale let mp : String = getMountPoint(from: disk) ?? kNotAvailable.locale let parentDiskName : String = getMediaName(from: getBSDParent(of: disk) ?? "") ?? kNotAvailable.locale let supportedFS = ["msdos", "fat16", "fat32", "exfat", "hfs"] if supportedFS.contains(fs) { self.targetPop.addItem(withTitle: "\(disk)\t\(name), \("mount point".locale): \(mp), \(fs.uppercased()), \(psm): (\(parentDiskName))") self.targetPop.invalidateIntrinsicContentSize() // get the image if disk == bootPartition { let image : NSImage = NSImage(named: "NSApplicationIcon")!.copy() as! NSImage image.size = NSMakeSize(16, 16) self.targetPop.lastItem?.image = image } else if let image : NSImage = getIconFor(volume: disk) { image.size = NSMakeSize(16, 16) self.targetPop.lastItem?.image = image } self.targetPop.lastItem?.representedObject = disk } } } if (selected != nil) { for item in self.targetPop.itemArray { if let d = item.representedObject as? String { if selected == d { self.targetPop.select(item) break } } } } } @IBAction func targetPopPressed(_ sender: FWPopUpButton!) { self.installedThemesCheckBox.isEnabled = false self.installedThemesCheckBox.isHidden = true self.installButton.isEnabled = false self.installButton.isHidden = true if let disk = sender?.selectedItem?.representedObject as? String { if !isMountPoint(path: disk) { DispatchQueue.global(priority: .background).async(execute: { () -> Void in let cmd = "diskutil mount \(disk)" let msg = String(format: "Clover wants to mount %@", disk) let script = "do shell script \"\(cmd)\" with prompt \"\(msg)\" with administrator privileges" let task = Process() if #available(OSX 10.12, *) { task.launchPath = "/usr/bin/osascript" task.arguments = ["-e", script] } else { task.launchPath = "/usr/sbin/diskutil" task.arguments = ["mount", disk] } task.terminationHandler = { t in DispatchQueue.main.async { self.view.window?.level = .floating self.view.window?.makeKeyAndOrderFront(nil) self.view.window?.level = .normal } if t.terminationStatus == 0 { if isMountPoint(path: disk) { DispatchQueue.main.async { self.targetVolume = getMountPoint(from: disk) self.populateTargets() self.showInstalledThemes(self.installedThemesCheckBox) } } } else { DispatchQueue.main.async { NSSound.beep() //self.installButton.isEnabled = true self.showInstalledThemes(self.installedThemesCheckBox) } } } task.launch() }) //} } else { self.targetVolume = getMountPoint(from: disk) self.populateTargets() self.showInstalledThemes(self.installedThemesCheckBox) } } else { self.targetVolume = nil self.showInstalledThemes(self.installedThemesCheckBox) } } } // MARK: ThemeManager Window controller final class ThemeManagerWC: NSWindowController, NSWindowDelegate { var viewController : NSViewController? = nil override var contentViewController: NSViewController? { get { self.viewController } set { self.viewController = newValue } } class func loadFromNib() -> ThemeManagerWC? { var topLevel: NSArray? = nil Bundle.main.loadNibNamed("ThemeManager", owner: self, topLevelObjects: &topLevel) if (topLevel != nil) { var wc : ThemeManagerWC? = nil for o in topLevel! { if o is ThemeManagerWC { wc = o as? ThemeManagerWC } } for o in topLevel! { if o is ThemeManagerVC { wc?.contentViewController = o as! ThemeManagerVC } } return wc } return nil } func windowShouldClose(_ sender: NSWindow) -> Bool { if AppSD.isInstalling { return false } let settingVC = AppSD.settingsWC?.contentViewController as? SettingsViewController settingVC?.disksPopUp.isEnabled = true settingVC?.updateCloverButton.isEnabled = true settingVC?.searchESPDisks() AppSD.isInstallerOpen = false settingVC?.themeUserCBox.isEnabled = true settingVC?.themeRepoField.isEnabled = true self.window = nil self.close() AppSD.themeManagerWC = nil // remove a strong reference AppSD.setActivationPolicy() return true } } final class ThemeTableRowView: NSTableRowView { override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) self.wantsLayer = true self.layer?.backgroundColor = NSColor.clear.cgColor } override var isEmphasized: Bool { set {} get { return false } } override var selectionHighlightStyle: NSTableView.SelectionHighlightStyle { set {} get { return .regular } } override func drawSelection(in dirtyRect: NSRect) { if self.selectionHighlightStyle != .none { let selectionRect = NSInsetRect(self.bounds, 2.5, 2.5) NSColor.green.setFill() //NSColor(calibratedWhite: 0.85, alpha: 0.6).setFill() let selectionPath = NSBezierPath.init(rect: selectionRect) selectionPath.fill() } } }