UTM/Platform/iOS/VMDrivesSettingsView.swift

164 lines
6.2 KiB
Swift

//
// Copyright © 2020 osy. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
// MARK: - Drives list
struct VMDrivesSettingsView: View {
@ObservedObject var config: UTMQemuConfiguration
@Binding var isCreateDriveShown: Bool
@Binding var isImportDriveShown: Bool
@State private var attemptDelete: IndexSet?
@EnvironmentObject private var data: UTMData
var body: some View {
ForEach($config.drives) { $drive in
NavigationLink(
destination: VMConfigDriveDetailsView(config: $drive, requestDriveDelete: .constant(nil)), label: {
Label(title: { labelTitle(for: drive) }, icon: { Image(systemName: "externaldrive") })
})
}.onDelete { offsets in
attemptDelete = offsets
}
.onMove(perform: moveDrives)
.nonbrokenSheet(isPresented: $isCreateDriveShown) {
CreateDrive(newDrive: UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target), onDismiss: newDrive)
}
.globalFileImporter(isPresented: $isImportDriveShown, allowedContentTypes: [.item], onCompletion: importDrive)
.actionSheet(item: $attemptDelete) { offsets in
ActionSheet(title: Text("Confirm Delete"), message: Text("Are you sure you want to permanently delete this disk image?"), buttons: [.cancel(), .destructive(Text("Delete")) {
deleteDrives(offsets: offsets)
}])
}
}
private func labelTitle(for drive: UTMQemuConfigurationDrive) -> Text {
if drive.interface == .none && drive.imageName == QEMUPackageFileName.efiVariables.rawValue {
return Text("EFI Variables", comment: "VMDrivesSettingsView")
} else {
return Text("\(drive.interface.prettyValue) Drive", comment: "VMDrivesSettingsView")
}
}
private func newDrive(drive: UTMQemuConfigurationDrive) {
config.drives.append(drive)
}
private func deleteDrives(offsets: IndexSet) {
config.drives.remove(atOffsets: offsets)
}
private func moveDrives(source: IndexSet, destination: Int) {
config.drives.move(fromOffsets: source, toOffset: destination)
}
private func importDrive(result: Result<URL, Error>) {
data.busyWorkAsync {
switch result {
case .success(let url):
await MainActor.run {
var drive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: true)
drive.imageURL = url
config.drives.append(drive)
}
break
case .failure(let err):
throw err
}
}
}
}
// MARK: - Create Drive
private extension View {
/// A sheet that isn't broken on older versions.
///
/// On iOS 14 and older, .sheet() breaks the table layout for some reason.
/// This workarounds it by putting the sheet inside an overlay which does
/// not affect displaying the sheet at all.
/// - Parameters:
/// - isPresented: same as .sheet()
/// - onDismiss: same as .sheet()
/// - content: same as .sheet()
/// - Returns: same as .sheet()
@ViewBuilder func nonbrokenSheet<Content>(isPresented: Binding<Bool>, onDismiss: (() -> Void)? = nil, @ViewBuilder content: @escaping () -> Content) -> some View where Content : View {
if #available(iOS 15, macOS 12, *) {
self.sheet(isPresented: isPresented, onDismiss: onDismiss, content: content)
} else {
self.overlay(EmptyView().sheet(isPresented: isPresented, onDismiss: onDismiss, content: content))
}
}
}
private struct CreateDrive: View {
@State var newDrive: UTMQemuConfigurationDrive
let onDismiss: (UTMQemuConfigurationDrive) -> Void
@Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>
var body: some View {
NavigationView {
VMConfigDriveCreateView(config: $newDrive)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", action: cancel)
}
ToolbarItem(placement: .confirmationAction) {
Button("Done", action: done)
}
}
}.navigationViewStyle(.stack)
}
private func cancel() {
presentationMode.wrappedValue.dismiss()
}
private func done() {
presentationMode.wrappedValue.dismiss()
onDismiss(newDrive)
}
}
// MARK: - Preview
struct VMConfigDrivesView_Previews: PreviewProvider {
@StateObject static private var config = UTMQemuConfiguration()
static var previews: some View {
Group {
VMDrivesSettingsView(config: config, isCreateDriveShown: .constant(false), isImportDriveShown: .constant(false))
CreateDrive(newDrive: UTMQemuConfigurationDrive()) { _ in
}
}.onAppear {
if config.drives.count == 0 {
var drive = UTMQemuConfigurationDrive(forArchitecture: .x86_64, target: QEMUTarget_x86_64.pc)
drive.imageName = "test.img"
drive.imageType = .disk
drive.interface = .ide
config.drives.append(drive)
drive = UTMQemuConfigurationDrive(forArchitecture: .x86_64, target: QEMUTarget_x86_64.pc)
drive.imageName = "bios.bin"
drive.imageType = .bios
drive.interface = .none
config.drives.append(drive)
}
}
}
}