|
1 | | -// FileInfoRow.swift |
2 | | -// MiMiNavigator |
3 | | -// |
4 | | -// Created by Iakov Senatov on 23.01.2026. |
5 | | -// Copyright © 2026 Senatov. All rights reserved. |
6 | | -// Description: Row component displaying file metadata with icon |
| 1 | + // FileInfoRow.swift |
| 2 | + // MiMiNavigator |
| 3 | + // |
| 4 | + // Created by Iakov Senatov on 23.01.2026. |
| 5 | + // Copyright © 2026 Senatov. All rights reserved. |
| 6 | + // Description: Row component displaying file metadata with icon |
7 | 7 |
|
8 | | -import SwiftUI |
9 | | -import AppKit |
| 8 | + import SwiftUI |
| 9 | + import AppKit |
10 | 10 |
|
11 | | -// MARK: - File Info Row |
12 | | -/// Displays file information including icon, path, name, date and size |
13 | | -struct FileInfoRow: View { |
14 | | - let title: String |
15 | | - let url: URL |
16 | | - let name: String |
17 | | - let size: Int64 |
18 | | - let date: Date? |
19 | | - |
20 | | - // MARK: - Body |
21 | | - var body: some View { |
22 | | - HStack(alignment: .top, spacing: 12) { |
23 | | - fileIcon |
24 | | - fileDetails |
25 | | - Spacer() |
| 11 | + // MARK: - File Info Row |
| 12 | + /// Displays file information including icon, path, name, date and size |
| 13 | + struct FileInfoRow: View { |
| 14 | + let title: String |
| 15 | + let url: URL |
| 16 | + let name: String |
| 17 | + let size: Int64 |
| 18 | + let date: Date? |
| 19 | + |
| 20 | + // MARK: - Body |
| 21 | + var body: some View { |
| 22 | + HStack(alignment: .top, spacing: 12) { |
| 23 | + fileIcon |
| 24 | + fileDetails |
| 25 | + Spacer() |
| 26 | + } |
26 | 27 | } |
27 | | - } |
28 | | - |
29 | | - // MARK: - Private Views |
30 | | - private var fileIcon: some View { |
31 | | - Image(nsImage: icon) |
32 | | - .resizable() |
33 | | - .frame(width: 48, height: 48) |
34 | | - .cornerRadius(4) |
35 | | - } |
36 | | - |
37 | | - private var fileDetails: some View { |
38 | | - VStack(alignment: .leading, spacing: 3) { |
39 | | - titleText |
40 | | - PathWithHighlight(path: parentPath) |
41 | | - nameText |
42 | | - dateText |
43 | | - sizeText |
| 28 | + |
| 29 | + // MARK: - Private Views |
| 30 | + private var fileIcon: some View { |
| 31 | + Image(nsImage: icon) |
| 32 | + .resizable() |
| 33 | + .frame(width: 48, height: 48) |
| 34 | + .cornerRadius(4) |
| 35 | + } |
| 36 | + |
| 37 | + private var fileDetails: some View { |
| 38 | + VStack(alignment: .leading, spacing: 3) { |
| 39 | + titleText |
| 40 | + PathWithHighlight(path: parentPath) |
| 41 | + nameText |
| 42 | + dateText |
| 43 | + sizeText |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + private var titleText: some View { |
| 48 | + Text(title) |
| 49 | + .font(.system(size: 12, weight: .semibold)) |
| 50 | + } |
| 51 | + |
| 52 | + private var nameText: some View { |
| 53 | + Text(name) |
| 54 | + .font(.system(size: 11)) |
| 55 | + .foregroundStyle(.primary) |
| 56 | + .lineLimit(2) |
| 57 | + } |
| 58 | + |
| 59 | + private var dateText: some View { |
| 60 | + Text(formattedDate) |
| 61 | + .font(.system(size: 10)) |
| 62 | + .foregroundStyle(.secondary) |
| 63 | + } |
| 64 | + |
| 65 | + private var sizeText: some View { |
| 66 | + Text(formattedSize) |
| 67 | + .font(.system(size: 10)) |
| 68 | + .foregroundStyle(.secondary) |
44 | 69 | } |
45 | 70 | } |
46 | | - |
47 | | - private var titleText: some View { |
48 | | - Text(title) |
49 | | - .font(.system(size: 12, weight: .semibold)) |
50 | | - } |
51 | | - |
52 | | - private var nameText: some View { |
53 | | - Text(name) |
54 | | - .font(.system(size: 11)) |
55 | | - .foregroundStyle(.primary) |
56 | | - .lineLimit(2) |
57 | | - } |
58 | | - |
59 | | - private var dateText: some View { |
60 | | - Text(formattedDate) |
61 | | - .font(.system(size: 10)) |
62 | | - .foregroundStyle(.secondary) |
63 | | - } |
64 | | - |
65 | | - private var sizeText: some View { |
66 | | - Text(formattedSize) |
67 | | - .font(.system(size: 10)) |
68 | | - .foregroundStyle(.secondary) |
69 | | - } |
70 | | -} |
71 | 71 |
|
72 | | -// MARK: - Computed Properties |
73 | | -private extension FileInfoRow { |
74 | | - var icon: NSImage { |
75 | | - NSWorkspace.shared.icon(forFile: url.path) |
76 | | - } |
77 | | - |
78 | | - var parentPath: String { |
79 | | - url.deletingLastPathComponent().path |
80 | | - } |
81 | | - |
82 | | - var formattedSize: String { |
83 | | - ByteCountFormatter.string(fromByteCount: size, countStyle: .file) |
84 | | - } |
85 | | - |
86 | | - var formattedDate: String { |
87 | | - guard let date = date else { return "" } |
88 | | - return Self.dateFormatter.string(from: date) |
| 72 | + // MARK: - Computed Properties |
| 73 | + private extension FileInfoRow { |
| 74 | + var icon: NSImage { |
| 75 | + // Cache icons by file extension to avoid thousands of NSWorkspace icon lookups |
| 76 | + let ext = url.pathExtension.lowercased() as NSString |
| 77 | + if let cached = Self.iconCache.object(forKey: ext) { |
| 78 | + return cached |
| 79 | + } |
| 80 | + |
| 81 | + let icon = NSWorkspace.shared.icon(forFile: url.path) |
| 82 | + Self.iconCache.setObject(icon, forKey: ext) |
| 83 | + return icon |
| 84 | + } |
| 85 | + |
| 86 | + var parentPath: String { |
| 87 | + url.deletingLastPathComponent().path |
| 88 | + } |
| 89 | + |
| 90 | + var formattedSize: String { |
| 91 | + ByteCountFormatter.string(fromByteCount: size, countStyle: .file) |
| 92 | + } |
| 93 | + |
| 94 | + var formattedDate: String { |
| 95 | + guard let date = date else { return "" } |
| 96 | + return Self.dateFormatter.string(from: date) |
| 97 | + } |
| 98 | + |
| 99 | + /// Cache icons by file extension (massively reduces NSWorkspace calls in large directories) |
| 100 | + static let iconCache: NSCache<NSString, NSImage> = { |
| 101 | + let cache = NSCache<NSString, NSImage>() |
| 102 | + cache.countLimit = 128 |
| 103 | + return cache |
| 104 | + }() |
| 105 | + |
| 106 | + static let dateFormatter: DateFormatter = { |
| 107 | + let formatter = DateFormatter() |
| 108 | + formatter.dateFormat = "d. MMMM yyyy 'at' HH:mm:ss" |
| 109 | + return formatter |
| 110 | + }() |
89 | 111 | } |
90 | | - |
91 | | - static let dateFormatter: DateFormatter = { |
92 | | - let formatter = DateFormatter() |
93 | | - formatter.dateFormat = "d. MMMM yyyy 'at' HH:mm:ss" |
94 | | - return formatter |
95 | | - }() |
96 | | -} |
97 | 112 |
|
98 | | -// MARK: - Preview |
99 | | -#Preview { |
100 | | - FileInfoRow( |
101 | | - title: "Copying", |
102 | | - url: URL(fileURLWithPath: "/Users/senat/Downloads/test.mp4"), |
103 | | - name: "test.mp4", |
104 | | - size: 1_234_567, |
105 | | - date: Date() |
106 | | - ) |
107 | | - .padding() |
108 | | - .frame(width: 400) |
109 | | -} |
| 113 | + // MARK: - Preview |
| 114 | + #Preview { |
| 115 | + FileInfoRow( |
| 116 | + title: "Copying", |
| 117 | + url: URL(fileURLWithPath: "/Users/senat/Downloads/test.mp4"), |
| 118 | + name: "test.mp4", |
| 119 | + size: 1_234_567, |
| 120 | + date: Date() |
| 121 | + ) |
| 122 | + .padding() |
| 123 | + .frame(width: 400) |
| 124 | + } |
0 commit comments