mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
app fixes
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user