feat: Kisani UI overhaul — brand identity, gallery, visual polish
- Rename app to Kisani (CFBundleDisplayName, xcodeproj → Kisani.xcodeproj) - BackupView: kisani. wordmark + SwiftUI-drawn server logo mark (no asset, adaptive ink/bg), 206pt progress ring with active-state glow, 4-page swipeable ring (progress/pending/failed/uploaded), 4-metric stats strip, friendly NAS card (Home Server label), hamburger → AppMenuView sheet - GalleryView: new NAS + local photo grid with source picker chip, 3-col lazy grid, ThumbnailCache, sync-status dots (green = backed up) - MainTabView: Browse tab → Gallery tab; tab bar uses ultraThinMaterial blur background (systemGray3 icons, 9.5pt labels) - All app icon sizes regenerated from 1024pt source via sips - LANMonitor: nasReachable Bool?, checkNASReachability(host:port:) TCP ping - project.yml: photo library usage description updated to Kisani branding Co-Authored-By: Kutesir <kutesir@provoc.ug> Co-Authored-By: Sentry <sentry@provoc.ug>
This commit is contained in:
@@ -3,79 +3,102 @@ import SwiftUI
|
||||
struct BrowseView: View {
|
||||
let connection: NASConnection
|
||||
@Binding var selectedPath: String
|
||||
var isPickerMode: Bool = false
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var currentPath: String = "/"
|
||||
@State private var currentPath: String
|
||||
@State private var items: [NASItem] = []
|
||||
@State private var isLoading = false
|
||||
@State private var error: String? = nil
|
||||
@State private var showCreateFolder = false
|
||||
@State private var newFolderName = ""
|
||||
|
||||
init(connection: NASConnection, selectedPath: Binding<String>, isPickerMode: Bool = false, initialPath: String = "/") {
|
||||
self.connection = connection
|
||||
self._selectedPath = selectedPath
|
||||
self.isPickerMode = isPickerMode
|
||||
self._currentPath = State(initialValue: initialPath)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
ZStack {
|
||||
AppTheme.background.ignoresSafeArea()
|
||||
if isPickerMode {
|
||||
NavigationStack { coreContent }
|
||||
} else {
|
||||
coreContent
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 0) {
|
||||
// Path breadcrumb bar
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "externaldrive")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(AppTheme.inkTertiary)
|
||||
Text(currentPath)
|
||||
.font(.system(size: 12, weight: .regular))
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, AppTheme.cardPad)
|
||||
.padding(.vertical, 10)
|
||||
.background(AppTheme.surfaceSunken)
|
||||
private var coreContent: some View {
|
||||
ZStack {
|
||||
AppTheme.background.ignoresSafeArea()
|
||||
|
||||
Rectangle()
|
||||
.fill(AppTheme.separator)
|
||||
.frame(height: 0.5)
|
||||
|
||||
if isLoading {
|
||||
loadingState
|
||||
} else if let err = error {
|
||||
errorState(err)
|
||||
} else if items.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
folderList
|
||||
VStack(spacing: 0) {
|
||||
// Path breadcrumb bar
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "externaldrive")
|
||||
.font(.system(size: 11, weight: .medium))
|
||||
.foregroundStyle(AppTheme.inkTertiary)
|
||||
Text(currentPath)
|
||||
.font(.system(size: 12, weight: .regular))
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
Spacer()
|
||||
if !isPickerMode && currentPath != "/" {
|
||||
Button(action: navigateUp) {
|
||||
Image(systemName: "arrow.up.circle")
|
||||
.font(.system(size: 18, weight: .regular))
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, AppTheme.cardPad)
|
||||
.padding(.vertical, 10)
|
||||
.background(AppTheme.surfaceSunken)
|
||||
|
||||
Rectangle()
|
||||
.fill(AppTheme.separator)
|
||||
.frame(height: 0.5)
|
||||
|
||||
if isLoading {
|
||||
loadingState
|
||||
} else if let err = error {
|
||||
errorState(err)
|
||||
} else if items.isEmpty {
|
||||
emptyState
|
||||
} else {
|
||||
folderList
|
||||
}
|
||||
}
|
||||
.navigationTitle("Choose folder")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
}
|
||||
.navigationTitle(isPickerMode ? "Choose folder" : "")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
if isPickerMode {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") { dismiss() }
|
||||
.font(AppTheme.body())
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
newFolderName = ""
|
||||
showCreateFolder = true
|
||||
} label: {
|
||||
Image(systemName: "folder.badge.plus")
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
}
|
||||
.disabled(currentPath == "/" && connection.nasProtocol == .smb)
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
newFolderName = ""
|
||||
showCreateFolder = true
|
||||
} label: {
|
||||
Image(systemName: "folder.badge.plus")
|
||||
.foregroundStyle(AppTheme.inkSecondary)
|
||||
}
|
||||
.disabled(currentPath == "/" && connection.nasProtocol == .smb)
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
selectButton
|
||||
}
|
||||
.alert("New folder", isPresented: $showCreateFolder) {
|
||||
TextField("Folder name", text: $newFolderName)
|
||||
Button("Create") { Task { await createFolder() } }
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
}
|
||||
.safeAreaInset(edge: .bottom) {
|
||||
if isPickerMode { selectButton }
|
||||
}
|
||||
.alert("New folder", isPresented: $showCreateFolder) {
|
||||
TextField("Folder name", text: $newFolderName)
|
||||
Button("Create") { Task { await createFolder() } }
|
||||
Button("Cancel", role: .cancel) {}
|
||||
}
|
||||
.task { await loadItems(path: currentPath) }
|
||||
.refreshable { await loadItems(path: currentPath) }
|
||||
@@ -183,14 +206,10 @@ struct BrowseView: View {
|
||||
ForEach(items) { item in
|
||||
FolderRow(
|
||||
item: item,
|
||||
isSelected: selectedPath == item.path
|
||||
) {
|
||||
if item.isDirectory {
|
||||
navigate(to: item.path)
|
||||
} else {
|
||||
selectedPath = item.path
|
||||
}
|
||||
}
|
||||
isSelected: selectedPath == item.path,
|
||||
onTap: { selectedPath = item.path },
|
||||
onNavigate: item.isDirectory ? { navigate(to: item.path) } : nil
|
||||
)
|
||||
.contextMenu {
|
||||
if item.isDirectory {
|
||||
Button {
|
||||
@@ -252,7 +271,7 @@ struct BrowseView: View {
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
selectedPath = currentPath
|
||||
if selectedPath == "/" { selectedPath = currentPath }
|
||||
dismiss()
|
||||
}) {
|
||||
Text("Select")
|
||||
@@ -300,6 +319,7 @@ struct BrowseView: View {
|
||||
}
|
||||
|
||||
private func navigate(to path: String) {
|
||||
if isPickerMode { selectedPath = "/" }
|
||||
currentPath = path
|
||||
Task { await loadItems(path: path) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user