mirror of
https://github.com/abue-ammar/tinycast.git
synced 2026-10-02 08:14:38 +08:00
* feat: Enhance file search functionality with type filtering and Quick Look support - Introduced FileSearchFilter to allow users to filter search results by type (All Types, Folders, Documents, Images, Audio, Video, Archives). - Updated FileSearchScreen to display Recently Used files when the search query is empty. - Implemented Quick Look functionality within the file search panel, allowing users to preview files without leaving the search interface. - Enhanced the UI to include a preview pane alongside search results, displaying relevant file information. - Added keyboard shortcuts for file actions (copy, paste, trash) and Quick Look toggle. - Improved the handling of empty states and error messages during file searches. - Updated documentation to reflect new features and usage instructions. * Read one attribute per Spotlight result, not four Opening Search Files sat for half a second before its Recently Used rows appeared, and a broad query took the best part of a second. Neither was Spotlight: `MDQueryExecute` returns in 15–75 ms. It was `MDItemCopyAttribute`, which costs about half a millisecond per attribute per result — reading the content type, the invisible flag and both date stamps over the ~430 candidates a recents query matches was 430 ms of the 550. `kMDItemPath` is the exception: `MDQuery` hands it back from its own cache, so a thousand of them read in 2.8 ms. Everything else a row needs — is it a folder, is it hidden, is it an application — now comes from one `resourceValues` stat, taken only for the candidates the ignore list did not already drop. Two hundred URLs stat in 13 ms where two hundred metadata fetches cost 200 ms. Recents no longer date every candidate either. Spotlight sorts on one attribute, so the service runs one sorted query per stamp and merges their heads; only the first twenty rows of each list can reach the merged one, and only those are dated. The sort attribute has to be named in `MDQueryCreate` — set afterwards through `MDQuerySetSortOrder` it is ignored, which is why the first version had to sort locally. The blank screen also skips the typing debounce, having no next keystroke to coalesce with. Measured on the developer home, release-optimized: recents 41 ms on a repeat against ~550 ms, and the five benchmark queries 54–107 ms against 192–831 ms. Building the expressions moved with the queries into the service, where the policy that shapes them already is; the session now owns only when a search runs. Move to Trash takes ⌃X, the chord the clipboard's delete already uses, and Paste File moves up beside Copy File in the Actions menu. * Let the preview play, in the pane as well as the overlay The overlay's dismissing tap gesture was laid over the whole card, preview included, so the click that should have hit a movie's play button closed the overlay instead — the transport drew, took the press and never saw it. Only the margin around the card dismisses now; anything over the preview belongs to the preview. The pane beside the list shows the file itself for the same reason it was worth having Quick Look at all: a still says nothing a movie or a long document needs said. `QuickLookSurface` moves out of the overlay into its own file and mounts in both, and the pane's copy waits 180 ms for the selection to hold, so arrow-keying a list of two hundred rows opens no preview it is about to drop. The thumbnail stands in until then. The live view is torn down whenever the palette is ordered out, the overlay covers it, or a folder is selected — one key over all three, so no `onChange` races the task, and never two preview extensions running for one file. * Give the preview its own player, 16:9, and an Escape AVKit cannot eat Three things the preview pane still got wrong. A movie never played. `QLPreviewView` draws a first frame and hands out a transport, but inside a non-activating panel it does not play, so movies and audio now go to an `AVPlayerView` of File Search's own. It is a copy of the shape the clipboard's preview uses rather than a share of it: that pane is a different surface with its own sizing and its own lifetime, and forty lines of teardown is the cheaper trade against coupling the two. The stage is 16:9 and sized before the block beneath it. Without the layout priority the aspect ratio fits itself into whatever height the Information rows left over, which is backwards — it shrank the preview to 328×173 instead of filling the pane's width at 436×234. The rows now scroll in what is left, through the plain scroll view the clipboard's preview uses, since a thin scrollbar over a document is chrome on chrome. Escape did not close Quick Look once a player was in it: a focused `AVPlayerView` answers the key window's Escape before SwiftUI's handler ever sees it. `PalettePanel.sendEvent` is the one place ahead of the responder chain, so the panel now owns that press while the overlay is up, and `PaletteEscapeAction` goes back to the shape it had. The Close chip is a button now too, for the pointer. * feat: Add FileSearchFileView for unified file preview handling in search results * feat: Replace FileSearchFileView with FileSearchSurface for unified file handling in search results * Hand the preview another file rather than building it a new one Clicking a row went blank before it went live. The stage gated the whole surface on a flag that flipped false on every selection, so each move dismantled the `QLPreviewView`, closed it, showed `Color.clear`, then built a fresh one — a teardown and a rebuild to show the next file. The surface now outlives the selection: only the settled URL changes, so a move hands the same view another item. The settle drops to 80 ms, which still coalesces a held arrow key and no longer reads as a wait on a click. Measured against the real QuickLook machinery: the first load in a process is ~130 ms and every load after it ~10 ms, so what a click waits for is the settle, not the preview. Text goes back to QuickLook with it. Drawing it here fixed the wrong thing — its scroll view is configured exactly like the palette's own, overlay style and autohiding, and QuickLook renders a document better than a monospaced `Text` in a `ScrollView` does. * Say in the feature doc what the preview surface now does * Answer a click on the press, and drop the timer in front of the preview The second a click took was SwiftUI waiting: `.onTapGesture(count: 2)` cannot deliver the single tap until the system's double-click interval has passed without a second press, so the selection — and the preview that follows it — sat still for that whole window. The clipboard's rows never felt this because they select in `mouseDown`. File Search now does the same, through an `onRowClick` catcher beside the right-click one already in `DesignSystem`: select on the press, open when the press is the second. The settle goes with it, and the state it needed. It was guarding a cost that is not there: measured against the real QuickLook machinery inside a panel shaped like the palette's — borderless, floating, non-activating, never key — handing a live `QLPreviewView` another file paints in about 8 ms, and the first load in a process in about 130 ms. Debouncing 8 ms of work bought nothing and spent 80. What is left is a surface mounted while it should be on screen and unmounted when it should not, which is also the whole of its teardown: no duration, no flag, no task.
68 lines
2.6 KiB
Swift
68 lines
2.6 KiB
Swift
import Foundation
|
|
|
|
@main
|
|
struct FileSearchPerformance {
|
|
static func main() throws {
|
|
let homeDirectory = FileManager.default.homeDirectoryForCurrentUser
|
|
let arguments = Array(CommandLine.arguments.dropFirst())
|
|
let queries = arguments.isEmpty ? ["a", "e", "swift", "pdf", "project"] : arguments
|
|
// The second list is deliberately heavy, so the run prices pattern matching.
|
|
let policies = [
|
|
(
|
|
"shipped",
|
|
FileSearchPolicy(
|
|
scopes: FileSearchScope.defaultScopes, ignorePatterns: [],
|
|
homeDirectory: homeDirectory)
|
|
),
|
|
(
|
|
"+patterns",
|
|
FileSearchPolicy(
|
|
scopes: FileSearchScope.defaultScopes,
|
|
ignorePatterns: ["*.tmp", "*.log", "**/[Cc]ache/**", "**/Logs/**", "vendor"],
|
|
homeDirectory: homeDirectory)
|
|
)
|
|
]
|
|
|
|
print("File search service latency; the palette adds a 120 ms debounce")
|
|
print("Home: \(homeDirectory.path)")
|
|
for (label, policy) in policies {
|
|
// The empty query is the blank screen's own list, and its latency is the one felt most.
|
|
for query in [""] + queries {
|
|
try measure(query: query, policy: policy, label: label)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func measure(
|
|
query: String, policy: FileSearchPolicy, label: String
|
|
) throws {
|
|
var samples: [Double] = []
|
|
var first = 0.0
|
|
var resultCount = 0
|
|
for run in 0..<6 {
|
|
let start = ContinuousClock.now
|
|
let results = try FileSearchService.search(query: query, policy: policy)
|
|
let elapsed = milliseconds(start.duration(to: .now))
|
|
if run == 0 {
|
|
first = elapsed
|
|
} else {
|
|
samples.append(elapsed)
|
|
}
|
|
resultCount = results.count
|
|
}
|
|
let ordered = samples.sorted()
|
|
let name = "\(label) \(query.isEmpty ? "(recents)" : query)"
|
|
.padding(toLength: 22, withPad: " ", startingAt: 0)
|
|
let metrics = String(
|
|
format: "first %7.2f ms repeat median %7.2f ms max %7.2f ms %3d results",
|
|
first, ordered[ordered.count / 2], ordered.last ?? 0, resultCount)
|
|
print("\(name) \(metrics)")
|
|
}
|
|
|
|
private static func milliseconds(_ duration: Duration) -> Double {
|
|
let components = duration.components
|
|
return Double(components.seconds) * 1_000
|
|
+ Double(components.attoseconds) / 1_000_000_000_000_000
|
|
}
|
|
}
|