Files

142 lines
5.0 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import SwiftUI
import AppKit
import UniformTypeIdentifiers
// MARK: - File shelf
//
// Drop files on the notch → "На полку": they wait in the top-left slot of the home
// view. Drag an icon out to put the file anywhere, click to open it, right-click to
// reveal or remove. The shelf keeps references (paths), not copies, and survives restarts;
// files that no longer exist are dropped silently.
struct ShelfItem: Identifiable, Codable, Equatable {
var id = UUID()
var path: String
var addedAt = Date()
var url: URL { URL(fileURLWithPath: path) }
var name: String { url.lastPathComponent }
}
@MainActor
final class FileShelf: ObservableObject {
static let shared = FileShelf()
static let maxItems = 12
private let key = "fileShelf"
@Published private(set) var items: [ShelfItem] = []
private init() {
if let data = UserDefaults.standard.data(forKey: key),
let saved = try? JSONDecoder().decode([ShelfItem].self, from: data) {
items = saved.filter { FileManager.default.fileExists(atPath: $0.path) }
}
}
func add(_ urls: [URL]) {
for url in urls where !items.contains(where: { $0.path == url.path }) {
items.insert(ShelfItem(path: url.path), at: 0)
}
if items.count > Self.maxItems { items = Array(items.prefix(Self.maxItems)) }
save()
}
func remove(_ item: ShelfItem) {
items.removeAll { $0.id == item.id }
save()
}
func clear() {
items = []
save()
}
/// Drops entries whose file was moved or deleted.
func prune() {
let alive = items.filter { FileManager.default.fileExists(atPath: $0.path) }
if alive != items { items = alive; save() }
}
private func save() {
if let data = try? JSONEncoder().encode(items) {
UserDefaults.standard.set(data, forKey: key)
}
}
}
// MARK: - Shelf pill (home view, top-left slot)
struct ShelfPill: View {
@ObservedObject private var shelf = FileShelf.shared
var body: some View {
ZStack {
if shelf.items.isEmpty {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(Color.white.opacity(0.08), style: StrokeStyle(lineWidth: 1, dash: [4, 4]))
Text("полка · брось файл на чёлку")
.font(.system(size: 10.5))
.foregroundColor(Color(hex: "#4A4E55"))
} else {
RoundedRectangle(cornerRadius: 14, style: .continuous)
.fill(Color(hex: "#0E0F11"))
RoundedRectangle(cornerRadius: 14, style: .continuous)
.stroke(Color.white.opacity(0.10), lineWidth: 1)
HStack(spacing: 6) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) {
ForEach(shelf.items) { item in
ShelfIcon(item: item)
}
}
.padding(.horizontal, 2)
}
Button {
shelf.clear()
SoundEngine.shared.play("close")
} label: {
Image(systemName: "xmark")
.font(.system(size: 8, weight: .bold))
.foregroundColor(Color(hex: "#5F646D"))
.frame(width: 16, height: 16)
.background(Circle().fill(Color.white.opacity(0.06)))
}
.buttonStyle(.plain)
.help("Очистить полку")
}
.padding(.horizontal, 8)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onAppear { shelf.prune() }
}
}
struct ShelfIcon: View {
let item: ShelfItem
@State private var isHovered = false
var body: some View {
Image(nsImage: NSWorkspace.shared.icon(forFile: item.path))
.resizable()
.interpolation(.high)
.frame(width: 28, height: 28)
.padding(2)
.background(RoundedRectangle(cornerRadius: 6).fill(Color.white.opacity(isHovered ? 0.10 : 0)))
.scaleEffect(isHovered ? 1.08 : 1)
.onHover { h in withAnimation(.easeOut(duration: 0.12)) { isHovered = h } }
.help(item.name)
.onTapGesture { NSWorkspace.shared.open(item.url) }
.onDrag {
SoundEngine.shared.play("pop")
return NSItemProvider(contentsOf: item.url) ?? NSItemProvider()
}
.contextMenu {
Button("Открыть") { NSWorkspace.shared.open(item.url) }
Button("Показать в Finder") { NSWorkspace.shared.activateFileViewerSelecting([item.url]) }
Divider()
Button("Убрать с полки") { FileShelf.shared.remove(item) }
}
}
}