Skip to content
Merged
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
89 changes: 76 additions & 13 deletions QuickShelf/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
// Created by slowhand on 2025/07/04.
//

import AppKit
import SwiftUI
import QuickLook

Expand All @@ -20,6 +21,10 @@ struct ContentView: View {
@State private var selection = Set<URL>()
@State private var previewUrl: URL?

@State private var editingUrl: URL?
@State private var renameText: String = ""
@State private var errorMessage: String?

var body: some View {
VStack(alignment: .leading) {
Text("Folder")
Expand All @@ -37,20 +42,36 @@ struct ContentView: View {
.font(.title3)
List(selection: $selection) {
ForEach(items.standardSorted(), id: \.url) { item in
ShelfItemView(
item: item,
isSelected: selection.contains(item.url),
onPreview: { url in
if let anchor = NSApp.keyWindow {
SlidePanelPreview.shared.show(url: url, beside: anchor, side: side,
size: NSSize(width: 380, height: 300))
}
}
)
.alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
if editingUrl == item.url {
ShelfItemEditView(
item: item,
text: $renameText,
onCommit: { commitRename() },
onCancel: { cancelRename() }
)
.alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
.listRowSeparatorTint(Color.white.opacity(0.3))
.tag(item.url)
} else {
ShelfItemView(
item: item,
isSelected: selection.contains(item.url),
isMultiSelected: selection.count != 1,
onPreview: { url in
if let anchor = NSApp.keyWindow {
SlidePanelPreview.shared.show(
url: url, beside: anchor, side: side,
size: NSSize(width: 380, height: 300)
)
}
},
onEdit: { startInlineRename() }
)
.alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
.listRowSeparatorTint(Color.white.opacity(0.3))
.tag(item.url)
.draggable(item)
}
}
}
.frame(height: 300)
Expand Down Expand Up @@ -83,8 +104,50 @@ struct ContentView: View {
}
}
}
.onDisappear {
SlidePanelPreview.shared.hide()
.onDisappear { SlidePanelPreview.shared.hide() }
.overlay(alignment: .bottom) {
if let msg = errorMessage {
ErrorBannerView(message: msg) {
withAnimation { errorMessage = nil }
}
.transition(.move(edge: .bottom).combined(with: .opacity))
.zIndex(10)
.padding(.bottom, 8)
}
}
}

private func startInlineRename() {
guard selection.count == 1, let url = selection.first else { return }
editingUrl = url
renameText = url.lastPathComponent
}

private func cancelRename() {
editingUrl = nil
renameText = ""
}

private func commitRename() {
guard let src = editingUrl else { return }
let newName = renameText.trimmingCharacters(in: .whitespacesAndNewlines)
guard !newName.isEmpty else { cancelRename(); return }

let dir = src.deletingLastPathComponent()
let dest = dir.appendingPathComponent(newName)

if FileManager.default.fileExists(atPath: dest.path) {
cancelRename();
return
}
do {
try FileManager.default.moveItem(at: src, to: dest)
self.items = load(path: dir)
self.selection = [dest]
cancelRename()
} catch {
NSSound.beep()
withAnimation { errorMessage = error.localizedDescription }
}
}

Expand Down
52 changes: 52 additions & 0 deletions QuickShelf/Views/ErrorBannerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// ErrorBannerView.swift
// QuickShelf
//
// Created by slowhand on 2025/09/04.
//

import SwiftUI

struct ErrorBannerView: View {
@State private var autoHideTaskID = UUID()

let message: String
var onClose: () -> Void

var body: some View {
HStack(spacing: 10) {
Image(systemName: "exclamationmark.triangle.fill")
Text(message)
.lineLimit(2)
.truncationMode(.tail)
Spacer()
Button {
onClose()
} label: {
Image(systemName: "xmark")
}
.buttonStyle(.plain)
.keyboardShortcut(.cancelAction)
}
.padding(.horizontal, 12)
.padding(.vertical, 10)
.background(.regularMaterial)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(.red.opacity(0.6), lineWidth: 1)
)
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding(.horizontal, 8)
.task(id: message) {
try? await Task.sleep(nanoseconds: 3_000_000_000)
await MainActor.run { onClose() }
}
}
}

#Preview {
ErrorBannerView(
message: "message",
onClose: {}
)
}
56 changes: 56 additions & 0 deletions QuickShelf/Views/ShelfItemEditView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// ShelfItemEditView.swift
// QuickShelf
//
// Created by slowhand on 2025/09/04.
//

import SwiftUI

struct ShelfItemEditView: View {
let item: ShelfItem
@Binding var text: String
var onCommit: () -> Void
var onCancel: () -> Void

@FocusState private var focused: Bool

var body: some View {
HStack {
if item.isDirectory {
Image(systemName: "folder")
} else {
if ProcessInfo.isPreview {
Image(systemName: "text.page")
} else {
Image(nsImage: NSWorkspace.shared.icon(forFile: item.url.path))
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
}
}

TextField("New name", text: $text, onCommit: onCommit)
.textFieldStyle(.roundedBorder)
.focused($focused)
.onAppear { focused = true }

Spacer()
}
.padding(.all, 8)
.onExitCommand { onCancel() }
}
}

#Preview {
@Previewable @State var text = ""
ShelfItemEditView(
item: ShelfItem(
url: URL(filePath: "/path/to/fileName.txt")!,
isDirectory: false
),
text: $text,
onCommit: {},
onCancel: {}
)
}
28 changes: 22 additions & 6 deletions QuickShelf/Views/ShelfItemView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ struct ShelfItemView: View {

let item: ShelfItem
let isSelected: Bool
let isMultiSelected: Bool
let onPreview: (URL) -> Void
let onEdit: () -> Void

var body: some View {
HStack {
Expand All @@ -32,15 +34,24 @@ struct ShelfItemView: View {
Text(item.url.lastPathComponent)
.foregroundColor(colorScheme == .dark ? .primary : .white.opacity(0.7))
Spacer()
if isSelected && !item.isDirectory {
if isSelected && !isMultiSelected {
Button {
onPreview(item.url)
} label: {
Image(systemName: "eye")
}
.help("Preview")
.buttonStyle(.borderless)
.keyboardShortcut(.space, modifiers: [])
.help("Preview")
.buttonStyle(.borderless)
.keyboardShortcut(.space, modifiers: [])
.opacity(!item.isDirectory ? 1 : 0)
Button {
onEdit()
} label: {
Image(systemName: "pencil")
}
.help("Rename")
.buttonStyle(.borderless)
.keyboardShortcut(.return, modifiers: [])
}
}
.padding(.all, 8)
Expand All @@ -54,7 +65,9 @@ struct ShelfItemView: View {
isDirectory: false
),
isSelected: false,
onPreview: { url in print("Previewing: \(url)")}
isMultiSelected: false,
onPreview: { url in print("Previewing: \(url)")},
onEdit: {}
)
}

Expand All @@ -65,5 +78,8 @@ struct ShelfItemView: View {
isDirectory: true
),
isSelected: false,
onPreview: { url in print("Previewing: \(url)")})
isMultiSelected: false,
onPreview: { url in print("Previewing: \(url)")},
onEdit: {}
)
}