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:
Robin Kutesa
2026-05-17 11:17:13 +03:00
parent 6cad8d9d6c
commit 59bdad803f
32 changed files with 983 additions and 227 deletions

View File

@@ -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) }
}