import Libbox import Library import SwiftUI #if os(iOS) import FileProvider import UIKit #elseif os(macOS) import ServiceManagement #endif @MainActor public struct CoreView: View { @Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var environments: ExtensionEnvironments @State private var isLoading = true @State private var alert: AlertState? @State private var disableDeprecatedWarnings = false @State private var version = "" @State private var dataSize: String? @State private var dataSizeLoaded = false #if os(macOS) @State private var helperUnavailable = false #endif public init() {} public var body: some View { Group { if isLoading { ProgressView().onAppear { Task { await loadSettings() } } } else { FormView { FormTextItem("Version", version) if let dataSize { FormTextItem("Data Size", dataSize) } else if !dataSizeLoaded { HStack { Text("Data Size") Spacer() ProgressView() } } else { #if os(macOS) HStack { Text("Data Size") Spacer() Text("Unavailable") .foregroundStyle(.red) .onTapGesture { alert = helperRequiredAlert() } } #endif } if Variant.isBeta { FormToggle("Disable Deprecated Warnings", "Do not show warnings about usages of deprecated features.", $disableDeprecatedWarnings, header: "Beta Settings") { newValue in await SharedPreferences.disableDeprecatedWarnings.set(newValue) } } Section("Working Directory") { #if os(macOS) if !Variant.useSystemExtension { FormButton { NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: FilePath.workingDirectory.relativePath) } label: { Label("Open", systemImage: "macwindow.and.cursorarrow") } } #elseif os(iOS) if #available(iOS 16.0, *) { FormButton { Task { await openInFilesApp() } } label: { Label("Browse", systemImage: "folder.fill") } } #endif FormButton(role: .destructive) { Task { await confirmDestroyWorkingDirectory() } } label: { Label("Destroy", systemImage: "trash.fill") } .foregroundColor(.red) } } } } .navigationTitle("Core") .alert($alert) .onAppear { guard !isLoading else { return } Task { await refreshWorkingDirectorySize() } } .onChangeCompat(of: scenePhase) { newValue in guard newValue == .active, !isLoading else { return } Task { await refreshWorkingDirectorySize() } } #if os(iOS) .navigationBarTitleDisplayMode(.inline) #endif } private nonisolated func loadSettings() async { if Variant.screenshotMode { await MainActor.run { version = "" dataSize = LibboxFormatBytes(1000 * 1000 * 10) isLoading = false } } else { await MainActor.run { version = LibboxVersion() isLoading = false } await loadSettingsBackground() } } private nonisolated func loadSettingsBackground() async { let disableDeprecatedWarnings = await SharedPreferences.disableDeprecatedWarnings.get() await MainActor.run { self.disableDeprecatedWarnings = disableDeprecatedWarnings } await refreshWorkingDirectorySize() } private nonisolated func refreshWorkingDirectorySize() async { guard !Variant.screenshotMode else { return } #if os(macOS) let helperUnavailable = Variant.useSystemExtension && HelperServiceManager.rootHelperStatus != .enabled #endif let workingDirectory = FilePath.workingDirectory let dataSize: String? = await BlockingIO.run { #if os(macOS) if Variant.useSystemExtension { if helperUnavailable { return nil } guard let size = try? RootHelperClient.shared.getWorkingDirectorySize() else { return nil } return LibboxFormatBytes(size) } else { return (try? workingDirectory.formattedSize()) ?? "Unknown" } #else return (try? workingDirectory.formattedSize()) ?? "Unknown" #endif } await MainActor.run { #if os(macOS) self.helperUnavailable = helperUnavailable #endif self.dataSize = dataSize dataSizeLoaded = true } } private func confirmDestroyWorkingDirectory() async { #if os(macOS) if helperUnavailable { alert = helperRequiredAlert() return } #endif if environments.extensionProfile?.status.isConnected == true { alert = AlertState( title: String(localized: "Service is Running"), message: String(localized: "The service must be stopped before destroying the working directory."), primaryButton: .destructive(String(localized: "Stop Service and Continue")) { [self] in Task { await stopServiceAndDestroy() } }, secondaryButton: .cancel() ) } else { await destroyWorkingDirectory() } } private func stopServiceAndDestroy() async { do { try await environments.extensionProfile!.stop() await destroyWorkingDirectory() } catch { alert = AlertState(action: "stop service before destroying working directory", error: error) } } private func destroyWorkingDirectory() async { do { let workingDirectory = FilePath.workingDirectory #if os(macOS) if Variant.useSystemExtension { try await BlockingIO.run { try RootHelperClient.shared.cleanWorkingDirectory() } } else { try await BlockingIO.run { try Self.clearWorkingDirectoryContents(at: workingDirectory) } } #else try await BlockingIO.run { try Self.clearWorkingDirectoryContents(at: workingDirectory) } #if os(iOS) if #available(iOS 16.0, *) { await notifyFileProviderWorkingDirectoryChanged() } #endif #endif isLoading = true } catch { alert = AlertState(action: "destroy working directory", error: error) } } private nonisolated static func clearWorkingDirectoryContents(at url: URL) throws { guard FileManager.default.fileExists(atPath: url.path) else { return } let contents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil) for item in contents { try FileManager.default.removeItem(at: item) } } #if os(macOS) private func helperRequiredAlert() -> AlertState { AlertState( title: String(localized: "Helper Service Required"), message: String(localized: "Managing working directory requires Helper Service."), primaryButton: .default(String(localized: "App Settings")) { NotificationCenter.default.post(name: .navigateToSettingsPage, object: SettingsPage.app) }, secondaryButton: .cancel(String(localized: "Ok")) ) } #endif #if os(iOS) @available(iOS 16.0, *) private nonisolated func fileProviderManager() async throws -> NSFileProviderManager { let domains = try await NSFileProviderManager.domains() guard let domain = domains.first(where: { $0.identifier.rawValue == AppConfiguration.fileProviderDomainID }) else { throw NSError(domain: "CoreView", code: 0, userInfo: [NSLocalizedDescriptionKey: "File provider domain not found"]) } guard let manager = NSFileProviderManager(for: domain) else { throw NSError(domain: "CoreView", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to get file provider manager"]) } return manager } @available(iOS 16.0, *) private nonisolated func notifyFileProviderWorkingDirectoryChanged() async { do { let manager = try await fileProviderManager() try await manager.signalEnumerator(for: .rootContainer) try await manager.signalEnumerator(for: .workingSet) } catch { await MainActor.run { alert = AlertState(action: "notify Files app about working directory changes", error: error) } } } @available(iOS 16.0, *) private nonisolated func openInFilesApp() async { do { let manager = try await fileProviderManager() try await manager.signalEnumerator(for: .workingSet) let url = try await manager.getUserVisibleURL(for: .rootContainer) guard let sharedURL = URL(string: "shareddocuments://\(url.path)") else { throw NSError(domain: "CoreView", code: 0, userInfo: [NSLocalizedDescriptionKey: "Failed to create shared documents URL"]) } await MainActor.run { UIApplication.shared.open(sharedURL) } } catch { await MainActor.run { alert = AlertState(action: "open working directory in Files", error: error) } } } #endif } private extension URL { func formattedSize() throws -> String? { guard let enumerator = FileManager.default.enumerator( at: self, includingPropertiesForKeys: [.totalFileAllocatedSizeKey] ) else { return nil } var size = 0 while let url = enumerator.nextObject() as? URL { size += try url.resourceValues(forKeys: [.totalFileAllocatedSizeKey]).totalFileAllocatedSize ?? 0 } let formatter = ByteCountFormatter() formatter.countStyle = .file guard let byteCount = formatter.string(for: size) else { return nil } return byteCount } }