Resources/MainUI/SidebarViewController.swift
//
// SidebarViewController.swift
// Aerial
//
// Created by Guillaume Louel on 15/07/2020.
// Copyright © 2020 Guillaume Louel. All rights reserved.
//
import Cocoa
enum SidebarMenus {
case modern, videos, settings, infos
}
class SidebarViewController: NSViewController {
@IBOutlet var sidebarOutlineView: SidebarOutlineView!
// For the download indicator
@IBOutlet var downloadIndicator: NSVisualEffectView!
@IBOutlet var downloadIndicatorProgress: NSProgressIndicator!
@IBOutlet var downloadIndicatorLabel: NSTextField!
@IBOutlet var downloadCancelButton: NSButton!
var windowController: PanelWindowController?
// Always start with the videos panel selected
var menuSelection: SidebarMenus = .modern
@IBOutlet var closeButton: NSButton!
var menuPath = "" // eh...
override func viewDidLoad() {
super.viewDidLoad()
if #available(OSX 10.16, *) {
closeButton.isHighlighted = true
} else {
// For some reason we need this on 10.12/10.13, there's a bug that somehow inverts colors in the recent Big Sur SDK for those older OSes
sidebarOutlineView.backgroundColor = .clear
sidebarOutlineView.gridColor = .alternateSelectedControlColor
}
sidebarOutlineView.delegate = self
sidebarOutlineView.dataSource = self
// Setup the updates for the download status
let videoManager = VideoManager.sharedInstance
videoManager.addCallback { done, total in
self.updateDownloads(done: done, total: total, progress: 0)
}
videoManager.addProgressCallback { done, total, progress in
self.updateDownloads(done: done, total: total, progress: progress)
}
downloadIndicator.isHidden = true
downloadIndicatorProgress.doubleValue = 0
}
override func viewDidAppear() {
self.reloadSidebar()
}
private func updateSidebarMenu(_ menu: SidebarMenus) {
if menu != menuSelection {
windowController?.switchFrom(menuSelection, to: menu)
// Make sure we mark the current one
menuSelection = menu
sidebarOutlineView.reloadData()
sidebarOutlineView.expandItem(nil, expandChildren: true)
sidebarOutlineView.selectRowIndexes([1], byExtendingSelection: false)
}
}
@IBAction func closeButton(_ sender: Any) {
windowController!.stopVideo()
if !downloadIndicator.isHidden {
// swiftlint:disable:next line_length
if !Aerial.helper.showAlert(question: "Downloads still in progress", text: "Your video downloads are still in progress. Are you sure you want to quit ? This will abandon your current downloads.", button1: "Quit and Abandon Downloads", button2: "Cancel") {
return
}
}
if Aerial.helper.appMode {
NSApplication.shared.terminate(nil)
} else {
if Aerial.helper.underCompanion {
windowController!.window?.close()
} else {
windowController!.window?.sheetParent?.endSheet(windowController!.window!)
}
}
}
// MARK: Download indicator
// Update the status of the download bar at the bottom of the sidebar
func updateDownloads(done: Int, total: Int, progress: Double) {
if total == 0 {
downloadIndicator.isHidden = true
downloadIndicatorProgress.doubleValue = 0
windowController!.updateViewInPlace()
// Sidebar.instance.refreshVideos()
/*
sidebarOutlineView.reloadData()
sidebarOutlineView.expandItem(nil, expandChildren: true)
sidebarOutlineView.selectRowIndexes([1], byExtendingSelection: false)
*/
} else if progress == 0 {
downloadIndicator.isHidden = false
downloadIndicatorProgress.doubleValue = Double(done)
downloadIndicatorProgress.maxValue = Double(total)
downloadIndicatorProgress.toolTip = "Downloading \(done) / \(total)"
downloadIndicatorLabel.stringValue = "Downloading \(done) / \(total)"
} else {
downloadIndicator.isHidden = false
downloadIndicatorProgress.doubleValue = Double(done) + progress
}
}
@IBAction func cancelDownloads(_ sender: Any) {
let videoManager = VideoManager.sharedInstance
videoManager.cancelAll()
}
}
extension SidebarViewController: NSOutlineViewDataSource {
func outlineView(_ outlineView: NSOutlineView, numberOfChildrenOfItem item: Any?) -> Int {
if let header = item as? Sidebar.Header {
return header.entries.count
}
debugLog("child count \(Sidebar.instance.modern.count)")
return Sidebar.instance.modern.count
}
func outlineView(_ outlineView: NSOutlineView, child index: Int, ofItem item: Any?) -> Any {
if let header = item as? Sidebar.Header {
return header.entries[index]
}
return Sidebar.instance.modern[index]
}
func outlineView(_ outlineView: NSOutlineView, isItemExpandable item: Any) -> Bool {
if item is Sidebar.Header {
return true
}
return false
}
func outlineView(_ outlineView: NSOutlineView, heightOfRowByItem item: Any) -> CGFloat {
if item is Sidebar.Header {
return 24
} else {
return 30
}
}
func reloadSidebar() {
// We need to reload our sidebar
// Sidebar.instance.refreshVideos()
debugLog("reload sidebar")
let set = sidebarOutlineView.selectedRowIndexes
sidebarOutlineView.reloadData()
sidebarOutlineView.expandItem(nil, expandChildren: true)
if set.isEmpty {
debugLog("empty set")
sidebarOutlineView.selectRowIndexes([1], byExtendingSelection: false)
} else {
debugLog("re set ing")
sidebarOutlineView.selectRowIndexes(set, byExtendingSelection: false)
}
}
}
extension SidebarViewController: NSOutlineViewDelegate {
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
var view: NSTableCellView?
if let header = item as? Sidebar.Header {
view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "HeaderCell"), owner: self) as? NSTableCellView
if let textField = view?.textField {
textField.stringValue = header.name
}
} else if let entry = item as? Sidebar.MenuEntry {
view = outlineView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "DataCell"), owner: self) as? NSTableCellView
if let textField = view?.textField {
textField.stringValue = entry.name
}
if let imageView = view?.imageView {
imageView.image = Sidebar.iconFor(entry.path, name: entry.name)
imageView.image?.isTemplate = true
imageView.sizeThatFits(CGSize(width: 24, height: 24)) // Hmm
}
}
// More code here
return view
}
func outlineView(_ outlineView: NSOutlineView, shouldSelectItem item: Any) -> Bool {
if item is Sidebar.Header {
return false
}
return true
}
func outlineViewSelectionDidChange(_ notification: Notification) {
guard let outlineView = notification.object as? NSOutlineView else {
return
}
let selectedIndex = outlineView.selectedRow
if let entry = outlineView.item(atRow: selectedIndex) as? Sidebar.MenuEntry {
windowController!.switchTo(entry.path)
}
}
func outlineView(_ outlineView: NSOutlineView, shouldShowOutlineCellForItem item: Any) -> Bool {
return false
}
@available(OSX 10.16, *)
func outlineView(_ outlineView: NSOutlineView, tintConfigurationForItem item: Any) -> NSTintConfiguration? {
if let entry = item as? Sidebar.MenuEntry {
if entry.name == "Favorites" {
return NSTintConfiguration(fixedColor: .init(red: 0.996, green: 0.741, blue: 0.066, alpha: 1.0))
} else {
return NSTintConfiguration.default
}
}
return nil
}
}
// Right click menu
/*extension SidebarViewController: SidebarOutlineViewDelegate {
// swiftlint:disable cyclomatic_complexity
func outlineView(outlineView: NSOutlineView, menuForItem item: Any) -> NSMenu? {
// Make sure we're right clicking a menu entry
if let entry = item as? Sidebar.MenuEntry {
if entry.path.starts(with: "videos:") {
let idx = entry.path.firstIndex(of: ":")
let path = String(entry.path[idx!...].dropFirst()) // Oh Swift...
menuPath = path // Store it for later use in selectors, it's ugly I know
// Grab all the videos
var videos: [AerialVideo]
if let mode = VideoList.instance.modeFromPath(path) {
let index = Int(path.split(separator: ":")[1])!
videos = VideoList.instance.getVideosForSource(index, mode: mode)
} else {
// all
videos = VideoList.instance.videos.sorted { $0.secondaryName < $1.secondaryName }
}
guard !videos.isEmpty else {
debugLog("empty videos")
return nil
}
let menu = NSMenu()
var hasUnfavs = false
var hasFavs = false
// Add a unhide menu just for the hidden category
if let mode = VideoList.instance.modeFromPath(path) {
if mode == .hidden {
let item = NSMenuItem(title: "Show videos", action: #selector(showVideos(_:)), keyEquivalent: "")
item.setIcons("eye")
menu.addItem(item)
return menu
}
}
// Add/remove favorites
if !videos.filter({ !PrefsVideos.favorites.contains($0.id) }).isEmpty {
let item = NSMenuItem(title: "Favorite videos", action: #selector(favoriteVideos(_:)), keyEquivalent: "")
item.setIcons("star.fill")
menu.addItem(item)
hasUnfavs = true
}
if !videos.filter({ PrefsVideos.favorites.contains($0.id) }).isEmpty {
let item = NSMenuItem(title: "Unfavorite videos", action: #selector(unfavoriteVideos(_:)), keyEquivalent: "")
item.setIcons("star")
menu.addItem(item)
hasFavs = true
}
// Don't show the hide videos option if we only have favs in that list
if !(hasFavs && !hasUnfavs) {
menu.addItem(NSMenuItem.separator())
let item = NSMenuItem(title: "Hide videos", action: #selector(hideVideos(_:)), keyEquivalent: "")
item.setIcons("eye.slash")
menu.addItem(item)
}
// Do we have uncached videos in here ?
if !videos.filter({!$0.isAvailableOffline}).isEmpty {
menu.addItem(NSMenuItem.separator())
let item = NSMenuItem(title: "Cache missing videos", action: #selector(cacheMissingVideos(_:)), keyEquivalent: "")
item.setIcons("arrow.down.circle")
menu.addItem(item)
}
if !videos.filter({ PrefsVideos.vibrance.keys.contains($0.id) }).isEmpty {
let item = NSMenuItem(title: "Reset vibrance", action: #selector(resetVibrance(_:)), keyEquivalent: "")
item.setIcons("slider.horizontal.3")
menu.addItem(item)
hasFavs = true
}
return menu
}
}
return nil
}
@objc func cacheMissingVideos(_ sender: Any) {
if menuPath == "" {
errorLog("Right click cache missing with no menu")
return
}
Cache.ensureDownload {
let videos = VideoList.instance.getVideosForPath(self.menuPath)
for video in videos.filter({ !$0.isAvailableOffline }) {
VideoManager.sharedInstance.queueDownload(video)
}
}
}
@objc func favoriteVideos(_ sender: Any) {
if menuPath == "" {
errorLog("Right click missing path")
return
}
let videos = VideoList.instance.getVideosForPath(self.menuPath)
for video in videos.filter({ !PrefsVideos.favorites.contains($0.id) }) {
PrefsVideos.favorites.append(video.id)
}
windowController!.updateViewInPlace()
}
@objc func unfavoriteVideos(_ sender: Any) {
if menuPath == "" {
errorLog("Right click missing path")
return
}
let videos = VideoList.instance.getVideosForPath(self.menuPath)
for video in videos.filter({ PrefsVideos.favorites.contains($0.id) }) {
PrefsVideos.favorites.remove(at: PrefsVideos.favorites.firstIndex(of: video.id)!)
}
windowController!.updateViewInPlace()
}
@objc func showVideos(_ sender: Any) {
if menuPath == "" {
errorLog("Right click missing path")
return
}
let videos = VideoList.instance.getVideosForPath(self.menuPath)
for video in videos.filter({ PrefsVideos.hidden.contains($0.id) }) {
PrefsVideos.hidden.remove(at: PrefsVideos.hidden.firstIndex(of: video.id)!)
}
// We need to reload our sidebar
// Sidebar.instance.refreshVideos()
sidebarOutlineView.reloadData()
sidebarOutlineView.expandItem(nil, expandChildren: true)
sidebarOutlineView.selectRowIndexes([1], byExtendingSelection: false)
}
@objc func hideVideos(_ sender: Any) {
if menuPath == "" {
errorLog("Right click missing path")
return
}
let videos = VideoList.instance.getVideosForPath(self.menuPath)
for video in videos.filter({ !PrefsVideos.hidden.contains($0.id) }) {
PrefsVideos.hidden.append(video.id)
}
// We need to reload our sidebar
// Sidebar.instance.refreshVideos()
sidebarOutlineView.reloadData()
sidebarOutlineView.expandItem(nil, expandChildren: true)
sidebarOutlineView.selectRowIndexes([1], byExtendingSelection: false)
}
@objc func resetVibrance(_ sender: Any) {
if menuPath == "" {
errorLog("Right click missing path")
return
}
let videos = VideoList.instance.getVideosForPath(self.menuPath)
for video in videos.filter({ PrefsVideos.vibrance.keys.contains($0.id) }) {
PrefsVideos.vibrance.removeValue(forKey: video.id)
}
windowController!.updateViewInPlace()
}
}*/