Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ xcuserdata/
# `go build` output
HTMLConverter/htmlconverter*.a
HTMLConverter/htmlconverter*.h

.idea
4 changes: 4 additions & 0 deletions Glance.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
7EB36482244B03CC00D7F96F /* markdown-main.css in Resources */ = {isa = PBXBuildFile; fileRef = 7EB3643D244B03CC00D7F96F /* markdown-main.css */; };
7EB36488244B65D900D7F96F /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB36487244B65D900D7F96F /* String.swift */; };
7EB3648A244B665700D7F96F /* ZIPPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB36489244B665700D7F96F /* ZIPPreview.swift */; };
A1B2C3D4E5F6789000112234 /* DirectoryPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D4E5F6789000112233 /* DirectoryPreview.swift */; };
7EB7491924228549007265A4 /* JupyterPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EB7491824228549007265A4 /* JupyterPreview.swift */; };
7ECC8CF2240CB4CC000D6970 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ECC8CF1240CB4CC000D6970 /* AppDelegate.swift */; };
7ECC8CF4240CB4CC000D6970 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ECC8CF3240CB4CC000D6970 /* ViewController.swift */; };
Expand Down Expand Up @@ -269,6 +270,7 @@
7EB3643D244B03CC00D7F96F /* markdown-main.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = "markdown-main.css"; sourceTree = "<group>"; };
7EB36487244B65D900D7F96F /* String.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
7EB36489244B665700D7F96F /* ZIPPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZIPPreview.swift; sourceTree = "<group>"; };
A1B2C3D4E5F6789000112233 /* DirectoryPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DirectoryPreview.swift; sourceTree = "<group>"; };
7EB7491824228549007265A4 /* JupyterPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JupyterPreview.swift; sourceTree = "<group>"; };
7ECC8CEE240CB4CC000D6970 /* Glance.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Glance.app; sourceTree = BUILT_PRODUCTS_DIR; };
7ECC8CF1240CB4CC000D6970 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -360,6 +362,7 @@
7E1DC529240E6FDE00D0A061 /* CodePreview.swift */,
7EB7491824228549007265A4 /* JupyterPreview.swift */,
7E1DC520240E6D8000D0A061 /* MarkdownPreview.swift */,
A1B2C3D4E5F6789000112233 /* DirectoryPreview.swift */,
7E7A9DAC244F24B900276E51 /* TARPreview.swift */,
7E413F7F2418DD6200CFBB1D /* TSVPreview.swift */,
7EB36489244B665700D7F96F /* ZIPPreview.swift */,
Expand Down Expand Up @@ -871,6 +874,7 @@
7E9F0D7E24168870007F1008 /* Script.swift in Sources */,
7EB3648A244B665700D7F96F /* ZIPPreview.swift in Sources */,
103654F42F726241006F2538 /* SevenZipPreview.swift in Sources */,
A1B2C3D4E5F6789000112234 /* DirectoryPreview.swift in Sources */,
7E9F0D7D24168870007F1008 /* Stylesheet.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
3 changes: 3 additions & 0 deletions QLPlugin/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
- public.xml (.xml)
-->

<!-- Directory -->
<string>public.folder</string>

<!-- Archive -->
<string>org.7-zip.7-zip-archive</string> <!-- .7z -->
<string>com.sun.java-archive</string> <!-- .jar (Java) -->
Expand Down
5 changes: 4 additions & 1 deletion QLPlugin/MainVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,10 @@ class MainVC: NSViewController, QLPreviewingController {
/// Generates a preview of the selected file and adds the corresponding child view controller.
private func previewFile(file: File) throws {
// Initialize `PreviewVC` for the file type
if let previewInitializerType = PreviewVCFactory.getPreviewInitializer(fileURL: file.url) {
if let previewInitializerType = PreviewVCFactory.getPreviewInitializer(
fileURL: file.url,
isDirectory: file.isDirectory
) {
// Generate file preview
let previewInitializer = previewInitializerType.init()
let previewVC = try previewInitializer.createPreviewVC(file: file)
Expand Down
5 changes: 4 additions & 1 deletion QLPlugin/Views/PreviewVCFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import Foundation
/// Returns a `PreviewVC` subclass that can be used to generate a preview of the provided file.
/// May return `nil` if the file is not supported.
class PreviewVCFactory {
static func getPreviewInitializer(fileURL: URL) -> Preview.Type? {
static func getPreviewInitializer(fileURL: URL, isDirectory: Bool = false) -> Preview.Type? {
if isDirectory {
return DirectoryPreview.self
}
switch fileURL.pathExtension.lowercased() {
case "gz":
// `gzip` is only supported for tarballs
Expand Down
54 changes: 44 additions & 10 deletions QLPlugin/Views/PreviewVCs/OutlinePreviewVC.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,38 @@ import Cocoa
class OutlinePreviewVC: NSViewController, PreviewVC {
@objc dynamic var rootNodes: [FileTreeNode]
private let labelText: String?
private let expandAll: Bool

@objc dynamic var customSortDescriptors = [NSSortDescriptor(key: "name", ascending: true)]

@IBOutlet private var treeController: NSTreeController!
@IBOutlet private var outlineView: NSOutlineView!
@IBOutlet private var label: NSTextField!

required convenience init(rootNodes: [FileTreeNode], labelText: String?) {
self.init(nibName: nil, bundle: nil, rootNodes: rootNodes, labelText: labelText)
required convenience init(
rootNodes: [FileTreeNode],
labelText: String?,
expandAll: Bool = false
) {
self.init(
nibName: nil,
bundle: nil,
rootNodes: rootNodes,
labelText: labelText,
expandAll: expandAll
)
}

init(
nibName nibNameOrNil: NSNib.Name?,
bundle nibBundleOrNil: Bundle?,
rootNodes: [FileTreeNode],
labelText: String?
labelText: String?,
expandAll: Bool = false
) {
self.rootNodes = rootNodes
self.labelText = labelText
self.expandAll = expandAll
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)

// Register required value transformers
Expand All @@ -38,7 +51,11 @@ class OutlinePreviewVC: NSViewController, PreviewVC {
override func viewDidLoad() {
super.viewDidLoad()
setUpView()
expandSingleRootItem()
if expandAll {
expandAllItems()
} else {
expandSingleRootItem()
}
}

private func setUpView() {
Expand All @@ -58,6 +75,11 @@ class OutlinePreviewVC: NSViewController, PreviewVC {
outlineView.expandItem(root.children?.first!)
}
}

/// Expands all items in the outline view.
private func expandAllItems() {
outlineView.expandItem(nil, expandChildren: true)
}
}

/// `ValueTransformer` which formats the provided date.
Expand All @@ -72,9 +94,13 @@ class DateTransformer: ValueTransformer {
dateFormatter.doesRelativeDateFormatting = true
}

override class func transformedValueClass() -> AnyClass { NSString.self }
override class func transformedValueClass() -> AnyClass {
NSString.self
}

override class func allowsReverseTransformation() -> Bool { false }
override class func allowsReverseTransformation() -> Bool {
false
}

override func transformedValue(_ value: Any?) -> Any? {
guard let date = value as? Date else {
Expand All @@ -97,9 +123,13 @@ class IconTransformer: ValueTransformer {
contentsOfFile: "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericDocumentIcon.icns"
)

override class func transformedValueClass() -> AnyClass { NSImage.self }
override class func transformedValueClass() -> AnyClass {
NSImage.self
}

override class func allowsReverseTransformation() -> Bool { false }
override class func allowsReverseTransformation() -> Bool {
false
}

override func transformedValue(_ value: Any?) -> Any? {
guard let isDirectoryNumber = value as? NSNumber else {
Expand All @@ -116,9 +146,13 @@ class SizeTransformer: ValueTransformer {
let byteCountFormatter = ByteCountFormatter()
let fallbackValue = "--"

override class func transformedValueClass() -> AnyClass { NSString.self }
override class func transformedValueClass() -> AnyClass {
NSString.self
}

override class func allowsReverseTransformation() -> Bool { false }
override class func allowsReverseTransformation() -> Bool {
false
}

override func transformedValue(_ value: Any?) -> Any? {
guard let size = value as? NSNumber else {
Expand Down
129 changes: 129 additions & 0 deletions QLPlugin/Views/Previews/DirectoryPreview.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import Foundation
import os.log

class DirectoryPreview: Preview {
let fileManager = FileManager.default

/// Max number of items to include in the tree to avoid performance issues.
let maxItems = 500
/// Max recursion depth.
let maxDepth = 5

required init() {}

/// Directories inside temporary extraction paths (e.g. from ZIP browsing in Finder)
/// should not be previewed by our plugin.
private func isTemporaryPath(_ path: String) -> Bool {
path.hasPrefix("/private/var/folders/")
|| path.hasPrefix("/var/folders/")
|| path.hasPrefix("/tmp/")
|| path.hasPrefix("/private/tmp/")
}

func createPreviewVC(file: File) throws -> PreviewVC {
if isTemporaryPath(file.path) {
throw PreviewError.fileSizeError(path: file.path)
}

let fileTree = FileTree()
var itemCount = 0
scanDirectory(
url: file.url,
fileTree: fileTree,
basePath: file.url.path,
depth: 0,
itemCount: &itemCount
)

let childrenCount = countItems(nodes: fileTree.root.childrenList)
let labelText = "\(childrenCount) items"

return OutlinePreviewVC(
rootNodes: fileTree.root.childrenList,
labelText: labelText,
expandAll: true
)
}

private func scanDirectory(
url: URL,
fileTree: FileTree,
basePath: String,
depth: Int,
itemCount: inout Int
) {
guard depth < maxDepth, itemCount < maxItems else {
return
}

let contents: [URL]
do {
contents = try fileManager.contentsOfDirectory(
at: url,
includingPropertiesForKeys: [
.isDirectoryKey,
.fileSizeKey,
.contentModificationDateKey,
],
options: [.skipsHiddenFiles]
)
} catch {
os_log(
"Could not read directory %{public}s: %{public}s",
log: Log.general,
type: .error,
url.path,
error.localizedDescription
)
return
}

for itemURL in contents {
guard itemCount < maxItems else {
return
}

let resourceValues = try? itemURL.resourceValues(forKeys: [
.isDirectoryKey,
.fileSizeKey,
.contentModificationDateKey,
])
let isDir = resourceValues?.isDirectory ?? false
let size = resourceValues?.fileSize ?? 0
let dateModified = resourceValues?.contentModificationDate

// Build relative path from the base directory
let relativePath = String(itemURL.path.dropFirst(basePath.count + 1))

do {
try fileTree.addNode(
path: relativePath,
isDirectory: isDir,
size: size,
dateModified: dateModified
)
itemCount += 1
} catch {
os_log("%{public}s", log: Log.parse, type: .error, error.localizedDescription)
}

if isDir {
scanDirectory(
url: itemURL,
fileTree: fileTree,
basePath: basePath,
depth: depth + 1,
itemCount: &itemCount
)
}
}
}

private func countItems(nodes: [FileTreeNode]) -> Int {
var count = nodes.count
for node in nodes where node.isDirectory {
count += countItems(nodes: node.childrenList)
}
return count
}
}
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ Alternatively, you can install Glance directly. The installation is slightly com

<p><img src="./AppStore/Assets/Screenshots/ScreenshotTSV.png" alt="" width="600"></p>

- **Directory**: folders are previewed as a tree view showing their contents (files, subfolders, sizes and dates)

## FAQ

**There are existing Quick Look apps for some of the supported file types. Why create another one?**
Expand Down