mirror of
https://github.com/bitsycore/Allofit.git
synced 2026-10-06 21:07:27 +00:00
Security - Elevated copies: root only reads the original; the copy is written by the user (sudo -u tee), so root never chowns / chmods a user-controlled path. Staging folder forced to 0700. - Service logs moved from fixed /tmp names to /Library/Logs/Allofit (root-owned) and ~/Library/Logs/Allofit; Diagnostics reveals instead of opening the log. - Index files are owner-only (0600; the root daemon's belongs to the installing user), set on the temp file before an atomic rename. - Root install passes the plist inline (base64, plutil -lint) instead of a user-writable temp file; the binary comes from Bundle.main. - Cache loader caps and checks the declared payload size; each save uses its own temp file. - Release action pinned to a commit; non-system LC_RPATHs stripped. Correctness - Move to Trash removes the files from the index (the watcher ignores the app's own operations) and registers Undo (Put Back); trashed folders are matched with the original URLs; failures are shown. - The saved event id stays below pending subtree walks (GUI and service). - Roots / exclusions changes restart the watcher from the snapshot's id. - Case-only renames no longer leave a ghost entry. - Service mode is saved only after a successful install; a saved but missing service falls back to the in-process indexer. Performance - New folders are merged without the O(n) removal pass. - Size / date sorts use a compact key array (539 -> 47 ms for 630k). - Selection, preview and actions use the selected records directly. - Service saves at most every 15 s; reader reloads pause while hidden and are deferred instead of dropped; window close saves only when dirty. Usability - Results appear during the first index; empty-list explanations. - Down arrow moves to the results, Up on the first row back; history on Up / Option-Up / Option-Down. - Search syntax popover and Help menu; shortcuts shown in the context menu; confirmations for Clear Cache and Uninstall; privacy usage strings; Group Containers excluded by default; wording, VoiceOver labels, plural.
171 lines
5.0 KiB
Swift
171 lines
5.0 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
import Quartz
|
|
|
|
// QuickLookPreviewView wraps Quartz's QLPreviewView so an inline Quick
|
|
// Look preview can be embedded inside a SwiftUI hierarchy. The same
|
|
// renderer powers the floating QLPreviewPanel (spacebar), so file-type
|
|
// coverage (PDFs, images, video, source files, plists, etc.) is
|
|
// identical between the inline pane and the floating panel.
|
|
struct QuickLookPreviewView: NSViewRepresentable {
|
|
|
|
// the file to preview; nil clears the view
|
|
let url: URL?
|
|
|
|
// creates the embedded Quick Look view
|
|
func makeNSView(context inContext: Context) -> NSView {
|
|
guard let vView = QLPreviewView(frame: .zero, style: .normal) else {
|
|
return NSView()
|
|
}
|
|
// keep the view alive when the parent window closes - we own its
|
|
// lifetime via SwiftUI, not via QLPreviewPanel's modal behaviour
|
|
vView.shouldCloseWithWindow = false
|
|
vView.autostarts = true
|
|
return vView
|
|
}
|
|
|
|
// shows the current file
|
|
func updateNSView(_ inView: NSView, context inContext: Context) {
|
|
guard let vQlView = inView as? QLPreviewView else { return }
|
|
vQlView.previewItem = (url as NSURL?)
|
|
}
|
|
}
|
|
|
|
// PreviewPane is the right-hand side panel in the main window. When
|
|
// exactly one row is selected it renders a Quick Look preview plus a
|
|
// small metadata footer. If the file isn't user-readable the preview
|
|
// area becomes a single big tap-target showing a lock icon - clicking
|
|
// it (or the badge that appears on the selected row when the pane is
|
|
// closed) triggers the sudo-elevation flow.
|
|
struct PreviewPane: View {
|
|
|
|
@EnvironmentObject var access: AccessManager
|
|
@EnvironmentObject var searchModel: WindowSearchModel
|
|
// passed in from ContentView (its @State) so this view re-renders
|
|
// whenever the user's selection changes
|
|
let selection: [FileRecord]
|
|
|
|
// the record to preview: only with exactly one selected
|
|
private var selectedRecord: FileRecord? {
|
|
return selection.count == 1 ? selection.first : nil
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
if let vRecord = selectedRecord {
|
|
content(for: vRecord)
|
|
} else {
|
|
emptyState
|
|
}
|
|
}
|
|
.background(Color(NSColor.controlBackgroundColor))
|
|
}
|
|
|
|
// preview + metadata footer for one selected record
|
|
private func content(for inRecord: FileRecord) -> some View {
|
|
let vEffectiveUrl = access.effectiveURL(for: inRecord)
|
|
let vReadable = ElevatedAccess.canRead(path: vEffectiveUrl.path)
|
|
return VStack(spacing: 0) {
|
|
Group {
|
|
if vReadable {
|
|
QuickLookPreviewView(url: vEffectiveUrl)
|
|
} else {
|
|
authorizePrompt(for: inRecord)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
|
|
Divider()
|
|
metadata(for: inRecord)
|
|
}
|
|
}
|
|
|
|
// full-area authorize hint shown when the selected file isn't
|
|
// user-readable. The whole area is the button target so users
|
|
// can click anywhere over the locked preview to authorize.
|
|
private func authorizePrompt(for inRecord: FileRecord) -> some View {
|
|
Button {
|
|
Task { await access.authorize(inRecord) }
|
|
} label: {
|
|
VStack(spacing: 10) {
|
|
if access.isAuthorizing(inRecord.id) {
|
|
ProgressView()
|
|
.controlSize(.regular)
|
|
} else {
|
|
Image(systemName: "lock.shield.fill")
|
|
.font(.system(size: 40))
|
|
.foregroundStyle(.orange)
|
|
}
|
|
Text(access.isAuthorizing(inRecord.id)
|
|
? "Authorizing…"
|
|
: "Click to authorize preview")
|
|
.font(.callout)
|
|
.foregroundColor(.secondary)
|
|
if let vErr = access.lastError, !access.isAuthorizing(inRecord.id) {
|
|
Text(vErr)
|
|
.font(.caption)
|
|
.foregroundColor(.red)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 20)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(access.isAuthorizing(inRecord.id))
|
|
}
|
|
|
|
// thin metadata bar at the bottom of the preview pane
|
|
private func metadata(for inRecord: FileRecord) -> some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(inRecord.name)
|
|
.font(.headline)
|
|
.lineLimit(2)
|
|
.truncationMode(.middle)
|
|
Text(inRecord.parentPath)
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.truncationMode(.middle)
|
|
.lineLimit(1)
|
|
.textSelection(.enabled)
|
|
HStack(spacing: 6) {
|
|
if !inRecord.isDirectory {
|
|
Text(Formatters.size(bytes: inRecord.size))
|
|
.monospacedDigit()
|
|
Text("·")
|
|
}
|
|
Text("Modified \(Formatters.date(inRecord.dateModified))")
|
|
.monospacedDigit()
|
|
}
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
.padding(12)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.background(.bar)
|
|
}
|
|
|
|
// placeholder shown when nothing or multiple rows are selected
|
|
private var emptyState: some View {
|
|
VStack(spacing: 10) {
|
|
Image(systemName: "eye.slash")
|
|
.font(.system(size: 32))
|
|
.foregroundColor(.secondary.opacity(0.6))
|
|
Text(placeholderText)
|
|
.font(.callout)
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 16)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
}
|
|
|
|
private var placeholderText: String {
|
|
if selection.isEmpty {
|
|
return "Select a file to preview"
|
|
}
|
|
return "\(selection.count) items selected"
|
|
}
|
|
}
|