app fixes

This commit is contained in:
2025-10-20 00:04:18 +02:00
parent 7c492627f0
commit cf5a848741
3 changed files with 150 additions and 58 deletions

View File

@@ -268,6 +268,7 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "cardboy-companion/Info.plist"; INFOPLIST_FILE = "cardboy-companion/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Cardboy;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld."; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld."; INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -304,6 +305,7 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "cardboy-companion/Info.plist"; INFOPLIST_FILE = "cardboy-companion/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = Cardboy;
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld."; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld.";
INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld."; INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription = "Cardboy Companion needs Bluetooth to sync time with your handheld.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;

View File

@@ -98,6 +98,68 @@ private struct FileManagerTabView: View {
@Binding var shareURL: URL? @Binding var shareURL: URL?
@Binding var errorWrapper: ErrorWrapper? @Binding var errorWrapper: ErrorWrapper?
@State private var navigationPath: [String] = []
var body: some View {
ZStack {
NavigationStack(path: $navigationPath) {
DirectoryView(
path: "/",
navigationPath: $navigationPath,
shareURL: $shareURL,
errorWrapper: $errorWrapper
)
.navigationDestination(for: String.self) { destination in
DirectoryView(
path: destination,
navigationPath: $navigationPath,
shareURL: $shareURL,
errorWrapper: $errorWrapper
)
}
}
if let operation = manager.activeFileOperation {
FileOperationHUD(operation: operation)
}
if manager.connectionState != .ready {
ConnectionOverlay(
state: manager.connectionState,
statusMessage: manager.statusMessage,
retryAction: manager.forceRescan
)
}
}
.onAppear {
if manager.currentDirectory != "/" {
manager.changeDirectory(to: "/")
}
navigationPath = stackForPath(manager.currentDirectory)
}
.onChange(of: manager.currentDirectory) { newValue in
let desired = stackForPath(newValue)
if desired != navigationPath {
navigationPath = desired
}
}
.onChange(of: navigationPath) { newValue in
let target = newValue.last ?? "/"
if target != manager.currentDirectory {
manager.changeDirectory(to: target)
}
}
}
}
private struct DirectoryView: View {
let path: String
@Binding var navigationPath: [String]
@Binding var shareURL: URL?
@Binding var errorWrapper: ErrorWrapper?
@EnvironmentObject private var manager: TimeSyncManager
@State private var showingImporter = false @State private var showingImporter = false
@State private var showingNewFolderSheet = false @State private var showingNewFolderSheet = false
@State private var showingRenameSheet = false @State private var showingRenameSheet = false
@@ -105,8 +167,18 @@ private struct FileManagerTabView: View {
@State private var renameText = "" @State private var renameText = ""
@State private var renameTarget: TimeSyncManager.RemoteFileEntry? @State private var renameTarget: TimeSyncManager.RemoteFileEntry?
private var pathComponents: [String] { private var pathSegments: [(name: String, fullPath: String)] {
manager.currentDirectory.split(separator: "/").map(String.init) var segments: [(String, String)] = []
var current = ""
for component in path.split(separator: "/").map(String.init) {
current += "/" + component
segments.append((component, current))
}
return segments
}
private var displayTitle: String {
pathSegments.last?.name ?? "Files"
} }
var body: some View { var body: some View {
@@ -117,18 +189,16 @@ private struct FileManagerTabView: View {
.font(.headline) .font(.headline)
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 4) { HStack(spacing: 4) {
Button(action: { manager.changeDirectory(to: "/") }) { Button(action: { navigationPath = [] }) {
Text("/") Text("/")
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
ForEach(pathComponents.indices, id: \.self) { index in ForEach(Array(pathSegments.enumerated()), id: \.element.fullPath) { index, segment in
let component = pathComponents[index]
Button(action: { Button(action: {
let path = "/" + pathComponents.prefix(index + 1).joined(separator: "/") navigationPath = Array(pathSegments.prefix(index + 1).map(\.fullPath))
manager.changeDirectory(to: path)
}) { }) {
Text(component) Text(segment.name)
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
} }
@@ -139,27 +209,13 @@ private struct FileManagerTabView: View {
List { List {
ForEach(manager.directoryEntries) { entry in ForEach(manager.directoryEntries) { entry in
FileRow(entry: entry) if entry.isDirectory {
.contentShape(Rectangle()) NavigationLink(value: entry.path) {
.onTapGesture { FileRow(entry: entry)
if entry.isDirectory {
manager.enter(directory: entry)
}
} }
.contextMenu { .contextMenu {
if entry.isDirectory { Button("Open") {
Button("Open", action: { manager.enter(directory: entry) }) navigationPath = stackForPath(entry.path)
} else {
Button("Download") {
manager.download(entry: entry) { result in
switch result {
case .success(let url):
shareURL = url
case .failure(let error):
errorWrapper = ErrorWrapper(message: error.localizedDescription)
}
}
}
} }
Button("Rename") { Button("Rename") {
@@ -174,32 +230,56 @@ private struct FileManagerTabView: View {
Text("Delete") Text("Delete")
} }
} }
} else {
FileRow(entry: entry)
.contextMenu {
Button("Download") {
manager.download(entry: entry) { result in
switch result {
case .success(let url):
shareURL = url
case .failure(let error):
errorWrapper = ErrorWrapper(message: error.localizedDescription)
}
}
}
Button("Rename") {
renameTarget = entry
renameText = entry.name
showingRenameSheet = true
}
Button(role: .destructive) {
manager.delete(entry: entry)
} label: {
Text("Delete")
}
}
}
} }
} }
.listStyle(.plain) .listStyle(.plain)
HStack { HStack(spacing: 12) {
Button { Menu {
showingImporter = true Button {
} label: { showingImporter = true
Label("Upload", systemImage: "square.and.arrow.up") } label: {
} Label("Upload File", systemImage: "square.and.arrow.up")
.buttonStyle(.bordered) }
Button { Button {
showingNewFolderSheet = true showingNewFolderSheet = true
newFolderName = "" newFolderName = ""
} label: {
Label("New Folder", systemImage: "folder.badge.plus")
}
} label: { } label: {
Label("New Folder", systemImage: "folder.badge.plus") Label("Actions", systemImage: "ellipsis.circle")
}
.buttonStyle(.bordered)
Button {
manager.navigateUp()
} label: {
Label("Up", systemImage: "arrow.up")
} }
.buttonStyle(.bordered) .buttonStyle(.bordered)
.controlSize(.large)
Spacer() Spacer()
@@ -208,21 +288,18 @@ private struct FileManagerTabView: View {
} label: { } label: {
Label("Refresh", systemImage: "arrow.clockwise") Label("Refresh", systemImage: "arrow.clockwise")
} }
.buttonStyle(.bordered) .buttonStyle(.borderedProminent)
.controlSize(.large)
} }
.padding([.horizontal, .bottom]) .padding([.horizontal, .bottom])
} }
if let operation = manager.activeFileOperation { }
FileOperationHUD(operation: operation) .navigationTitle(displayTitle)
} .navigationBarTitleDisplayMode(.inline)
.onAppear {
if manager.connectionState != .ready { if manager.currentDirectory != path {
ConnectionOverlay( manager.changeDirectory(to: path)
state: manager.connectionState,
statusMessage: manager.statusMessage,
retryAction: manager.forceRescan
)
} }
} }
.fileImporter( .fileImporter(
@@ -297,6 +374,17 @@ private struct FileManagerTabView: View {
} }
} }
private func stackForPath(_ path: String) -> [String] {
guard path != "/" else { return [] }
var stack: [String] = []
var current = ""
for component in path.split(separator: "/").map(String.init) {
current += "/" + component
stack.append(current)
}
return stack
}
private struct FileRow: View { private struct FileRow: View {
let entry: TimeSyncManager.RemoteFileEntry let entry: TimeSyncManager.RemoteFileEntry

View File

@@ -191,9 +191,11 @@ final class TimeSyncManager: NSObject, ObservableObject {
payload.append(isDst) payload.append(isDst)
payload.append(UInt8(0)) // Reserved byte payload.append(UInt8(0)) // Reserved byte
connectionState = .ready
statusMessage = "Sending current time…"
peripheral.writeValue(payload, for: characteristic, type: .withResponse) peripheral.writeValue(payload, for: characteristic, type: .withResponse)
lastSyncDate = now
connectionState = .ready
let timeString = DateFormatter.localizedString(from: now, dateStyle: .none, timeStyle: .medium)
statusMessage = "Time synced at \(timeString)."
} }
// MARK: - File operations exposed to UI // MARK: - File operations exposed to UI