@@ -3,6 +3,9 @@ import Carbon
33import SwiftUI
44
55struct SearchOverlayView : View {
6+ private static let inlinePreviewCharacterLimit = 12_000
7+ private static let metadataScanCharacterLimit = 20_000
8+
69 @ObservedObject var store : ClipboardStore
710 let onClose : ( ) -> Void
811 let onConfirm : ( ClipboardEntry ) -> Void
@@ -220,10 +223,19 @@ struct SearchOverlayView: View {
220223 if selectedEntry. kind == . image {
221224 imagePreview ( for: selectedEntry, availableSize: geometry. size)
222225 } else {
223- Text ( selectedEntry. content)
224- . font ( font ( for: selectedEntry. kind) )
225- . textSelection ( . enabled)
226- . frame ( maxWidth: . infinity, alignment: . topLeading)
226+ VStack ( alignment: . leading, spacing: 10 ) {
227+ Text ( overlayPreviewText ( for: selectedEntry) )
228+ . font ( font ( for: selectedEntry. kind) )
229+ . textSelection ( . enabled)
230+ . frame ( maxWidth: . infinity, alignment: . topLeading)
231+
232+ if isInlinePreviewTruncated ( for: selectedEntry) {
233+ Text ( " Showing an excerpt here for speed. Use ⌘Y for the full preview. " )
234+ . font ( . system( size: 11 , weight: . regular, design: . rounded) )
235+ . foregroundStyle ( . secondary)
236+ }
237+ }
238+ . frame ( maxWidth: . infinity, alignment: . topLeading)
227239 }
228240
229241 Divider ( ) . overlay ( . white. opacity ( 0.08 ) )
@@ -257,18 +269,26 @@ struct SearchOverlayView: View {
257269 @ViewBuilder
258270 private func imagePreview( for entry: ClipboardEntry , availableSize: CGSize ) -> some View {
259271 if let imagePath = entry. imagePath,
260- let image = NSImage ( contentsOfFile: imagePath) {
261- let previewHeight = adaptiveImagePreviewHeight ( for: availableSize, imageSize: image. size)
272+ let imageSize = ThumbnailCache . imageDimensions ( at: imagePath) {
273+ let previewHeight = adaptiveImagePreviewHeight ( for: availableSize, imageSize: imageSize)
274+ let maxPixelSize = max ( availableSize. width, availableSize. height) * 2
275+ let previewImage = ThumbnailCache . shared. thumbnail ( for: imagePath, maxPixelSize: maxPixelSize)
262276
263277 ZStack {
264278 RoundedRectangle ( cornerRadius: 12 , style: . continuous)
265279 . fill ( . white. opacity ( 0.05 ) )
266280
267- Image ( nsImage: image)
268- . resizable ( )
269- . scaledToFit ( )
270- . frame ( maxWidth: . infinity, maxHeight: . infinity)
271- . padding ( 8 )
281+ if let previewImage {
282+ Image ( nsImage: previewImage)
283+ . resizable ( )
284+ . scaledToFit ( )
285+ . frame ( maxWidth: . infinity, maxHeight: . infinity)
286+ . padding ( 8 )
287+ } else {
288+ Text ( " Image preview unavailable " )
289+ . font ( . system( size: 12 , weight: . regular, design: . rounded) )
290+ . foregroundStyle ( . secondary)
291+ }
272292 }
273293 . frame ( maxWidth: . infinity)
274294 . frame ( height: previewHeight)
@@ -449,11 +469,6 @@ struct SearchOverlayView: View {
449469 }
450470
451471 private func metadataRows( for entry: ClipboardEntry , contentForStats: String ) -> [ ( title: String , value: String ) ] {
452- let chars = contentForStats. count
453- let words = contentForStats
454- . split ( whereSeparator: { $0. isWhitespace || $0. isNewline } )
455- . count
456-
457472 if entry. kind == . image,
458473 let imagePath = entry. imagePath,
459474 let size = ThumbnailCache . imageDimensions ( at: imagePath) {
@@ -480,15 +495,50 @@ struct SearchOverlayView: View {
480495 return rows
481496 }
482497
498+ let stats = inlineMetadataStats ( for: contentForStats)
483499 return [
484500 ( " Type " , entry. kind. rawValue) ,
485501 ( " Source " , entry. sourceApp ?? " Unknown " ) ,
486- ( " Characters " , " \( chars ) " ) ,
487- ( " Words " , " \( words ) " ) ,
502+ ( " Characters " , stats . characterLabel ) ,
503+ ( " Words " , stats . wordLabel ) ,
488504 ( " Copied " , entry. date. formatted ( date: . abbreviated, time: . shortened) )
489505 ]
490506 }
491507
508+ private func overlayPreviewText( for entry: ClipboardEntry ) -> String {
509+ let preview = truncatedPrefix (
510+ of: entry. content,
511+ limit: Self . inlinePreviewCharacterLimit
512+ )
513+ return preview. text
514+ }
515+
516+ private func isInlinePreviewTruncated( for entry: ClipboardEntry ) -> Bool {
517+ truncatedPrefix (
518+ of: entry. content,
519+ limit: Self . inlinePreviewCharacterLimit
520+ ) . truncated
521+ }
522+
523+ private func inlineMetadataStats( for content: String ) -> ( characterLabel: String , wordLabel: String ) {
524+ let preview = truncatedPrefix (
525+ of: content,
526+ limit: Self . metadataScanCharacterLimit
527+ )
528+ let characterCount = preview. text. count
529+ let wordCount = preview. text
530+ . split ( whereSeparator: { $0. isWhitespace || $0. isNewline } )
531+ . count
532+
533+ let suffix = preview. truncated ? " + " : " "
534+ return ( " \( characterCount) \( suffix) " , " \( wordCount) \( suffix) " )
535+ }
536+
537+ private func truncatedPrefix( of text: String , limit: Int ) -> ( text: String , truncated: Bool ) {
538+ let end = text. index ( text. startIndex, offsetBy: limit, limitedBy: text. endIndex) ?? text. endIndex
539+ return ( String ( text [ ..< end] ) , end != text. endIndex)
540+ }
541+
492542 private func font( for kind: ClipboardEntry . Kind ) -> Font {
493543 switch kind {
494544 case . code, . path:
0 commit comments