Skip to content

Commit 0a5af61

Browse files
committed
add finder sidebar and r-menu console
1 parent 5caafbd commit 0a5af61

12 files changed

Lines changed: 343 additions & 26 deletions

GUI/Resources/curr_version.asc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2026.04.26 20:26:46 at Host: NEVA
1+
2026.04.27 01:32:47 at Host: NEVA

GUI/Sources/App/MiMiNavigatorApp+MW.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ extension MiMiNavigatorApp {
2626
// MARK: - Main Window Content
2727
var mainWindowContent: some View {
2828
log.debug(#function)
29-
return DuoFilePanelView()
29+
return DuoFilePanelView(isFinderSidebarVisible: $isFinderSidebarVisible)
3030
// MARK: - Environment
3131
.environment(appState)
3232
.environment(dragDropManager)

GUI/Sources/App/MiMiNavigatorApp.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ struct MiMiNavigatorApp: App {
2323
@State var showHiddenFiles = UserPreferences.shared.snapshot.showHiddenFiles
2424
@State var showAutomationOnboarding = false
2525
@State var showFullDiskOnboarding = false
26+
@State var isFinderSidebarVisible = false
2627

2728
// MARK: - Lifecycle State
2829

GUI/Sources/ContextMenu/ActionsEnums/PanelBackgroundAction.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ enum PanelBackgroundAction: String, CaseIterable, Identifiable {
3232
// Navigation helpers
3333
case openInFinder
3434
case openInTerminal
35+
case console
3536

3637
// Cross-panel
3738
case mirrorPath
@@ -61,6 +62,7 @@ enum PanelBackgroundAction: String, CaseIterable, Identifiable {
6162
case .sortByType: return "Sort by Type"
6263
case .openInFinder: return "Open in Finder"
6364
case .openInTerminal: return "Open in Terminal"
65+
case .console: return "Console"
6466
case .mirrorPath: return "Mirror Panel"
6567
case .openMarkedOnOtherPanel: return "Open Marked Dir on Other Panel"
6668
case .getInfo: return "Get Info"
@@ -84,6 +86,7 @@ enum PanelBackgroundAction: String, CaseIterable, Identifiable {
8486
case .sortByType: return "doc"
8587
case .openInFinder: return "folder"
8688
case .openInTerminal: return "terminal"
89+
case .console: return "terminal"
8790
case .mirrorPath: return "arrow.left.arrow.right.square"
8891
case .openMarkedOnOtherPanel: return "folder.badge.arrow.right"
8992
case .getInfo: return "info.circle"

GUI/Sources/ContextMenu/Menus/ContextMenu/CntMenuCoord+BackgroundActions.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ extension CntMenuCoord {
4040
log.info("\(#function) sort action '\(action.rawValue)' not yet implemented")
4141
case .openInFinder:
4242
RevealInFinderService.shared.revealInFinder(currentPath)
43-
case .openInTerminal:
43+
case .openInTerminal, .console:
4444
openTerminal(at: currentPath)
4545
case .mirrorPath:
4646
mirrorPathToOtherPanel(panel, appState: appState)

GUI/Sources/ContextMenu/Menus/SecondContext/ContextMenuNSMenuBuilder.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ enum ContextMenuNSMenuBuilder {
180180
menu.addItem(item(.paste, onAction: onAction, isEnabled: ClipboardManager.shared.hasContent))
181181
menu.addItem(item(.copyAsPathname, onAction: onAction))
182182
menu.addItem(item(.openInFinder, onAction: onAction))
183-
menu.addItem(item(.openInTerminal, onAction: onAction))
183+
menu.addItem(item(.console, onAction: onAction))
184184
menu.addItem(item(.getInfo, onAction: onAction))
185185
menu.addItem(item(.addToFavorites, onAction: onAction))
186186
menu.addItem(.separator())

GUI/Sources/ContextMenu/Menus/SecondContext/PanelBackgroundContextMenu.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ struct PanelBackgroundContextMenu: View {
7373
// SECTION 4: Open in external apps
7474
// ═══════════════════════════════════════════
7575
menuButton(.openInFinder)
76-
menuButton(.openInTerminal)
76+
menuButton(.console)
7777

7878
Divider()
7979

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
// FinderSidebarView.swift
2+
// MiMiNavigator
3+
//
4+
// Created by Iakov Senatov on 27.04.2026.
5+
// Copyright © 2026 Senatov. All rights reserved.
6+
// Description: Finder-style source list embedded in the main dual-panel window.
7+
8+
import AppKit
9+
import FileModelKit
10+
import SwiftUI
11+
12+
// MARK: - Finder Sidebar View
13+
struct FinderSidebarView: View {
14+
let appState: AppState
15+
@State private var volumes: [FinderSidebarItem] = []
16+
@State private var selectedID: String?
17+
18+
private enum Layout {
19+
static let rowHeight: CGFloat = 26
20+
static let iconWidth: CGFloat = 18
21+
static let horizontalPadding: CGFloat = 10
22+
static let sectionSpacing: CGFloat = 10
23+
}
24+
25+
// MARK: - Body
26+
var body: some View {
27+
ScrollView {
28+
VStack(alignment: .leading, spacing: Layout.sectionSpacing) {
29+
section(title: "Favorites", items: favoriteItems)
30+
section(title: "iCloud", items: iCloudItems)
31+
section(title: "Locations", items: locationItems)
32+
tagsSection
33+
Spacer(minLength: 0)
34+
}
35+
.padding(.horizontal, 10)
36+
.padding(.vertical, 12)
37+
}
38+
.background(.regularMaterial)
39+
.overlay(alignment: .trailing) {
40+
Divider()
41+
}
42+
.onAppear(perform: refreshVolumes)
43+
}
44+
45+
// MARK: - Favorites
46+
private var favoriteItems: [FinderSidebarItem] {
47+
let home = FileManager.default.homeDirectoryForCurrentUser
48+
return [
49+
FinderSidebarItem(title: "AirDrop", systemImage: "antenna.radiowaves.left.and.right", action: .airDrop),
50+
FinderSidebarItem(title: "Photos Library", systemImage: "photo.on.rectangle", action: .openIfExists(home.appendingPathComponent("Pictures/Photos Library.photoslibrary"))),
51+
FinderSidebarItem(title: "Documents", systemImage: "folder.fill", action: .navigate(home.appendingPathComponent("Documents", isDirectory: true))),
52+
FinderSidebarItem(title: "Applications", systemImage: "folder.fill", action: .navigate(URL(fileURLWithPath: "/Applications", isDirectory: true))),
53+
FinderSidebarItem(title: "Desktop", systemImage: "folder.fill", action: .navigate(home.appendingPathComponent("Desktop", isDirectory: true))),
54+
FinderSidebarItem(title: "Downloads", systemImage: "folder.fill", action: .navigate(home.appendingPathComponent("Downloads", isDirectory: true))),
55+
FinderSidebarItem(title: "Movies", systemImage: "folder.fill", action: .navigate(home.appendingPathComponent("Movies", isDirectory: true))),
56+
FinderSidebarItem(title: "Music", systemImage: "folder.fill", action: .navigate(home.appendingPathComponent("Music", isDirectory: true))),
57+
FinderSidebarItem(title: "Pictures", systemImage: "folder.fill", action: .navigate(home.appendingPathComponent("Pictures", isDirectory: true)))
58+
].filter(\.shouldShow)
59+
}
60+
61+
// MARK: - iCloud
62+
private var iCloudItems: [FinderSidebarItem] {
63+
let cloudDocs = FileManager.default.homeDirectoryForCurrentUser
64+
.appendingPathComponent("Library/Mobile Documents/com~apple~CloudDocs", isDirectory: true)
65+
return [
66+
FinderSidebarItem(title: "iCloud Drive", systemImage: "icloud", action: .navigate(cloudDocs))
67+
].filter(\.shouldShow)
68+
}
69+
70+
// MARK: - Locations
71+
private var locationItems: [FinderSidebarItem] {
72+
volumes + [
73+
FinderSidebarItem(title: "Network", systemImage: "globe", action: .network)
74+
]
75+
}
76+
77+
// MARK: - Tags
78+
private var tagsSection: some View {
79+
VStack(alignment: .leading, spacing: 2) {
80+
sectionHeader("Tags")
81+
ForEach(FinderSidebarTag.standard) { tag in
82+
HStack(spacing: 8) {
83+
Circle()
84+
.fill(tag.color)
85+
.frame(width: 12, height: 12)
86+
Text(tag.title)
87+
.font(.system(size: 13))
88+
.lineLimit(1)
89+
Spacer(minLength: 0)
90+
}
91+
.frame(height: Layout.rowHeight)
92+
.padding(.horizontal, Layout.horizontalPadding)
93+
}
94+
}
95+
}
96+
97+
// MARK: - Section
98+
private func section(title: String, items: [FinderSidebarItem]) -> some View {
99+
VStack(alignment: .leading, spacing: 2) {
100+
sectionHeader(title)
101+
ForEach(items) { item in
102+
Button {
103+
handle(item)
104+
} label: {
105+
row(item)
106+
}
107+
.buttonStyle(.plain)
108+
.help(item.helpText)
109+
}
110+
}
111+
}
112+
113+
// MARK: - Section Header
114+
private func sectionHeader(_ title: String) -> some View {
115+
Text(title)
116+
.font(.system(size: 11, weight: .semibold))
117+
.foregroundStyle(.secondary)
118+
.padding(.horizontal, Layout.horizontalPadding)
119+
.padding(.top, 2)
120+
}
121+
122+
// MARK: - Row
123+
private func row(_ item: FinderSidebarItem) -> some View {
124+
HStack(spacing: 8) {
125+
Image(systemName: item.systemImage)
126+
.font(.system(size: 14))
127+
.symbolRenderingMode(.hierarchical)
128+
.foregroundStyle(item.tint)
129+
.frame(width: Layout.iconWidth, height: Layout.iconWidth)
130+
Text(item.title)
131+
.font(.system(size: 13))
132+
.foregroundStyle(.primary)
133+
.lineLimit(1)
134+
.truncationMode(.middle)
135+
Spacer(minLength: 0)
136+
}
137+
.frame(height: Layout.rowHeight)
138+
.padding(.horizontal, Layout.horizontalPadding)
139+
.background(selectionBackground(for: item))
140+
.clipShape(RoundedRectangle(cornerRadius: 6, style: .continuous))
141+
}
142+
143+
// MARK: - Selection Background
144+
private func selectionBackground(for item: FinderSidebarItem) -> some View {
145+
RoundedRectangle(cornerRadius: 6, style: .continuous)
146+
.fill(isSelected(item) ? Color.accentColor.opacity(0.18) : Color.clear)
147+
}
148+
149+
// MARK: - Selection
150+
private func isSelected(_ item: FinderSidebarItem) -> Bool {
151+
guard selectedID == item.id else { return false }
152+
return true
153+
}
154+
155+
// MARK: - Handle Item
156+
private func handle(_ item: FinderSidebarItem) {
157+
selectedID = item.id
158+
switch item.action {
159+
case .airDrop:
160+
openAirDrop()
161+
case .network:
162+
NetworkNeighborhoodCoordinator.shared.toggle()
163+
case .navigate(let url):
164+
navigate(to: url)
165+
case .openIfExists(let url):
166+
openIfExists(url)
167+
}
168+
}
169+
170+
// MARK: - Navigate
171+
private func navigate(to url: URL) {
172+
Task { @MainActor in
173+
let panel = appState.focusedPanel
174+
appState.updatePath(url, for: panel)
175+
await appState.scanner.clearCooldown(for: panel)
176+
await appState.scanner.refreshFiles(currSide: panel, force: true)
177+
log.info("[FinderSidebar] navigate panel=\(panel) path='\(url.path)'")
178+
}
179+
}
180+
181+
// MARK: - Open If Exists
182+
private func openIfExists(_ url: URL) {
183+
var isDirectory: ObjCBool = false
184+
guard FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) else { return }
185+
if isDirectory.boolValue {
186+
navigate(to: url)
187+
} else {
188+
NSWorkspace.shared.open(url)
189+
}
190+
}
191+
192+
// MARK: - Open AirDrop
193+
private func openAirDrop() {
194+
guard let url = URL(string: "airdrop://") else { return }
195+
NSWorkspace.shared.open(url)
196+
}
197+
198+
// MARK: - Refresh Volumes
199+
private func refreshVolumes() {
200+
let urls = FileManager.default.mountedVolumeURLs(includingResourceValuesForKeys: [.volumeNameKey], options: [.skipHiddenVolumes]) ?? []
201+
volumes = urls.compactMap(volumeItem)
202+
}
203+
204+
// MARK: - Volume Item
205+
private func volumeItem(for url: URL) -> FinderSidebarItem? {
206+
guard url.isFileURL else { return nil }
207+
let name = (try? url.resourceValues(forKeys: [.volumeNameKey]).volumeName) ?? url.lastPathComponent
208+
guard !name.isEmpty else { return nil }
209+
return FinderSidebarItem(title: name, systemImage: "externaldrive.fill", tint: .secondary, action: .navigate(url))
210+
}
211+
}
212+
213+
// MARK: - Finder Sidebar Item
214+
private struct FinderSidebarItem: Identifiable {
215+
let title: String
216+
let systemImage: String
217+
let tint: Color
218+
let action: FinderSidebarAction
219+
220+
var id: String {
221+
"\(title)-\(helpText)"
222+
}
223+
224+
var helpText: String {
225+
switch action {
226+
case .airDrop:
227+
return "AirDrop"
228+
case .network:
229+
return "Network"
230+
case .navigate(let url), .openIfExists(let url):
231+
return url.path
232+
}
233+
}
234+
235+
var shouldShow: Bool {
236+
switch action {
237+
case .airDrop, .network:
238+
return true
239+
case .navigate(let url), .openIfExists(let url):
240+
return FileManager.default.fileExists(atPath: url.path)
241+
}
242+
}
243+
244+
init(title: String, systemImage: String, tint: Color = .blue, action: FinderSidebarAction) {
245+
self.title = title
246+
self.systemImage = systemImage
247+
self.tint = tint
248+
self.action = action
249+
}
250+
}
251+
252+
// MARK: - Finder Sidebar Action
253+
private enum FinderSidebarAction {
254+
case airDrop
255+
case network
256+
case navigate(URL)
257+
case openIfExists(URL)
258+
}
259+
260+
// MARK: - Finder Sidebar Tag
261+
private struct FinderSidebarTag: Identifiable {
262+
let title: String
263+
let color: Color
264+
265+
var id: String { title }
266+
267+
static let standard: [FinderSidebarTag] = [
268+
FinderSidebarTag(title: "Red", color: .red),
269+
FinderSidebarTag(title: "Orange", color: .orange),
270+
FinderSidebarTag(title: "Yellow", color: .yellow),
271+
FinderSidebarTag(title: "Green", color: .green),
272+
FinderSidebarTag(title: "Blue", color: .blue),
273+
FinderSidebarTag(title: "Purple", color: .purple),
274+
FinderSidebarTag(title: "Gray", color: .gray)
275+
]
276+
}

0 commit comments

Comments
 (0)