diff --git a/Build.xcconfig b/Build.xcconfig index 956e42e25..945dc0444 100644 --- a/Build.xcconfig +++ b/Build.xcconfig @@ -17,8 +17,8 @@ // Configuration settings file format documentation can be found at: // https://help.apple.com/xcode/#/dev745c5c974 -MARKETING_VERSION = 4.5.1 -CURRENT_PROJECT_VERSION = 96 +MARKETING_VERSION = 4.5.3 +CURRENT_PROJECT_VERSION = 99 // Codesigning settings defined optionally, see Documentation/iOSDevelopment.md #include? "CodeSigning.xcconfig" diff --git a/Configuration/UTMAppleConfiguration.swift b/Configuration/UTMAppleConfiguration.swift index 40fa78f3f..08c8c206a 100644 --- a/Configuration/UTMAppleConfiguration.swift +++ b/Configuration/UTMAppleConfiguration.swift @@ -265,11 +265,13 @@ extension UTMAppleConfiguration { } if !ignoringDrives { vzconfig.storageDevices = try drives.compactMap { drive in - guard let attachment = try drive.vzDiskImage() else { + guard let attachment = try drive.vzDiskImage(useFsWorkAround: system.boot.operatingSystem == .linux) else { return nil } if #available(macOS 13, *), drive.isExternal { return VZUSBMassStorageDeviceConfiguration(attachment: attachment) + } else if #available(macOS 14, *), drive.isNvme, system.boot.operatingSystem == .linux { + return VZNVMExpressControllerDeviceConfiguration(attachment: attachment) } else { return VZVirtioBlockDeviceConfiguration(attachment: attachment) } diff --git a/Configuration/UTMAppleConfigurationDrive.swift b/Configuration/UTMAppleConfigurationDrive.swift index ef53dbbf6..e14488c35 100644 --- a/Configuration/UTMAppleConfigurationDrive.swift +++ b/Configuration/UTMAppleConfigurationDrive.swift @@ -25,6 +25,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { var sizeMib: Int = 0 var isReadOnly: Bool var isExternal: Bool + var isNvme: Bool var imageURL: URL? var imageName: String? @@ -36,6 +37,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { private enum CodingKeys: String, CodingKey { case isReadOnly = "ReadOnly" + case isNvme = "Nvme" case imageName = "ImageName" case bookmark = "Bookmark" // legacy only case identifier = "Identifier" @@ -55,12 +57,14 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { sizeMib = newSize isReadOnly = false isExternal = false + isNvme = false } - init(existingURL url: URL?, isExternal: Bool = false) { + init(existingURL url: URL?, isExternal: Bool = false, isNvme: Bool = false) { self.imageURL = url self.isReadOnly = isExternal self.isExternal = isExternal + self.isNvme = isNvme } init(from decoder: Decoder) throws { @@ -83,6 +87,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { isExternal = true } isReadOnly = try container.decodeIfPresent(Bool.self, forKey: .isReadOnly) ?? isExternal + isNvme = try container.decodeIfPresent(Bool.self, forKey: .isNvme) ?? false id = try container.decode(String.self, forKey: .identifier) } @@ -92,12 +97,18 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { try container.encodeIfPresent(imageName, forKey: .imageName) } try container.encode(isReadOnly, forKey: .isReadOnly) + try container.encode(isNvme, forKey: .isNvme) try container.encode(id, forKey: .identifier) } - func vzDiskImage() throws -> VZDiskImageStorageDeviceAttachment? { + func vzDiskImage(useFsWorkAround: Bool = false) throws -> VZDiskImageStorageDeviceAttachment? { if let imageURL = imageURL { - return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly) + // Use cached caching mode for virtio drive to prevent fs corruption on linux when possible + if #available(macOS 12.0, *), !isNvme, useFsWorkAround { + return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly, cachingMode: .cached, synchronizationMode: .full) + } else { + return try VZDiskImageStorageDeviceAttachment(url: imageURL, readOnly: isReadOnly) + } } else { return nil } @@ -107,6 +118,7 @@ struct UTMAppleConfigurationDrive: UTMConfigurationDrive { imageName?.hash(into: &hasher) sizeMib.hash(into: &hasher) isReadOnly.hash(into: &hasher) + isNvme.hash(into: &hasher) isExternal.hash(into: &hasher) id.hash(into: &hasher) } @@ -127,6 +139,7 @@ extension UTMAppleConfigurationDrive { sizeMib = oldDrive.sizeMib isReadOnly = oldDrive.isReadOnly isExternal = oldDrive.isExternal + isNvme = false imageURL = oldDrive.imageURL } } diff --git a/Configuration/UTMQemuConfiguration+Arguments.swift b/Configuration/UTMQemuConfiguration+Arguments.swift index 0857e3dff..b88110d0f 100644 --- a/Configuration/UTMQemuConfiguration+Arguments.swift +++ b/Configuration/UTMQemuConfiguration+Arguments.swift @@ -472,8 +472,9 @@ import Virtualization // for getting network interfaces "if=pflash" "format=raw" "unit=0" - "file=" + "file.filename=" bios + "file.locking=off" "readonly=on" f() f("-drive") @@ -733,13 +734,16 @@ import Virtualization // for getting network interfaces } "id=drive\(drive.id)" if let imageURL = drive.imageURL { - "file=" + "file.filename=" imageURL } else if !isCd { - "file=" + "file.filename=" placeholderUrl } if drive.isReadOnly || isCd { + if drive.imageURL != nil { + "file.locking=off" + } "readonly=on" } else { "discard=unmap" diff --git a/Documentation/MacDevelopment.md b/Documentation/MacDevelopment.md index 9d923163d..801ad1b3e 100644 --- a/Documentation/MacDevelopment.md +++ b/Documentation/MacDevelopment.md @@ -16,7 +16,9 @@ git submodule update --init --recursive ## Dependencies -The easy way is to get the prebuilt dependences from [GitHub Actions][1]. Pick the latest release and download all of the `Sysroot-macos-*` artifacts. You need to be logged in to GitHub to download artifacts. If you only intend to run locally, it is alright to just download the sysroot for your architecture. +The easy way is to get the prebuilt dependences from [GitHub Actions][1]. Pick the latest release and download all of the `Sysroot-macos-*` artifacts. You need to be logged in to GitHub to download artifacts. If you only intend to run locally, it is alright to just download the sysroot for your architecture. After downloading the prebuilt artifacts of your choice, extract them to the root directory where you cloned the repository. + +To build UTM, make sure you have the latest version of Xcode installed. ### Building Dependencies (Advanced) @@ -58,7 +60,7 @@ If you are developing QEMU and wish to pass in a custom path to QEMU, you can us You can build UTM with the script: ```sh -./scripts/build_utm.sh -t TEAMID -p macos -a ARCH -o /path/to/output/directory +./scripts/build_utm.sh -t TEAMID -k macosx -s macos -a ARCH -o /path/to/output/directory ``` `ARCH` can be `x86_64` or `arm64` or `"arm64 x86_64"` (quotes are required) for a universal binary. The built artifact is an unsigned `.xcarchive` which you can use with the package tool (see below). diff --git a/Documentation/iOSDevelopment.md b/Documentation/iOSDevelopment.md index 8a22b9912..5bde558ef 100644 --- a/Documentation/iOSDevelopment.md +++ b/Documentation/iOSDevelopment.md @@ -15,12 +15,20 @@ Alternatively, run `git submodule update --init --recursive` after cloning if yo The easy way is to get the prebuilt dependences from [GitHub Actions][1]. Pick the latest release and download the `Sysroot-*` artifact for the targets you wish to develop on. You need to be logged in to GitHub to download artifacts. -| | Intel | Apple Silicon | -|--------------|----------------------------|---------------------------| -| iOS | N/A | `ios-arm64` | -| iOS SE | N/A | `ios-tci-arm64` | -| Simulator | `ios_simulator-x86_64` | `ios_simulator-arm64` | -| Simulator SE | `ios_simulator-tci-x86_64` | `ios_simulator-tci-arm64` | +| | Intel | Apple Silicon | +|-----------------------|----------------------------|--------------------------------| +| iOS | N/A | `ios-arm64` | +| iOS SE | N/A | `ios-tci-arm64` | +| iOS Simulator | `ios_simulator-x86_64` | `ios_simulator-arm64` | +| iOS Simulator SE | `ios_simulator-tci-x86_64` | `ios_simulator-tci-arm64` | +| visionOS | N/A | `visionos-arm64` | +| visionOS SE | N/A | `visionos-tci-arm64` | +| visionOS Simulator | N/A | `visionos_simulator-arm64` | +| visionOS Simulator SE | N/A | `visionos_simulator-tci-arm64` | + +After downloading the prebuilt artifacts of your choice, extract them to the root directory where you cloned the repository. + +To build UTM, make sure you have the latest version of Xcode installed. ### Building Dependencies (Advanced) @@ -39,13 +47,13 @@ If you want to build the dependencies yourself, it is highly recommended that yo ### Command Line -You can build UTM with the script: +You can build UTM for iOS with the script (run `./scripts/build_utm.sh` for all options): ``` -./scripts/build_utm.sh -p ios -a arm64 -o /path/to/output/directory +./scripts/build_utm.sh -k iphoneos -s iOS -a arm64 -o /path/to/output/directory ``` -The built artifact is an unsigned `.xcarchive` which you can use with the package tool (see below). Replace `ios` with `ios-tci` to build UTM SE. +The built artifact is an unsigned `.xcarchive` which you can use with the package tool (see below). Replace `iOS` with `iOS-SE` to build UTM SE. Replace `iphoneos` with `xros` to build for visionOS. ### Packaging diff --git a/Platform/Main.swift b/Platform/Main.swift index 23c448a4c..09dd6bfdb 100644 --- a/Platform/Main.swift +++ b/Platform/Main.swift @@ -15,6 +15,7 @@ // import Logging +import TipKit let logger = Logger(label: "com.utmapp.UTM") { label in var utmLogger = UTMLoggingSwift(label: label) @@ -60,6 +61,10 @@ class Main { #if os(iOS) || os(visionOS) // register defaults registerDefaultsFromSettingsBundle() + // register tips + if #available(iOS 17, macOS 14, *) { + try? Tips.configure() + } #endif UTMApp.main() } diff --git a/Platform/Shared/BusyIndicator.swift b/Platform/Shared/BusyIndicator.swift index aef05d8e3..7a02d8af8 100644 --- a/Platform/Shared/BusyIndicator.swift +++ b/Platform/Shared/BusyIndicator.swift @@ -17,13 +17,38 @@ import SwiftUI struct BusyIndicator: View { + @Binding var progress: Float? + + init(progress: Binding = .constant(nil)) { + _progress = progress + } + var body: some View { - Spinner(size: .large) + progressView .frame(width: 100, height: 100, alignment: .center) .foregroundColor(.white) .background(Color.gray.opacity(0.5)) .clipShape(RoundedRectangle(cornerRadius: 25.0, style: .continuous)) } + + #if os(macOS) + @ViewBuilder + private var progressView: some View { + if let progress = progress { + ProgressView(value: progress) + .progressViewStyle(.circular) + .controlSize(.large) + } else { + Spinner(size: .large) + } + } + #else + // TODO: implement progress spinner for iOS + @ViewBuilder + private var progressView: some View { + Spinner(size: .large) + } + #endif } struct BusyIndicator_Previews: PreviewProvider { diff --git a/Platform/Shared/BusyOverlay.swift b/Platform/Shared/BusyOverlay.swift index ce6febdc7..17fc29129 100644 --- a/Platform/Shared/BusyOverlay.swift +++ b/Platform/Shared/BusyOverlay.swift @@ -22,7 +22,7 @@ struct BusyOverlay: View { var body: some View { Group { if data.busy { - BusyIndicator() + BusyIndicator(progress: $data.busyProgress) } else { EmptyView() } diff --git a/Platform/Shared/ContentView.swift b/Platform/Shared/ContentView.swift index eb19f3362..d0cca7edc 100644 --- a/Platform/Shared/ContentView.swift +++ b/Platform/Shared/ContentView.swift @@ -19,6 +19,7 @@ import UniformTypeIdentifiers #if os(iOS) import IQKeyboardManagerSwift #endif +import TipKit // on visionOS, there is no text to show more than UTM #if WITH_QEMU_TCI && !os(visionOS) @@ -48,6 +49,9 @@ struct ContentView: View { .disabled(data.busy && !data.showNewVMSheet && !data.showSettingsModal) .sheet(isPresented: $releaseHelper.isReleaseNotesShown, onDismiss: { releaseHelper.closeReleaseNotes() + if #available(iOS 17, macOS 14, *) { + UTMTipCreateVM.isVMListEmpty = data.virtualMachines.count == 0 + } }, content: { VMReleaseNotesView(helper: releaseHelper).padding() }) @@ -80,15 +84,19 @@ struct ContentView: View { .onAppear { Task { await data.listRefresh() + await releaseHelper.fetchReleaseNotes() + if #available(iOS 17, macOS 14, *) { + if !releaseHelper.isReleaseNotesShown { + UTMTipCreateVM.isVMListEmpty = data.virtualMachines.count == 0 + UTMTipDonate.timesLaunched += 1 + } + } #if os(macOS) if isServerAutostart { await data.remoteServer.start() } #endif } - Task { - await releaseHelper.fetchReleaseNotes() - } #if os(macOS) NSWindow.allowsAutomaticWindowTabbing = false #else @@ -121,6 +129,17 @@ struct ContentView: View { #endif #endif } + #if WITH_SERVER + .onChange(of: isServerAutostart) { newValue in + if newValue { + Task { + if isServerAutostart && !data.remoteServer.state.isServerActive { + await data.remoteServer.start() + } + } + } + } + #endif } private func handleURL(url: URL) { diff --git a/Platform/Shared/RAMSlider.swift b/Platform/Shared/RAMSlider.swift index 66e0b5458..2b4051355 100644 --- a/Platform/Shared/RAMSlider.swift +++ b/Platform/Shared/RAMSlider.swift @@ -60,7 +60,7 @@ struct RAMSlider: View { } NumberTextField("", number: $systemMemory, prompt: "Size", onEditingChanged: validateMemorySize) .frame(width: 80) - Text("MB") + Text("MiB") } }.frame(height: 30) } diff --git a/Platform/Shared/SizeTextField.swift b/Platform/Shared/SizeTextField.swift index 3997dc6de..1c51c99a1 100644 --- a/Platform/Shared/SizeTextField.swift +++ b/Platform/Shared/SizeTextField.swift @@ -40,9 +40,9 @@ struct SizeTextField: View { Button(action: { isGiB.toggle() }, label: { Group { if isGiB { - Text("GB") + Text("GiB") } else { - Text("MB") + Text("MiB") } }.foregroundColor(.blue) }).buttonStyle(.plain) diff --git a/Platform/Shared/UTMTips.swift b/Platform/Shared/UTMTips.swift new file mode 100644 index 000000000..c125a806c --- /dev/null +++ b/Platform/Shared/UTMTips.swift @@ -0,0 +1,82 @@ +// +// Copyright © 2024 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 TipKit + +@available(iOS 17, macOS 14, *) +struct UTMTipDonate: Tip { + @Parameter + static var timesLaunched: Int = 0 + + var title: Text { + Text("Support UTM") + } + + var message: Text? { + Text("Enjoying the app? Consider making a donation to support development.") + } + + var actions: [Action] { + Action(id: "donate", title: "Donate") + Action(id: "no-thanks", title: "No Thanks") + } + + var rules: [Rule] { + #Rule(Self.$timesLaunched) { + $0 > 3 + } + } +} + +@available(iOS 17, macOS 14, *) +struct UTMTipHideToolbar: Tip { + @Parameter + static var didHideToolbar: Bool = true + + var title: Text { + Text("Tap to hide/show toolbar") + } + + var message: Text? { + Text("When the toolbar is hidden, the icon will disappear after a few seconds. To show the icon again, tap anywhere on the screen.") + } + + var rules: [Rule] { + #Rule(Self.$didHideToolbar) { + !$0 + } + } +} + +@available(iOS 17, macOS 14, *) +struct UTMTipCreateVM: Tip { + @Parameter(.transient) + static var isVMListEmpty: Bool = false + + var title: Text { + Text("Start Here") + } + + var message: Text? { + Text("Create a new virtual machine or import an existing one.") + } + + var rules: [Rule] { + #Rule(Self.$isVMListEmpty) { + $0 + } + } +} diff --git a/Platform/Shared/UTMUnavailableVMView.swift b/Platform/Shared/UTMUnavailableVMView.swift index 8e687345d..6252417e2 100644 --- a/Platform/Shared/UTMUnavailableVMView.swift +++ b/Platform/Shared/UTMUnavailableVMView.swift @@ -73,6 +73,8 @@ fileprivate struct WrappedVMDetailsView: View { } #if os(macOS) .frame(width: 230) + #else + .padding() #endif } } @@ -92,6 +94,8 @@ fileprivate struct UnsupportedVMDetailsView: View { } #if os(macOS) .frame(width: 230) + #else + .padding() #endif } } diff --git a/Platform/Shared/VMConfigInfoView.swift b/Platform/Shared/VMConfigInfoView.swift index 1c574bfa8..6721b8359 100644 --- a/Platform/Shared/VMConfigInfoView.swift +++ b/Platform/Shared/VMConfigInfoView.swift @@ -123,16 +123,23 @@ struct VMConfigInfoView: View { switch iconStyle { case .custom: #if os(macOS) - Button(action: { imageSelectVisible.toggle() }, label: { + VStack { IconPreview(url: config.iconURL) - }).fileImporter(isPresented: $imageSelectVisible, allowedContentTypes: [.image]) { result in - switch result { - case .success(let url): - imageCustomSelected(url: url) - case .failure: - break + .onTapGesture { + imageSelectVisible.toggle() + } + Button(action: { imageSelectVisible.toggle() }, label: { + Text("Choose") + }).fileImporter(isPresented: $imageSelectVisible, allowedContentTypes: [.image]) { result in + switch result { + case .success(let url): + imageCustomSelected(url: url) + case .failure: + break + } } - }.buttonStyle(.plain) + } + .frame(width: 90) #else Button(action: { imageSelectVisible.toggle() }, label: { IconPreview(url: config.iconURL) @@ -141,18 +148,34 @@ struct VMConfigInfoView: View { }.buttonStyle(.plain) #endif case .operatingSystem: - Button(action: { imageSelectVisible.toggle() }, label: { + #if os(macOS) + VStack { IconPreview(url: config.iconURL) - }).popover(isPresented: $imageSelectVisible, arrowEdge: .bottom) { - IconSelect(onIconSelected: imageSelected) - }.buttonStyle(.plain) + .onTapGesture { + imageSelectVisible.toggle() + } + Button(action: { imageSelectVisible.toggle() }, label: { + Text("Choose") + }).popover(isPresented: $imageSelectVisible, arrowEdge: .bottom) { + IconSelect(current: config.iconURL, onIconSelected: imageSelected) + } + } + .frame(width: 90) + #else + IconSelect(current: config.iconURL, onIconSelected: imageSelected) + #endif default: #if os(macOS) - Image(systemName: "desktopcomputer") - .resizable() - .frame(width: 30.0, height: 30.0) - .padding() - .foregroundColor(Color(NSColor.disabledControlTextColor)) + VStack { + Image(systemName: "desktopcomputer") + .resizable() + .frame(width: 30.0, height: 30.0) + .foregroundColor(Color(NSColor.disabledControlTextColor)) + Button {} label: { + Text("Choose") + }.disabled(true) + } + .frame(width: 90) #else EmptyView() #endif @@ -190,7 +213,6 @@ private struct IconPreview: View { Spacer() #endif Logo(logo: PlatformImage(contentsOfURL: url)) - .padding() #if !os(macOS) Spacer() #endif @@ -198,9 +220,16 @@ private struct IconPreview: View { } } +#if os(macOS) +let iconGridSize: CGFloat = 80 +#else +let iconGridSize: CGFloat = 100 +#endif + private struct IconSelect: View { + let current: URL? let onIconSelected: (URL) -> Void - private let gridLayout = [GridItem(.adaptive(minimum: 60))] + private let gridLayout = [GridItem(.adaptive(minimum: iconGridSize))] private var icons: [URL] { let paths = Bundle.main.paths(forResourcesOfType: "png", inDirectory: "Icons") let urls = paths.map({ URL(fileURLWithPath: $0) }) @@ -210,6 +239,7 @@ private struct IconSelect: View { } #if os(macOS) + typealias PlatformImage = NSImage #else typealias PlatformImage = UIImage @@ -218,50 +248,53 @@ private struct IconSelect: View { struct IconSelectModifier: ViewModifier { @Environment(\.presentationMode) private var presentationMode: Binding - #if os(macOS) - let isPhone: Bool = false - #else - var isPhone: Bool { - UIDevice.current.userInterfaceIdiom == .phone - } - #endif - func body(content: Content) -> some View { - if isPhone { - return AnyView( - VStack { - HStack { - Spacer() - Button(action: { presentationMode.wrappedValue.dismiss() }, label: { - Text("Cancel") - }).padding() - } - ScrollView { - content.padding(.bottom) - } - } - ) - } else { - return AnyView( - ScrollView { - content.padding([.top, .bottom]) - }.frame(width: 400, height: 400) - ) - } + #if os(macOS) + return AnyView( + ScrollView { + content.padding(16) + }.frame(width: 480, height: 400) + ) + #else + return AnyView(content) + #endif } } var body: some View { - LazyVGrid(columns: gridLayout, spacing: 30) { + LazyVGrid(columns: gridLayout, spacing: 0) { ForEach(icons, id: \.self) { icon in Button(action: { onIconSelected(icon) }, label: { - Logo(logo: PlatformImage(contentsOfURL: icon)) + VStack(alignment: .center) { + Logo(logo: PlatformImage(contentsOfURL: icon)) + Text(iconToTitle(icon)) + .lineLimit(2, optionalReservesSpace: true) + .font(.footnote) + .multilineTextAlignment(.center) + } + .padding(8) + .frame(width: iconGridSize, height: iconGridSize) + .overlay( + RoundedRectangle(cornerRadius: 10) + .stroke(current == icon ? Color.accentColor : Color.clear, lineWidth: 2) + ) }).buttonStyle(.plain) } }.modifier(IconSelectModifier()) } } +private extension View { + @ViewBuilder + func lineLimit(_ limit: Int, optionalReservesSpace: Bool) -> some View { + if #available(macOS 13, iOS 16, *) { + self.lineLimit(limit, reservesSpace: optionalReservesSpace) + } else { + self.lineLimit(limit) + } + } +} + struct VMConfigInfoView_Previews: PreviewProvider { @State static private var config = UTMConfigurationInfo() @@ -271,9 +304,83 @@ struct VMConfigInfoView_Previews: PreviewProvider { #if os(macOS) .scrollable() #endif - IconSelect() { _ in + IconSelect(current: nil) { _ in } } } } + +private func iconToTitle(_ icon: URL?) -> LocalizedStringKey { + guard let fileName = icon?.deletingPathExtension().lastPathComponent else { + return "Custom" + } + return ICON_TITLE_MAP[fileName] ?? "Custom" +} + +private let ICON_TITLE_MAP: [String: LocalizedStringKey] = [ + "AIX": "AIX", + "IOS": "iOS", + "Windows7": "Windows 7", + "almalinux": "AlmaLinux", + "alpine": "Alpine", + "amigaos": "AmigaOS", + "android": "Android", + "apple-tv": "Apple TV", + "arch-linux": "Arch Linux", + "backtrack": "BackTrack", + "bada": "Bada", + "beos": "BeOS", + "centos": "CentOS", + "chrome-os": "Chrome OS", + "cyanogenmod": "CyanogenMod", + "debian": "Debian", + "elementary-os": "Elementary OS", + "fedora": "Fedora", + "firefox-os": "Firefox OS", + "freebsd": "FreeBSD", + "gentoo": "Gentoo", + "haiku-os": "Haiku OS", + "hp-ux": "HP-UX", + "kaios": "KaiOS", + "knoppix": "Knoppix", + "kubuntu": "Kubuntu", + "linux": "Linux", + "lubuntu": "Lubuntu", + "mac": "macOS", + "maemo": "Maemo", + "mandriva": "Mandriva", + "meego": "MeeGo", + "mint": "Linux Mint", + "netbsd": "NetBSD", + "nintendo": "Nintendo", + "nixos": "NixOS", + "openbsd": "OpenBSD", + "openwrt": "OpenWrt", + "os2": "OS/2", + "palmos": "Palm OS", + "playstation-portable": "PlayStation Portable", + "playstation": "PlayStation", + "pop-os": "Pop!_OS", + "red-hat": "Red Hat", + "remix-os": "Remix OS", + "risc-os": "RISC OS", + "sabayon": "Sabayon", + "sailfish-os": "Sailfish OS", + "slackware": "Slackware", + "solaris": "Solaris", + "suse": "openSUSE", + "syllable": "Syllable", + "symbian": "Symbian", + "threadx": "ThreadX", + "tizen": "Tizen", + "ubuntu": "Ubuntu", + "webos": "webOS", + "windows-11": "Windows 11", + "windows-9x": "Windows 9x", + "windows-xp": "Windows XP", + "windows": "Windows", + "xbox": "Xbox", + "xubuntu": "Xubuntu", + "yunos": "YunOS", +] diff --git a/Platform/Shared/VMConfigSystemView.swift b/Platform/Shared/VMConfigSystemView.swift index a2c1ce884..606f97b76 100644 --- a/Platform/Shared/VMConfigSystemView.swift +++ b/Platform/Shared/VMConfigSystemView.swift @@ -55,7 +55,7 @@ struct VMConfigSystemView: View { HStack { NumberTextField("", number: $config.jitCacheSize, prompt: "Default", onEditingChanged: validateMemorySize) .multilineTextAlignment(.trailing) - Text("MB") + Text("MiB") } } } diff --git a/Platform/Shared/VMNavigationListView.swift b/Platform/Shared/VMNavigationListView.swift index 5fee68f3f..acb50c7a2 100644 --- a/Platform/Shared/VMNavigationListView.swift +++ b/Platform/Shared/VMNavigationListView.swift @@ -15,6 +15,7 @@ // import SwiftUI +import TipKit struct VMNavigationListView: View { @EnvironmentObject private var data: UTMData @@ -104,7 +105,31 @@ private struct VMListModifier: ViewModifier { @EnvironmentObject private var data: UTMData @State private var settingsPresented = false @State private var sheetPresented = false - + @State private var donatePresented = false + + private let _donateTip: Any? + private let _createTip: Any? + + @available(iOS 17, macOS 14, *) + private var donateTip: UTMTipDonate { + _donateTip as! UTMTipDonate + } + + @available(iOS 17, macOS 14, *) + private var createTip: UTMTipCreateVM { + _createTip as! UTMTipCreateVM + } + + init() { + if #available(iOS 17, macOS 14, *) { + _donateTip = UTMTipDonate() + _createTip = UTMTipCreateVM() + } else { + _donateTip = nil + _createTip = nil + } + } + func body(content: Content) -> some View { content #if os(macOS) @@ -123,7 +148,40 @@ private struct VMListModifier: ViewModifier { #else #if !WITH_REMOTE // FIXME: implement remote feature ToolbarItem(placement: .navigationBarLeading) { - newButton + if #available(iOS 17, visionOS 99, *) { + Button { + createTip.invalidate(reason: .actionPerformed) + data.newVM() + } label: { + Image(systemName: "plus") // SwiftUI bug: tip won't show up if this is a label + }.help("Create a new VM") + .popoverTip(createTip, arrowEdge: .top) + } else { + newButton + } + } + #endif + #if !WITH_REMOTE + ToolbarItem(placement: .navigationBarLeading) { + if #available(iOS 17, visionOS 99, *) { + Button { + donateTip.invalidate(reason: .actionPerformed) + donatePresented.toggle() + } label: { + Image(systemName: "heart.fill") // SwiftUI bug: tip won't show up if this is a label + }.popoverTip(donateTip, arrowEdge: .top) { action in + donateTip.invalidate(reason: .actionPerformed) + if action.id == "donate" { + donatePresented.toggle() + } + } + } else { + Button { + donatePresented.toggle() + } label: { + Label("Donate", systemImage: "heart.fill") + } + } } #endif #if !os(visionOS) && !WITH_REMOTE @@ -147,23 +205,37 @@ private struct VMListModifier: ViewModifier { #if !WITH_REMOTE UTMSettingsView() #endif + } else if donatePresented { + #if !os(macOS) && !WITH_REMOTE + UTMDonateView() + #endif } } .onChange(of: data.showNewVMSheet) { newValue in if newValue { settingsPresented = false + donatePresented = false sheetPresented = true } } .onChange(of: settingsPresented) { newValue in if newValue { data.showNewVMSheet = false + donatePresented = false + sheetPresented = true + } + } + .onChange(of: donatePresented) { newValue in + if newValue { + data.showNewVMSheet = false + settingsPresented = false sheetPresented = true } } .onChange(of: sheetPresented) { newValue in if !newValue { settingsPresented = false + donatePresented = false data.showNewVMSheet = false } } @@ -174,6 +246,11 @@ private struct VMListModifier: ViewModifier { .sheet(isPresented: $data.showNewVMSheet) { VMWizardView() } + #if !os(macOS) && !WITH_REMOTE + .sheet(isPresented: $donatePresented) { + UTMDonateView() + } + #endif .onReceive(NSNotification.OpenVirtualMachine) { _ in data.showNewVMSheet = false } diff --git a/Platform/Shared/VMRemovableDrivesView.swift b/Platform/Shared/VMRemovableDrivesView.swift index 0682533bc..ee8750f55 100644 --- a/Platform/Shared/VMRemovableDrivesView.swift +++ b/Platform/Shared/VMRemovableDrivesView.swift @@ -15,6 +15,7 @@ // import SwiftUI +import UniformTypeIdentifiers struct VMRemovableDrivesView: View { @ObservedObject var vm: VMData @@ -25,7 +26,10 @@ struct VMRemovableDrivesView: View { /// Explanation see "SwiftUI FileImporter modal bug" in the `body` @State private var workaroundFileImporterBug: Bool = false @State private var currentDrive: UTMQemuConfigurationDrive? - + + private static let shareDirectoryUTType = UTType.folder + private static let diskImageUTType = UTType.data + private var qemuVM: (any UTMSpiceVirtualMachine)! { vm.wrapped as? any UTMSpiceVirtualMachine } @@ -73,8 +77,21 @@ struct VMRemovableDrivesView: View { } else { Button("Browse…", action: { shareDirectoryFileImportPresented.toggle() }) } - }.fileImporter(isPresented: $shareDirectoryFileImportPresented, allowedContentTypes: [.folder], onCompletion: selectShareDirectory) - .disabled(mode == .virtfs && vm.state != .stopped) + }.fileImporter(isPresented: $shareDirectoryFileImportPresented, allowedContentTypes: [Self.shareDirectoryUTType], onCompletion: selectShareDirectory) + .disabled(mode == .virtfs && vm.state != .stopped) + .onDrop(of: [Self.shareDirectoryUTType], isTargeted: nil) { providers in + guard let item = providers.first, item.hasItemConformingToTypeIdentifier(Self.shareDirectoryUTType.identifier) else { return false } + + item.loadItem(forTypeIdentifier: Self.shareDirectoryUTType.identifier) { url, error in + if let url = url as? URL { + selectShareDirectory(result: .success(url)) + } + if let error = error { + selectShareDirectory(result: .failure(error)) + } + } + return true + } } ForEach(config.drives.filter { $0.isExternal }) { drive in HStack { @@ -128,12 +145,25 @@ struct VMRemovableDrivesView: View { .lineLimit(1) .truncationMode(.tail) .foregroundColor(.secondary) - }.fileImporter(isPresented: $diskImageFileImportPresented, allowedContentTypes: [.data]) { result in + }.fileImporter(isPresented: $diskImageFileImportPresented, allowedContentTypes: [Self.diskImageUTType]) { result in if let currentDrive = self.currentDrive { selectRemovableImage(forDrive: currentDrive, result: result) self.currentDrive = nil } } + .onDrop(of: [Self.diskImageUTType], isTargeted: nil) { providers in + guard let item = providers.first, item.hasItemConformingToTypeIdentifier(Self.diskImageUTType.identifier) else { return false } + + item.loadItem(forTypeIdentifier: Self.diskImageUTType.identifier) { url, error in + if let url = url as? URL{ + selectRemovableImage(forDrive: drive, result: .success(url)) + } + if let error { + selectRemovableImage(forDrive: drive, result: .failure(error)) + } + } + return true + } } } } diff --git a/Platform/Shared/VMSettingsAddDeviceMenuView.swift b/Platform/Shared/VMSettingsAddDeviceMenuView.swift index e3d8e0126..636266fa0 100644 --- a/Platform/Shared/VMSettingsAddDeviceMenuView.swift +++ b/Platform/Shared/VMSettingsAddDeviceMenuView.swift @@ -74,12 +74,12 @@ struct VMSettingsAddDeviceMenuView: View { Button { isImportDriveShown.toggle() } label: { - Label("Import Drive", systemImage: "externaldrive") + Label("Import Drive…", systemImage: "externaldrive") } Button { isCreateDriveShown.toggle() } label: { - Label("New Drive", systemImage: "externaldrive.badge.plus") + Label("New Drive…", systemImage: "externaldrive.badge.plus") } #endif } label: { diff --git a/Platform/Shared/VMWizardDrivesView.swift b/Platform/Shared/VMWizardDrivesView.swift index 3daa362c4..4da457c2d 100644 --- a/Platform/Shared/VMWizardDrivesView.swift +++ b/Platform/Shared/VMWizardDrivesView.swift @@ -28,7 +28,7 @@ struct VMWizardDrivesView: View { NumberTextField("", number: $wizardState.storageSizeGib) .textFieldStyle(.roundedBorder) .frame(maxWidth: 50) - Text("GB") + Text("GiB") } } header: { Text("Size") diff --git a/Platform/Shared/VMWizardState.swift b/Platform/Shared/VMWizardState.swift index 2c3711033..48d9dbfc2 100644 --- a/Platform/Shared/VMWizardState.swift +++ b/Platform/Shared/VMWizardState.swift @@ -135,6 +135,7 @@ enum VMBootDevice: Int, Identifiable { @Published var sharingReadOnly: Bool = false @Published var name: String? @Published var isOpenSettingsAfterCreation: Bool = false + @Published var useNvmeAsDiskInterface = false /// SwiftUI BUG: on macOS 12, when VoiceOver is enabled and isBusy changes the disable state of a button being clicked, var isNeverDisabledWorkaround: Bool { @@ -342,7 +343,11 @@ enum VMBootDevice: Int, Identifiable { } } if !isSkipDiskCreate { - config.drives.append(UTMAppleConfigurationDrive(newSize: storageSizeGib * bytesInGib / bytesInMib)) + var newDisk = UTMAppleConfigurationDrive(newSize: storageSizeGib * bytesInGib / bytesInMib) + if #available(macOS 14, *), useNvmeAsDiskInterface { + newDisk.isNvme = true + } + config.drives.append(newDisk) } if #available(macOS 12, *), let sharingDirectoryURL = sharingDirectoryURL { config.sharedDirectories = [UTMAppleConfigurationSharedDirectory(directoryURL: sharingDirectoryURL, isReadOnly: sharingReadOnly)] diff --git a/Platform/UTMData.swift b/Platform/UTMData.swift index 66ee559d1..b1364bcb8 100644 --- a/Platform/UTMData.swift +++ b/Platform/UTMData.swift @@ -27,6 +27,7 @@ import AltKit #if WITH_SERVER import Combine #endif +import SwiftCopyfile #if WITH_REMOTE import CocoaSpiceNoUsb @@ -65,6 +66,9 @@ struct AlertMessage: Identifiable { /// View: show busy spinner @Published var busy: Bool + /// View: show a percent progress in the busy spinner + @Published var busyProgress: Float? + /// View: currently selected VM @Published var selectedVM: VMData? @@ -577,9 +581,12 @@ struct AlertMessage: Identifiable { /// - Parameter vm: VM to calculate size /// - Returns: Size in bytes func computeSize(for vm: VMData) async -> Int64 { - let path = vm.pathUrl - guard let enumerator = fileManager.enumerator(at: path, includingPropertiesForKeys: [.totalFileAllocatedSizeKey]) else { - logger.error("failed to create enumerator for \(path)") + return computeSize(recursiveFor: vm.pathUrl) + } + + private func computeSize(recursiveFor url: URL) -> Int64 { + guard let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: [.totalFileAllocatedSizeKey]) else { + logger.error("failed to create enumerator for \(url)") return 0 } var total: Int64 = 0 @@ -612,9 +619,13 @@ struct AlertMessage: Identifiable { /// - Parameter asShortcut: Create a shortcut rather than a copy func importUTM(from url: URL, asShortcut: Bool = true) async throws { guard url.isFileURL else { return } - _ = url.startAccessingSecurityScopedResource() - defer { url.stopAccessingSecurityScopedResource() } - + let isScopedAccess = url.startAccessingSecurityScopedResource() + defer { + if isScopedAccess { + url.stopAccessingSecurityScopedResource() + } + } + logger.info("importing: \(url)") // attempt to turn temp URL to presistent bookmark early otherwise, // when stopAccessingSecurityScopedResource() is called, we lose access @@ -661,12 +672,26 @@ struct AlertMessage: Identifiable { } private func copyItemWithCopyfile(at srcURL: URL, to dstURL: URL) async throws { - try await Task.detached(priority: .userInitiated) { - let status = copyfile(srcURL.path, dstURL.path, nil, copyfile_flags_t(COPYFILE_ALL | COPYFILE_RECURSIVE | COPYFILE_CLONE | COPYFILE_DATA_SPARSE)) - if status < 0 { - throw NSError(domain: NSPOSIXErrorDomain, code: Int(errno)) + let totalSize = computeSize(recursiveFor: srcURL) + var lastUpdate = Date() + var lastProgress: CopyManager.Progress? + var copiedSize: Int64 = 0 + defer { + busyProgress = nil + } + for try await progress in CopyManager.default.copyItemProgress(at: srcURL, to: dstURL, flags: [.all, .recursive, .clone, .dataSparse]) { + if let _lastProgress = lastProgress, _lastProgress.srcPath != _lastProgress.srcPath { + copiedSize += _lastProgress.bytesCopied + lastProgress = progress + } else { + lastProgress = progress } - }.value + if totalSize > 0 && lastUpdate.timeIntervalSinceNow < -1 { + lastUpdate = Date() + let completed = Float(copiedSize + progress.bytesCopied) / Float(totalSize) + busyProgress = completed > 1.0 ? 1.0 : completed + } + } } // MARK: - Downloading VMs @@ -755,7 +780,15 @@ struct AlertMessage: Identifiable { func reclaimSpace(for driveUrl: URL, withCompression isCompressed: Bool = false) async throws { let baseUrl = driveUrl.deletingLastPathComponent() let dstUrl = Self.newImage(from: driveUrl, to: baseUrl, withExtension: "qcow2") - try await UTMQemuImage.convert(from: driveUrl, toQcow2: dstUrl, withCompression: isCompressed) + defer { + busyProgress = nil + } + try await UTMQemuImage.convert(from: driveUrl, toQcow2: dstUrl, withCompression: isCompressed) { progress in + Task { @MainActor in + self.busyProgress = progress / 100 + } + } + busyProgress = nil do { try fileManager.replaceItem(at: driveUrl, withItemAt: dstUrl, backupItemName: nil, resultingItemURL: nil) } catch { diff --git a/Platform/UTMReleaseHelper.swift b/Platform/UTMReleaseHelper.swift index 4848aa648..684d529c4 100644 --- a/Platform/UTMReleaseHelper.swift +++ b/Platform/UTMReleaseHelper.swift @@ -47,10 +47,10 @@ class UTMReleaseHelper: ObservableObject { return } let configuration = URLSessionConfiguration.ephemeral - configuration.allowsCellularAccess = false + configuration.allowsCellularAccess = true configuration.allowsExpensiveNetworkAccess = false configuration.allowsConstrainedNetworkAccess = false - configuration.waitsForConnectivity = true + configuration.waitsForConnectivity = false configuration.httpAdditionalHeaders = ["Accept": "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28"] let session = URLSession(configuration: configuration) diff --git a/Platform/VMData.swift b/Platform/VMData.swift index f96826e22..fb70720b5 100644 --- a/Platform/VMData.swift +++ b/Platform/VMData.swift @@ -521,7 +521,7 @@ extension VMRemoteDataError: LocalizedError { case .notImplemented: return NSLocalizedString("This function is not implemented.", comment: "VMData") case .backendNotSupported: - return NSLocalizedString("This VM is configured for a backend that does not support remote clients.", comment: "VMData") + return NSLocalizedString("This VM is not available or is configured for a backend that does not support remote clients.", comment: "VMData") } } } diff --git a/Platform/de.lproj/Localizable.strings b/Platform/de.lproj/Localizable.strings index 9b0a636de..ad4f2ecd1 100644 --- a/Platform/de.lproj/Localizable.strings +++ b/Platform/de.lproj/Localizable.strings @@ -364,7 +364,7 @@ "Force Multicore" = "Multicore erzwingen"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB Debug Stub"; @@ -520,7 +520,7 @@ "Maximum Shared USB Devices" = "Anzahl weitergeleiteter USB-Geräte (max.)"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "Speicher"; diff --git a/Platform/es-419.lproj/Localizable.strings b/Platform/es-419.lproj/Localizable.strings index 05f1b542b..da3fb3e79 100644 --- a/Platform/es-419.lproj/Localizable.strings +++ b/Platform/es-419.lproj/Localizable.strings @@ -675,7 +675,7 @@ "Full Graphics" = "Gráficos completos"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB Debug Stub"; @@ -797,7 +797,7 @@ "Import…" = "Importar..."; /* No comment provided by engineer. */ -"Import Drive" = "Importar unidad"; +"Import Drive…" = "Importar unidad..."; /* No comment provided by engineer. */ "Import VHDX Image" = "Importar una imagen VHDX"; @@ -989,7 +989,7 @@ "Maximum Shared USB Devices" = "Número máximo de dispositivos USB compartidos"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "Memoria"; @@ -1055,7 +1055,7 @@ "New…" = "Nuevo..."; /* No comment provided by engineer. */ -"New Drive" = "Nueva unidad"; +"New Drive…" = "Nueva unidad..."; /* No comment provided by engineer. */ "New from template…" = "Nuevo desde plantilla..."; diff --git a/Platform/fi.lproj/Localizable.strings b/Platform/fi.lproj/Localizable.strings index 58d87eaef..6b01b6fcf 100644 --- a/Platform/fi.lproj/Localizable.strings +++ b/Platform/fi.lproj/Localizable.strings @@ -440,7 +440,7 @@ "Full Graphics" = "Täysi grafiikka"; /* No comment provided by engineer. */ -"GB" = "Gt"; +"GiB" = "GiB"; /* No comment provided by engineer. */ "Generate Windows Installer ISO" = "Luo Windows Installer ISO"; @@ -521,7 +521,7 @@ "Import…" = "Tuo..."; /* No comment provided by engineer. */ -"Import Drive" = "Tuo asema"; +"Import Drive…" = "Tuo asema..."; /* No comment provided by engineer. */ "Import VHDX Image" = "Importer une image VHDX"; @@ -650,7 +650,7 @@ "Maximum Shared USB Devices" = "Jaettujen USB-laitteiden enimmäismäärä"; /* No comment provided by engineer. */ -"MB" = "Mt"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "Muisti"; @@ -680,7 +680,7 @@ "New" = "Uusi"; /* No comment provided by engineer. */ -"New Drive" = "Uusi asema"; +"New Drive…" = "Uusi asema…"; /* VMConfigPortForwardingViewController */ "New port forward" = "Uusi portitus"; diff --git a/Platform/fr.lproj/Localizable.strings b/Platform/fr.lproj/Localizable.strings index 11f54201c..806bb2839 100644 --- a/Platform/fr.lproj/Localizable.strings +++ b/Platform/fr.lproj/Localizable.strings @@ -463,11 +463,11 @@ // RAMSlider.swift "Size" = "Taille"; -"MB" = "MB"; +"MiB" = "MiB"; // SizeTextField.swift "The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "La quantité de stockage à allouer pour cette image. Ignoré si importation d’image. Si c'est une image brute, un fichier vide de la même taille sera enregistré avec la VM. Sinon, l’image disque sera dynamiquement étendue jusqu’à cette taille."; -"GB" = "GB"; +"GiB" = "GiB"; // VMCardView.swift "Run" = "Démarrer"; @@ -679,8 +679,8 @@ "Removable" = "Amovible"; // VMSettingsAddDeviceMenuView.swift -"Import Drive" = "Importer un lecteur"; -"New Drive" = "Nouveu lecteur"; +"Import Drive…" = "Importer un lecteur…"; +"New Drive…" = "Nouveau lecteur…";= // VMToolbarModifier.swift "Remove selected shortcut" = "Supprimer le raccourci sélectionné"; diff --git a/Platform/iOS/Display/VMDisplayTerminalViewController.swift b/Platform/iOS/Display/VMDisplayTerminalViewController.swift index b2195884b..f65a52c4e 100644 --- a/Platform/iOS/Display/VMDisplayTerminalViewController.swift +++ b/Platform/iOS/Display/VMDisplayTerminalViewController.swift @@ -85,14 +85,26 @@ extension VMDisplayTerminalViewController { var useAutoLayout: Bool { get { true } } - + + // This prevents curved edge from cutting off the content + var additionalTopPadding: CGFloat { + if UIDevice.current.userInterfaceIdiom == .pad { + let scenes = UIApplication.shared.connectedScenes + let windowScene = scenes.first as? UIWindowScene + guard let window = windowScene?.windows.first else { return 0 } + return window.safeAreaInsets.bottom + } else { + return 0 + } + } + func makeFrame (keyboardDelta: CGFloat, _ fn: String = #function, _ ln: Int = #line) -> CGRect { if useAutoLayout { return CGRect.zero } else { return CGRect (x: view.safeAreaInsets.left, - y: view.safeAreaInsets.top, + y: view.safeAreaInsets.top + additionalTopPadding, width: view.frame.width - view.safeAreaInsets.left - view.safeAreaInsets.right, height: view.frame.height - view.safeAreaInsets.top - keyboardDelta) } @@ -101,13 +113,16 @@ extension VMDisplayTerminalViewController { func setupKeyboardMonitor () { if #available(iOS 15.0, *), useAutoLayout { + #if os(visionOS) + let inputAccessoryHeight: CGFloat = 0 + #else + let inputAccessoryHeight = terminalView.inputAccessoryView?.frame.height ?? 0 + #endif terminalView.translatesAutoresizingMaskIntoConstraints = false - terminalView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor).isActive = true + terminalView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: additionalTopPadding).isActive = true terminalView.leftAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leftAnchor).isActive = true terminalView.rightAnchor.constraint(equalTo: view.safeAreaLayoutGuide.rightAnchor).isActive = true - terminalView.bottomAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true - terminalView.keyboardLayoutGuide.topAnchor.constraint(equalTo: terminalView.bottomAnchor).isActive = true - terminalView.keyboardLayoutGuide.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor).isActive = true + terminalView.bottomAnchor.constraint(equalTo: view.keyboardLayoutGuide.topAnchor, constant: -inputAccessoryHeight).isActive = true } else { NotificationCenter.default.addObserver( self, diff --git a/Platform/iOS/Donation.storekit b/Platform/iOS/Donation.storekit new file mode 100644 index 000000000..c05231e8b --- /dev/null +++ b/Platform/iOS/Donation.storekit @@ -0,0 +1,195 @@ +{ + "identifier" : "A2B91788", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + { + "displayPrice" : "0.99", + "familyShareable" : false, + "internalID" : "4FFB8333", + "localizations" : [ + { + "description" : "The most basic building block.", + "displayName" : "Transistor", + "locale" : "en_US" + } + ], + "productID" : "consumable.small", + "referenceName" : "Transistor", + "type" : "Consumable" + }, + { + "displayPrice" : "4.99", + "familyShareable" : false, + "internalID" : "B6BBB675", + "localizations" : [ + { + "description" : "Each one has a unique functionality.", + "displayName" : "Chip", + "locale" : "en_US" + } + ], + "productID" : "consumable.medium", + "referenceName" : "Chip", + "type" : "Consumable" + }, + { + "displayPrice" : "9.99", + "familyShareable" : false, + "internalID" : "FAEB2D3C", + "localizations" : [ + { + "description" : "Gets the work done.", + "displayName" : "Computer", + "locale" : "en_US" + } + ], + "productID" : "consumable.large", + "referenceName" : "Computer", + "type" : "Consumable" + } + ], + "settings" : { + "_failTransactionsEnabled" : false, + "_locale" : "en_US", + "_storefront" : "USA", + "_storeKitErrors" : [ + { + "current" : null, + "enabled" : false, + "name" : "Load Products" + }, + { + "current" : null, + "enabled" : false, + "name" : "Purchase" + }, + { + "current" : null, + "enabled" : false, + "name" : "Verification" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Store Sync" + }, + { + "current" : null, + "enabled" : false, + "name" : "Subscription Status" + }, + { + "current" : null, + "enabled" : false, + "name" : "App Transaction" + }, + { + "current" : null, + "enabled" : false, + "name" : "Manage Subscriptions Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Refund Request Sheet" + }, + { + "current" : null, + "enabled" : false, + "name" : "Offer Code Redeem Sheet" + } + ] + }, + "subscriptionGroups" : [ + { + "id" : "D96518D8", + "localizations" : [ + + ], + "name" : "Donation", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "0.99", + "familyShareable" : false, + "groupNumber" : 3, + "internalID" : "C7B7ED7B", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "Ones and zeros would be lonely without them.", + "displayName" : "Logic", + "locale" : "en_US" + } + ], + "productID" : "subscription.small", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "Logic", + "subscriptionGroupID" : "D96518D8", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "4.99", + "familyShareable" : false, + "groupNumber" : 2, + "internalID" : "A193204D", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "Converts inputs to outputs in a fancy way.", + "displayName" : "Function", + "locale" : "en_US" + } + ], + "productID" : "subscription.medium", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "Function", + "subscriptionGroupID" : "D96518D8", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "9.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "08CDD5EB", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "Maybe it was all a mistake.", + "displayName" : "Software", + "locale" : "en_US" + } + ], + "productID" : "subscription.large", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "Software", + "subscriptionGroupID" : "D96518D8", + "type" : "RecurringSubscription" + } + ] + } + ], + "version" : { + "major" : 3, + "minor" : 0 + } +} diff --git a/Platform/iOS/Settings.bundle/zh-HK.lproj/Root.strings b/Platform/iOS/Settings.bundle/zh-HK.lproj/Root.strings index 9272e39b9..169d83020 100644 --- a/Platform/iOS/Settings.bundle/zh-HK.lproj/Root.strings +++ b/Platform/iOS/Settings.bundle/zh-HK.lproj/Root.strings @@ -145,3 +145,52 @@ /* (No Comment) */ "Up" = "上"; +// Additional Strings (unable to be extracted by Xcode) + +/* (No Comment) */ +"ABOUT" = "關於"; + +/* (No Comment) */ +"Build" = "建立版號"; + +/* (No Comment) */ +"CURSOR - DRAG SPEED" = "指標 - 拖放速度"; + +/* (No Comment) */ +"CURSOR - SCROLL WHEEL" = "指標 - 捲動輪"; + +/* (No Comment) */ +"Default" = "預設值"; + +/* (No Comment) */ +"DEVICES" = "裝置"; + +/* (No Comment) */ +"Disable screen dimming when idle" = "閒置時停用變暗螢幕"; + +/* (No Comment) */ +"Do not save VM screenshot to disk" = "不要儲存虛擬電腦的螢幕截圖至磁碟"; + +/* (No Comment) */ +"FPS Limit" = "FPS 限制"; + +/* (No Comment) */ +"GRAPHICS" = "圖形"; + +/* (No Comment) */ +"IDLE" = "閒置"; + +/* (No Comment) */ +"Invert Scroll" = "反轉捲動"; + +/* (No Comment) */ +"License" = "許可"; + +/* (No Comment) */ +"Prefer device to external microphone" = "偏好外置咪高風裝置"; + +/* (No Comment) */ +"Renderer Backend" = "渲染器後端"; + +/* (No Comment) */ +"Version" = "版本"; diff --git a/Platform/iOS/Settings.bundle/zh-Hans.lproj/Root.strings b/Platform/iOS/Settings.bundle/zh-Hans.lproj/Root.strings index 85c5f20c8..292198a39 100644 --- a/Platform/iOS/Settings.bundle/zh-Hans.lproj/Root.strings +++ b/Platform/iOS/Settings.bundle/zh-Hans.lproj/Root.strings @@ -145,3 +145,52 @@ /* (No Comment) */ "Up" = "上"; +// Additional Strings (unable to be extracted by Xcode) + +/* (No Comment) */ +"ABOUT" = "关于"; + +/* (No Comment) */ +"Build" = "构建"; + +/* (No Comment) */ +"CURSOR - DRAG SPEED" = "指针 - 拖放速度"; + +/* (No Comment) */ +"CURSOR - SCROLL WHEEL" = "指针 - 滚轮"; + +/* (No Comment) */ +"Default" = "默认"; + +/* (No Comment) */ +"DEVICES" = "设备"; + +/* (No Comment) */ +"Disable screen dimming when idle" = "闲置时禁用屏幕变暗"; + +/* (No Comment) */ +"Do not save VM screenshot to disk" = "不将虚拟机截图保存到磁盘"; + +/* (No Comment) */ +"FPS Limit" = "FPS 上限"; + +/* (No Comment) */ +"GRAPHICS" = "图形"; + +/* (No Comment) */ +"IDLE" = "闲置"; + +/* (No Comment) */ +"Invert Scroll" = "反转滚动"; + +/* (No Comment) */ +"License" = "许可"; + +/* (No Comment) */ +"Prefer device to external microphone" = "首选外置麦克风设备"; + +/* (No Comment) */ +"Renderer Backend" = "渲染器后端"; + +/* (No Comment) */ +"Version" = "版本"; diff --git a/Platform/iOS/UTMDonateStore.swift b/Platform/iOS/UTMDonateStore.swift new file mode 100644 index 000000000..24e0124a8 --- /dev/null +++ b/Platform/iOS/UTMDonateStore.swift @@ -0,0 +1,203 @@ +// +// Copyright © 2024 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 Foundation +import StoreKit + +@available(iOS 15, *) +class UTMDonateStore: ObservableObject { + typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState + + enum StoreError: Error { + case failedVerification + } + + let productImages: [String: String] = [ + "consumable.small": "switch.2", + "consumable.medium": "memorychip", + "consumable.large": "pc", + "subscription.small": "sum", + "subscription.medium": "function", + "subscription.large": "opticaldisc", + ] + + @Published private(set) var consumables: [Product] + @Published private(set) var subscriptions: [Product] + + @Published private(set) var purchasedSubscriptions: [Product] = [] + @Published private(set) var subscriptionGroupStatus: RenewalState? + + @Published private(set) var isLoaded: Bool = false + @Published private(set) var id: UUID = UUID() + + var updateListenerTask: Task? = nil + + init() { + //Initialize empty products, and then do a product request asynchronously to fill them in. + consumables = [] + subscriptions = [] + + //Start a transaction listener as close to app launch as possible so you don't miss any transactions. + updateListenerTask = listenForTransactions() + + Task { + //During store initialization, request products from the App Store. + await requestProducts() + + //Deliver products that the customer purchases. + await updateCustomerProductStatus() + } + } + + deinit { + updateListenerTask?.cancel() + } + + func listenForTransactions() -> Task { + return Task.detached { + //Iterate through any transactions that don't come from a direct call to `purchase()`. + for await result in Transaction.updates { + do { + let transaction = try self.checkVerified(result) + + //Deliver products to the user. + await self.updateCustomerProductStatus() + + //Always finish a transaction. + await transaction.finish() + } catch { + //StoreKit has a transaction that fails verification. Don't deliver content to the user. + logger.error("Transaction failed verification") + } + } + } + } + + @MainActor + func requestProducts() async { + isLoaded = false + do { + let storeProducts = try await Product.products(for: productImages.keys) + + var newConsumables: [Product] = [] + var newSubscriptions: [Product] = [] + + //Filter the products into categories based on their type. + for product in storeProducts { + switch product.type { + case .consumable: + newConsumables.append(product) + case .autoRenewable: + newSubscriptions.append(product) + default: + //Ignore this product. + logger.error("Unknown product: \(product)") + } + } + + //Sort each product category by price, lowest to highest, to update the store. + consumables = sortByPrice(newConsumables) + subscriptions = sortByPrice(newSubscriptions) + } catch { + logger.error("Failed product request from the App Store server: \(error)") + } + isLoaded = true + } + + func purchase(with action: () async throws -> Product.PurchaseResult) async throws -> Transaction? { + //Begin purchasing the `Product` the user selects. + let result = try await action() + + switch result { + case .success(let verification): + //Check whether the transaction is verified. If it isn't, + //this function rethrows the verification error. + let transaction = try checkVerified(verification) + + //The transaction is verified. Deliver content to the user. + await updateCustomerProductStatus() + + //Always finish a transaction. + await transaction.finish() + + return transaction + case .userCancelled, .pending: + return nil + default: + return nil + } + } + + func isPurchased(_ product: Product) async throws -> Bool { + //Determine whether the user purchases a given product. + switch product.type { + case .autoRenewable: + return purchasedSubscriptions.contains(product) + default: + return false + } + } + + func checkVerified(_ result: VerificationResult) throws -> T { + //Check whether the JWS passes StoreKit verification. + switch result { + case .unverified: + //StoreKit parses the JWS, but it fails verification. + throw StoreError.failedVerification + case .verified(let safe): + //The result is verified. Return the unwrapped value. + return safe + } + } + + @MainActor + func updateCustomerProductStatus() async { + var purchasedSubscriptions: [Product] = [] + + //Iterate through all of the user's purchased products. + for await result in Transaction.currentEntitlements { + do { + //Check whether the transaction is verified. If it isn’t, catch `failedVerification` error. + let transaction = try checkVerified(result) + + //Check the `productType` of the transaction and get the corresponding product from the store. + switch transaction.productType { + case .autoRenewable: + if let subscription = subscriptions.first(where: { $0.id == transaction.productID }) { + purchasedSubscriptions.append(subscription) + } + default: + break + } + } catch { + logger.error("failed to update product status: \(error)") + } + } + + //Update the store information with auto-renewable subscription products. + self.purchasedSubscriptions = purchasedSubscriptions + + //Check the `subscriptionGroupStatus` to learn the auto-renewable subscription state to determine whether the customer + //is new (never subscribed), active, or inactive (expired subscription). This app has only one subscription + //group, so products in the subscriptions array all belong to the same group. The statuses that + //`product.subscription.status` returns apply to the entire subscription group. + subscriptionGroupStatus = try? await subscriptions.first?.subscription?.status.first?.state + } + + func sortByPrice(_ products: [Product]) -> [Product] { + products.sorted(by: { return $0.price < $1.price }) + } +} diff --git a/Platform/iOS/UTMDonateView.swift b/Platform/iOS/UTMDonateView.swift new file mode 100644 index 000000000..a3279ac34 --- /dev/null +++ b/Platform/iOS/UTMDonateView.swift @@ -0,0 +1,271 @@ +// +// Copyright © 2024 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 +import StoreKit + +struct UTMDonateView: View { + @Environment(\.presentationMode) var presentationMode + + private var appIcon: String? { + guard let icons = Bundle.main.object(forInfoDictionaryKey: "CFBundleIcons") as? [String: Any], + let primaryIcon = icons["CFBundlePrimaryIcon"] as? [String: Any], + let iconFiles = primaryIcon["CFBundleIconFiles"] as? [String], + let iconFileName = iconFiles.last else { + return nil + } + return iconFileName + } + + var body: some View { + NavigationView { + VStack { + if let appIcon = appIcon, let image = UIImage(named: appIcon) { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .clipShape(RoundedRectangle(cornerRadius: 12.632, style: .continuous)) + .frame(width: 72, height: 72) + } + Text("Your support is the driving force that helps UTM stay independent. Your contribution, no matter the size, makes a significant difference. It enables us to develop new features and maintain existing ones. Thank you for considering a donation to support us.") + .padding() + if #available(iOS 15, *) { + StoreView() + } else { + List { + Link("GitHub Sponsors", destination: URL(string: "https://github.com/sponsors/utmapp")!) + } + } + }.navigationTitle("Support UTM") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button("Close") { + presentationMode.wrappedValue.dismiss() + } + } + } + }.navigationViewStyle(.stack) + } +} + +@available(iOS 15, *) +private struct StoreView: View { + @StateObject private var store = UTMDonateStore() + + var body: some View { + if !store.isLoaded { + ProgressView() + Spacer() + } else { + List { + if !store.consumables.isEmpty { + Section("One Time Donation") { + ForEach(store.consumables) { item in + ListCellView(store: store, product: item) + } + } + .listStyle(.grouped) + } + if !store.subscriptions.isEmpty { + Section("Recurring Donation") { + ForEach(store.subscriptions) { item in + ListCellView(store: store, product: item) + } + Link("Manage Subscriptions…", destination: URL(string: "itms-apps://apps.apple.com/account/subscriptions")!) + } + .listStyle(.grouped) + } + if store.consumables.isEmpty && store.subscriptions.isEmpty { + Link("GitHub Sponsors", destination: URL(string: "https://github.com/sponsors/utmapp")!) + } else { + Button("Restore Purchases") { + Task { + try? await AppStore.sync() + } + } + } + } + } + } +} + +@available(iOS 15, *) +private struct ListCellView: View { + @ObservedObject var store: UTMDonateStore + @State var isPurchased: Bool = false + @State var errorTitle = "" + @State var isShowingError: Bool = false + @State var isLoaded: Bool = false + + #if os(visionOS) + @Environment(\.purchase) var purchase + #endif + + let product: Product + let purchasingEnabled: Bool + + var systemImage: String? { + store.productImages[product.id] + } + + init(store: UTMDonateStore, product: Product, purchasingEnabled: Bool = true) { + self.store = store + self.product = product + self.purchasingEnabled = purchasingEnabled + } + + var body: some View { + HStack { + Image(systemName: systemImage ?? "heart.fill") + .font(.system(size: 36)) + .frame(width: 48, height: 48) + .padding(.trailing, 20) + if purchasingEnabled { + productDetail + Spacer() + buyButton + .buttonStyle(BuyButtonStyle(isPurchased: isPurchased)) + .disabled(isPurchased) + } else { + productDetail + } + } + .alert(isPresented: $isShowingError, content: { + Alert(title: Text(errorTitle), message: nil, dismissButton: .default(Text("OK"))) + }) + } + + @ViewBuilder + var productDetail: some View { + VStack(alignment: .leading) { + Text(product.displayName) + .bold() + Text(product.description) + } + } + + func subscribeButton(_ subscription: Product.SubscriptionInfo) -> some View { + let unit: String + let plural = 1 < subscription.subscriptionPeriod.value + switch subscription.subscriptionPeriod.unit { + case .day: + unit = plural ? String.localizedStringWithFormat(NSLocalizedString("%d days", comment: "UTMDonateView"), subscription.subscriptionPeriod.value) : NSLocalizedString("day", comment: "UTMDonateView") + case .week: + unit = plural ? String.localizedStringWithFormat(NSLocalizedString("%d weeks", comment: "UTMDonateView"), subscription.subscriptionPeriod.value) : NSLocalizedString("week", comment: "UTMDonateView") + case .month: + unit = plural ? String.localizedStringWithFormat(NSLocalizedString("%d months", comment: "UTMDonateView"), subscription.subscriptionPeriod.value) : NSLocalizedString("month", comment: "UTMDonateView") + case .year: + unit = plural ? String.localizedStringWithFormat(NSLocalizedString("%d years", comment: "UTMDonateView"), subscription.subscriptionPeriod.value) : NSLocalizedString("year", comment: "UTMDonateView") + @unknown default: + unit = NSLocalizedString("period", comment: "UTMDonateView") + } + + return VStack { + Text(product.displayPrice) + .foregroundColor(.white) + .bold() + .padding(EdgeInsets(top: -4.0, leading: 0.0, bottom: -8.0, trailing: 0.0)) + Divider() + .background(Color.white) + Text(unit) + .foregroundColor(.white) + .font(.system(size: 12)) + .padding(EdgeInsets(top: -8.0, leading: 0.0, bottom: -4.0, trailing: 0.0)) + } + } + + var buyButton: some View { + Button(action: { + Task { + await buy() + } + }) { + if !isLoaded { + ProgressView() + .tint(.white) + } else if isPurchased { + Text(Image(systemName: "checkmark")) + .bold() + .foregroundColor(.white) + } else { + if let subscription = product.subscription { + subscribeButton(subscription) + } else { + Text(product.displayPrice) + .foregroundColor(.white) + .bold() + } + } + } + .onAppear { + Task { + isPurchased = (try? await store.isPurchased(product)) ?? false + isLoaded = true + } + } + .onChange(of: store.purchasedSubscriptions) { _ in + Task { + isPurchased = (try? await store.isPurchased(product)) ?? false + } + } + } + + func buy() async { + do { + if try await store.purchase(with: { + #if os(visionOS) + try await purchase(product) + #else + try await product.purchase() + #endif + }) != nil { + withAnimation { + isPurchased = true + } + } + } catch UTMDonateStore.StoreError.failedVerification { + errorTitle = NSLocalizedString("Your purchase could not be verified by the App Store.", comment: "UTMDonateView") + isShowingError = true + } catch { + logger.error("Failed purchase for \(product.id): \(error)") + } + } +} + +private struct BuyButtonStyle: ButtonStyle { + let isPurchased: Bool + + init(isPurchased: Bool = false) { + self.isPurchased = isPurchased + } + + func makeBody(configuration: Self.Configuration) -> some View { + var bgColor: Color = isPurchased ? Color.green : Color.blue + bgColor = configuration.isPressed ? bgColor.opacity(0.7) : bgColor.opacity(1) + + return configuration.label + .frame(width: 50) + .padding(10) + .background(bgColor) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + .scaleEffect(configuration.isPressed ? 0.9 : 1.0) + } +} + +#Preview { + UTMDonateView() +} diff --git a/Platform/iOS/VMDrivesSettingsView.swift b/Platform/iOS/VMDrivesSettingsView.swift index c8cab8012..3e703a89f 100644 --- a/Platform/iOS/VMDrivesSettingsView.swift +++ b/Platform/iOS/VMDrivesSettingsView.swift @@ -35,6 +35,16 @@ struct VMDrivesSettingsView: View { attemptDelete = offsets } .onMove(perform: moveDrives) + Button { + isImportDriveShown.toggle() + } label: { + Text("Import Drive…") + } + Button { + isCreateDriveShown.toggle() + } label: { + Text("New Drive…") + } .nonbrokenSheet(isPresented: $isCreateDriveShown) { CreateDrive(newDrive: UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target), onDismiss: newDrive) } @@ -71,7 +81,7 @@ struct VMDrivesSettingsView: View { switch result { case .success(let url): await MainActor.run { - var drive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: true) + var drive = UTMQemuConfigurationDrive(forArchitecture: config.system.architecture, target: config.system.target, isExternal: false) drive.imageURL = url config.drives.append(drive) } diff --git a/Platform/iOS/VMToolbarDisplayMenuView.swift b/Platform/iOS/VMToolbarDisplayMenuView.swift index f0d1e4f46..574f8c3ff 100644 --- a/Platform/iOS/VMToolbarDisplayMenuView.swift +++ b/Platform/iOS/VMToolbarDisplayMenuView.swift @@ -55,7 +55,10 @@ struct VMToolbarDisplayMenuView: View { Picker("", selection: externalWindowBinding.device) { MenuLabel("None", systemImage: "rectangle.dashed").tag(nil as VMWindowState.Device?) ForEach(session.devices) { device in - if case .display(_, let index) = device { + switch device { + case .serial(_, let index): + MenuLabel("Serial \(index): \(session.qemuConfig.serials[index].target.prettyValue)", systemImage: "rectangle.connected.to.line.below").tag(device as VMWindowState.Device?) + case .display(_, let index): MenuLabel("Display \(index): \(session.qemuConfig.displays[index].hardware.prettyValue)", systemImage: "display").tag(device as VMWindowState.Device?) } } diff --git a/Platform/iOS/VMToolbarView.swift b/Platform/iOS/VMToolbarView.swift index 5a9d2bfc0..2d7141be5 100644 --- a/Platform/iOS/VMToolbarView.swift +++ b/Platform/iOS/VMToolbarView.swift @@ -15,9 +15,10 @@ // import SwiftUI +import TipKit struct VMToolbarView: View { - @AppStorage("ToolbarIsCollapsed") private var isCollapsed: Bool = true + @AppStorage("ToolbarIsCollapsed") private var isCollapsed: Bool = false @AppStorage("ToolbarLocation") private var location: ToolbarLocation = .topRight @State private var shake: Bool = true @State private var isMoving: Bool = false @@ -145,6 +146,7 @@ struct VMToolbarView: View { } label: { Label("Hide", systemImage: isCollapsed ? nameOfHideIcon : nameOfShowIcon) }.buttonStyle(.toolbar(horizontalSizeClass: horizontalSizeClass, verticalSizeClass: verticalSizeClass)) + .modifier(HideToolbarTipModifier(isCollapsed: $isCollapsed)) .opacity(toolbarToggleOpacity) .modifier(Shake(shake: shake)) .position(position(for: geometry)) @@ -387,3 +389,35 @@ extension MenuStyle where Self == ToolbarMenuStyle { DispatchQueue.main.asyncAfter(deadline: .now() + 15, execute: longIdleTask!) } } + +private struct HideToolbarTipModifier: ViewModifier { + @Binding var isCollapsed: Bool + private let _hideToolbarTip: Any? + + @available(iOS 17, *) + private var hideToolbarTip: UTMTipHideToolbar { + _hideToolbarTip as! UTMTipHideToolbar + } + + init(isCollapsed: Binding) { + _isCollapsed = isCollapsed + if #available(iOS 17, *) { + _hideToolbarTip = UTMTipHideToolbar() + } else { + _hideToolbarTip = nil + } + } + + @ViewBuilder + func body(content: Content) -> some View { + if #available(iOS 17, *) { + content + .popoverTip(hideToolbarTip, arrowEdge: .top) + .onAppear { + UTMTipHideToolbar.didHideToolbar = isCollapsed + } + } else { + content + } + } +} diff --git a/Platform/iOS/VMWindowView.swift b/Platform/iOS/VMWindowView.swift index ce4e1b82f..688ff6d00 100644 --- a/Platform/iOS/VMWindowView.swift +++ b/Platform/iOS/VMWindowView.swift @@ -56,8 +56,10 @@ struct VMWindowView: View { switch device { case .display(_, _): VMDisplayHostedView(vm: session.vm, device: device, state: $state) + .prefersPersistentSystemOverlaysHidden() case .serial(_, _): VMDisplayHostedView(vm: session.vm, device: device, state: $state) + .prefersPersistentSystemOverlaysHidden() } } else if !state.isBusy && state.isRunning { // headless @@ -307,3 +309,13 @@ fileprivate struct VMToolbarOrnamentModifier: ViewModifier { } } #endif + +private extension View { + func prefersPersistentSystemOverlaysHidden() -> some View { + if #available(iOS 16, *) { + return self.persistentSystemOverlays(.hidden) + } else { + return self + } + } +} diff --git a/Platform/iOS/ja.lproj/InfoPlist.strings b/Platform/iOS/ja.lproj/InfoPlist.strings index 8437e0527..54ae442fb 100644 --- a/Platform/iOS/ja.lproj/InfoPlist.strings +++ b/Platform/iOS/ja.lproj/InfoPlist.strings @@ -1,5 +1,5 @@ /* Privacy - Local Network Usage Description */ -"NSLocalNetworkUsageDescription" = "仮想マシンがローカルネットワークにアクセスする可能性があります。UTMもAltServerと通信するためにローカルネットワークを使用します。"; +"NSLocalNetworkUsageDescription" = "仮想マシンがローカルネットワークにアクセスできるようになります。UTMもローカルサーバと通信するためにネットワークを使用することがあります。"; /* Privacy - Location Always and When In Use Usage Description */ "NSLocationAlwaysAndWhenInUseUsageDescription" = "UTMは定期的に位置データを要求することで、システムがバックグラウンドプロセスをアクティブに保つようにします。位置データが送信されることはありません。"; diff --git a/Platform/iOS/zh-HK.lproj/Info-Remote-InfoPlist.strings b/Platform/iOS/zh-HK.lproj/Info-Remote-InfoPlist.strings new file mode 100644 index 000000000..6387088a3 --- /dev/null +++ b/Platform/iOS/zh-HK.lproj/Info-Remote-InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle name */ +"CFBundleName" = "UTM 遠端"; + +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "UTM 使用本地網絡尋找並連接至 UTM 遠端伺服器。"; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "任何虛擬電腦都需要許可才能由咪高風進行錄製。"; + diff --git a/Platform/iOS/zh-Hans.lproj/Info-Remote-InfoPlist.strings b/Platform/iOS/zh-Hans.lproj/Info-Remote-InfoPlist.strings new file mode 100644 index 000000000..3112abb40 --- /dev/null +++ b/Platform/iOS/zh-Hans.lproj/Info-Remote-InfoPlist.strings @@ -0,0 +1,9 @@ +/* Bundle name */ +"CFBundleName" = "UTM 远程"; + +/* Privacy - Local Network Usage Description */ +"NSLocalNetworkUsageDescription" = "UTM 使用本地网络查找和连接 UTM 远程服务器。"; + +/* Privacy - Microphone Usage Description */ +"NSMicrophoneUsageDescription" = "任何虚拟机都需要获得许可才能从麦克风录音。"; + diff --git a/Platform/it.lproj/Localizable.strings b/Platform/it.lproj/Localizable.strings index 2ccc771ce..da616dfe0 100644 --- a/Platform/it.lproj/Localizable.strings +++ b/Platform/it.lproj/Localizable.strings @@ -126,6 +126,18 @@ /* No comment provided by engineer. */ "Are you sure you want to permanently delete this disk image?" = "Sei sicuro di voler eliminare questa immagine definitivamente?"; +/* No comment provided by engineer. */ +"Add a new drive." = "Aggiungi un nuovo disco."; + +/* No comment provided by engineer. */ +"Select an existing disk image." = "Selezione un'immagine disco esistente."; + +/* No comment provided by engineer. */ +"Create an empty drive." = "Crea un disco vuoto."; + +/* No comment provided by engineer. */ +"Add a new device." = "Aggiungi un nuovo dispositivo."; + /* No comment provided by engineer. */ "Are you sure you want to reset this VM? Any unsaved changes will be lost." = "Sei sicuro di voler riavviare questa Macchina Virtuale? Le modifiche non salvate andranno perse."; @@ -141,6 +153,9 @@ /* No comment provided by engineer. */ "Balloon Device" = "Dispositivo Balloon"; +/* No comment provided by engineer. */ +"TPM can be used to protect secrets in the guest operating system. Note that the host will always be able to read these secrets and therefore no expectation of physical security is provided." = "TPM può essere utilizzato per conservare informazioni riservate del sistema. Il dispositivo host sarà sempre in grado di accedere a questi dati."; + /* UTMLegacyQemuConfiguration UTMQemuConstants */ "BIOS" = "BIOS"; @@ -157,6 +172,9 @@ /* No comment provided by engineer. */ "Boot from kernel image" = "Avvia da immagine del kernel"; +/* No comment provided by engineer. */ +"Ubuntu Install Guide" = "Guida all'installazione di Ubuntu"; + /* No comment provided by engineer. */ "Boot Arguments" = "Argomenti di Avvio"; @@ -185,7 +203,7 @@ "Bridged Settings" = "Impostazioni Bridged"; /* No comment provided by engineer. */ -"Bridged Interface" = "Interfaccia Bridget"; +"Bridged Interface" = "Interfaccia Bridged"; /* Welcome view */ "Browse UTM Gallery" = "Scopri la UTM Gallery"; @@ -253,7 +271,7 @@ "Clone" = "Clona"; /* No comment provided by engineer. */ -"Clone selected VM" = "Clona la Macchina Virtuale selezionata"; +"Clone selected VM" = "Clona la VM selezionata"; /* No comment provided by engineer. */ "Clone…" = "Clona..."; @@ -304,7 +322,7 @@ "Create" = "Crea"; /* Welcome view */ -"Create a New Virtual Machine" = "Crea una Nuova Macchina Virtuale (VM)"; +"Create a New Virtual Machine" = "Crea una Nuova Macchina Virtuale"; /* No comment provided by engineer. */ "Create a new VM with the same configuration as this one but without any data." = "Crea una nuova Macchina Virtuale con la stessa configurazione, ma senza alcun dato."; @@ -341,7 +359,10 @@ "Delete Drive" = "Elimina Disco"; /* No comment provided by engineer. */ -"Delete selected VM" = "Elimina la Macchina Virtuale selezionata"; +"Delete this drive." = "Elimina questo disco"; + +/* No comment provided by engineer. */ +"Delete selected VM" = "Elimina la VM selezionata"; /* No comment provided by engineer. */ "Delete…" = "Elimina..."; @@ -364,6 +385,9 @@ /* No comment provided by engineer. */ "DHCP Start" = "Inzio DHCP"; +/* No comment provided by engineer. */ +"DHCP End" = "Fine DHCP"; + /* No comment provided by engineer. */ "Directory" = "Cartella"; @@ -398,13 +422,13 @@ "Disk Image" = "Immagine del Disco"; /* VMDisplayAppleWindowController */ -"Display" = "Monitor"; +"Display" = "Schermo"; /* VMDisplayQemuDisplayController */ -"Display %lld: %@" = "Monitor %1$lld: %2$@"; +"Display %lld: %@" = "Schermo %1$lld: %2$@"; /* VMDisplayQemuDisplayController */ -"Disposable Mode" = "Modo desechable"; +"Disposable Mode" = "Modalità usa e getta"; /* No comment provided by engineer. */ "DNS Search Domains" = "Dominio di Ricerca DNS"; @@ -483,7 +507,7 @@ "Edit…" = "Modifica..."; /* No comment provided by engineer. */ -"Edit selected VM" = "Modifica la Macchina Virtuale selezionata"; +"Edit selected VM" = "Modifica la VM selezionata"; /* VMDrivesSettingsView */ "EFI Variables" = "Variabli EFI"; @@ -521,6 +545,9 @@ /* No comment provided by engineer. */ "Enable Clipboard Sharing" = "Abilita Condivisione Appunti"; +/* No comment provided by engineer. */ +"Requires SPICE guest agent tools to be installed." = "Richiede l'installazione degli strumenti SPICE sul sistema guest."; + /* No comment provided by engineer. */ "Enable Directory Sharing" = "Abilita Condivisione Cartella"; @@ -535,6 +562,8 @@ "Enable Rosetta on Linux (x86_64 Emulation)" = "Abilita Rosetta su Linux (Emulazione x86_64)"; +"If enabled, a virtiofs share tagged 'rosetta' will be available on the Linux guest for installing Rosetta for emulating x86_64 on ARM64." = "Se abilitato, verrà resa disponibile la condivisione virtiofs 'rosetta' al guest Linux, per poter installare Rosetta ed emulare x86_64 su ARM64."; + /* No comment provided by engineer. */ "Enable hardware OpenGL acceleration" = "Abilita l'accelerazione hardware OpenGL"; @@ -568,6 +597,9 @@ /* No comment provided by engineer. */ "Export QEMU Command…" = "Esporta comando QEMU…"; +/* No comment provided by engineer. */ +"Export all arguments as a text file. This is only for debugging purposes as UTM's built-in QEMU differs from upstream QEMU in supported arguments." = "Esporta tutti gli argomenti in un file di testo. Consigliato per il debugging: gli argomenti QEMU utilizzati da UTM potrebbero differire da quelli di QEMU upstream"; + /* Word for decompressing a compressed folder */ "Extracting…" = "Estrazione…"; @@ -638,10 +670,6 @@ /* No comment provided by engineer. */ "Fit To Screen" = "Adatta allo Schermo"; -/* Configuration boot device - UTMQemuConstants */ -"Floppy" = "Dischetto"; - /* No comment provided by engineer. */ "Font" = "Font"; @@ -675,7 +703,7 @@ "Full Graphics" = "Grafica Completa"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB Debug Stub"; @@ -797,11 +825,17 @@ "Import…" = "Importa..."; /* No comment provided by engineer. */ -"Import Drive" = "Importa Disco"; +"Import Drive…" = "Importa Disco..."; /* No comment provided by engineer. */ "Import VHDX Image" = "Importa un'Immagine VHDX"; +/* No comment provided by engineer. */ +"Fetch latest Windows installer…" = "Ottieni l'installer di Windows più recente"; + +/* No comment provided by engineer. */ +"Windows Install Guide" = "Guida all'installazione di Windows"; + /* No comment provided by engineer. */ "Import Virtual Machine…" = "Importa una Macchina Virtuale..."; @@ -823,6 +857,8 @@ /* No comment provided by engineer. */ "Interface" = "Interfaccia"; +"Hardware interface on the guest used to mount this image. Different operating systems support different interfaces. The default will be the most common interface." = "L'interfaccia hardware utilizzata dal sistema guest per montare questa imnmagine. Ciascun sistema operativo può supportare diversi tipi di interfacce. L'interfaccia predefinita sarà quella utilizzata comunemente."; + /* VMDisplayWindowController */ "Install Windows Guest Tools…" = "Installa gli Strumenti Guest per Windows…"; @@ -886,6 +922,9 @@ /* No comment provided by engineer. */ "Invert scrolling" = "Inverti lo scorrimento"; +/* No comment provided by engineer. */ +"If enabled, scroll wheel input will be inverted." = "Se abilitato, lo scorrimento della rotella del mouse verrà invertito"; + /* No comment provided by engineer. */ "IP Configuration" = "Impostazioni IP"; @@ -916,6 +955,78 @@ /* No comment provided by engineer. */ "Keep UTM running after last window is closed and all VMs are shut down" = "Esegui UTM anche quando tutte le fineste sono chiuse e nessuna VM è in esecuzione"; +/* No comment provided by engineer. */ +"Show dock icon" = "Mostra icona nel dock"; + +/* No comment provided by engineer. */ +"Show menu bar icon" = "Mostra icona nella barra dei menu"; + +/* No comment provided by engineer. */ +"Prevent system from sleeping when any VM is running" = "Impedisci lo stop del sistema quando una VM è in esecuzione"; + +/* No comment provided by engineer. */ +"Do not show confirmation when closing a running VM" = "Non mostrare un avviso alla chiusura di una VM in esecuzione"; + +/* No comment provided by engineer. */ +"Closing a VM without properly shutting it down could result in data loss." = "La chiusura di una VM senza il corretto spegnimento potrebbe comportare perdita di dati."; + +/* No comment provided by engineer. */ +"QEMU Graphics Acceleration" = "Accelerazione Grafica QEMU"; + +/* No comment provided by engineer. */ +"By default, the best renderer for this device will be used. You can override this with to always use a specific renderer. This only applies to QEMU VMs with GPU accelerated graphics." = "Il miglior Renderer per questo dispositivo verrà selezionato automaticamente. Puoi sceglierne un altro con questa opzione. Si applica solo alle VM QEMU con accelerazione GPU"; + +/* No comment provided by engineer. */ +"If set, a frame limit can improve smoothness in rendering by preventing stutters when set to the lowest value your device can handle." = "Se impostato, un limite di frame può migliorare l'esperienza quando impostato al valore minimo di refresh del proprio dispositivo."; + +/* No comment provided by engineer. */ +"By default, the best backend for the target will be used. If the selected backend is not available for any reason, an alternative will automatically be selected." = "Il miglior backand disponibile verrà scelto automaticamente. Se il backend selezionato non fosse disponibile, verrà selezionata un'alternativa automaticamente"; + +/* No comment provided by engineer. */ +"FPS Limit" = "Limite FPS"; + +/* No comment provided by engineer. */ +"QEMU Sound" = "Suono QEMU"; + +/* No comment provided by engineer. */ +"Renderer Backend" = "Backend Renderer"; + +/* No comment provided by engineer. */ +"Sound Backend" = "Backend Suono"; + +/* No comment provided by engineer. */ +"Mouse/Keyboard" = "Mouse/Tastiera"; + +/* No comment provided by engineer. */ +"QEMU Pointer" = "Puntatore QEMU"; + +/* No comment provided by engineer. */ +"QEMU Keyboard" = "Tastiera QEMU"; + +/* No comment provided by engineer. */ +"Capture input automatically when entering full screen" = "Cattura automaticamente l'input in modalità schermo intero"; + +/* No comment provided by engineer. */ +"If enabled, input capture will toggle automatically when entering and exiting full screen mode." = "Se abilitato, la cattura dell'input verrà attivata e disattivata all'entrata e all'uscita dalla modalità a schermo intero"; + +/* No comment provided by engineer. */ +"Capture input automatically when window is focused" = "Cattura automaticamente l'input quando la finestra è in primo piano"; + +/* No comment provided by engineer. */ +"If enabled, input capture will toggle automatically when the VM's window is focused." = "Se abilitato, la cattura dell'imput verrà automaticamente attivata o disattivata quando la finestra della VM è in primo piano o non selezionata"; + +/* No comment provided by engineer. */ +"Option (⌥) is Meta key" = "Option (⌥) come Meta key"; + +/* No comment provided by engineer. */ +"If enabled, Option will be mapped to the Meta key which can be useful for emacs. Otherwise, option will work as the system intended (such as for entering international text)." = "Se abilitata, il tasto Option (⌥) verrà mappato come Meta key, utile per emacs. In caso contrario, il tasto Option funzionerà come da impostazioni di sistema (ad esempio per inserimento di testo speciale o con caratteri internazionali)"; + +/* No comment provided by engineer. */ +"Num Lock is forced on" = "Forza attivazione Num Lock"; + +/* No comment provided by engineer. */ +"Sound Backend" = "Mostra icona nella barra dei menu"; + /* No comment provided by engineer. */ "Legacy" = "Legacy"; @@ -986,10 +1097,10 @@ "Manual Serial Device (advanced)" = "Dispositivo Seriale Manuale (avanzate)"; /* No comment provided by engineer. */ -"Maximum Shared USB Devices" = "Numero Massimo di Dispositivi USB Condivisi"; +"Maximum Shared USB Devices" = "Dispositivi USB Condivisi Massimi"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "Memoria"; @@ -1049,13 +1160,13 @@ "Network Mode" = "Modalità di Rete"; /* No comment provided by engineer. */ -"New" = "Nuova"; +"New" = "Crea"; /* No comment provided by engineer. */ -"New…" = "Nuova..."; +"New…" = "Crea..."; /* No comment provided by engineer. */ -"New Drive" = "Nuovo Disco"; +"New Drive…" = "Nuovo Disco..."; /* No comment provided by engineer. */ "New from template…" = "Nuovo da template..."; @@ -1064,7 +1175,7 @@ "New port forward" = "Nuovo Inoltro di Porta"; /* No comment provided by engineer. */ -"New Virtual Machine" = "Nuova Macchina Virtuale (VM)"; +"New Virtual Machine" = "Nuova Macchina Virtuale"; /* No comment provided by engineer. */ "New VM" = "Nuova VM"; @@ -1104,7 +1215,7 @@ /* UTMLegacyQemuConfiguration UTMQemuConstants */ -"None" = "Nessuno"; +"None" = "Nessuno/a"; /* UTMQemuConstants */ "None (Advanced)" = "Nessuno (Avanzato)"; @@ -1146,7 +1257,11 @@ "Optionally select a directory to make accessible inside the VM. Note that support for shared directories varies by the guest operating system and may require additional guest drivers to be installed. See UTM support pages for more details." = "Se vuoi, puoi selezionare una cartella da rendere accessibile all'interno della VM. Il supporto alle cartelle condivise varia in base al sistema operativo e potrebbe essere necessario installare driver sulla VM. Controlla le pagine di supporto di UTM per ulteriori informazioni."; /* No comment provided by engineer. */ +"Skip ISO boot" = "Salta avvio da ISO"; "Other" = "Altro"; +"Options" = "Opzioni"; +"Legacy Hardware" = "Hardware Legacy"; +"If checked, emulated devices with higher compatibility will be instantiated at the cost of performance." = "Se attivo, i dispositivi emulati avranno una compatibilità elevata, al costo di prestazioni peggiori."; /* No comment provided by engineer. */ "Path" = "Percorso"; @@ -1215,7 +1330,7 @@ "QEMU" = "QEMU"; /* No comment provided by engineer. */ -"QEMU Arguments" = "Argomenti di QEMU"; +"QEMU Arguments" = "Argomenti QEMU"; /* UTMQemuVirtualMachine */ "QEMU exited from an error: %@" = "QEMU si è interrotto a causa di un errore: %@"; @@ -1235,6 +1350,9 @@ /* No comment provided by engineer. */ "Quit" = "Esci"; +/* No comment provided by engineer. */ +"Terminate UTM and stop all running VMs." = "Termina UTM e tutte le macchine virtuali in esecuzione."; + /* VMQemuDisplayMetalWindowController */ "Quitting UTM will kill all running VMs." = "L'uscita da UTM causerà l'interruzione di tutte le VM in esecuzione."; @@ -1247,6 +1365,9 @@ /* No comment provided by engineer. */ "Raw Image" = "Immagine Raw"; +/* No comment provided by engineer. */ +"Advanced. If checked, a raw disk image is used. Raw disk image does not support snapshots and will not dynamically expand in size." = "Impostazione Avanzata. Se attivo, verrà utilizzata un'immagine disco raw. Le immagini disco raw non supportano le snapshot e non vengono ridimensionate automaticamente."; + /* VMDisplayAppleController */ "Read Only" = "Sola Lettura"; @@ -1257,7 +1378,19 @@ "Reclaim Space" = "Recupera Spazio"; /* No comment provided by engineer. */ -"Reclaim disk space by re-converting the disk image." = "Recupera spazio su disco ri-convertendo l'immagine disco."; +"Reclaim disk space by re-converting the disk image." = "Recupera spazio su disco riconvertendo l'immagine disco."; + +/* No comment provided by engineer. */ +"Compress" = "Comprimi"; + +/* No comment provided by engineer. */ +"Compress by re-converting the disk image and compressing the data." = "Recupera spazio su disco riconvertendo l'immagine disco e comprimendo i dati."; + +/* No comment provided by engineer. */ +"Resize" = "Ridimensiona"; + +/* No comment provided by engineer. */ +"Increase the size of the disk image." = "Incrementa la dimensione dell'immagine disco."; /* UTMQemuConstants */ "Regular" = "Normale"; @@ -1265,6 +1398,12 @@ /* No comment provided by engineer. */ "Removable" = "Rimovibile"; +/* No comment provided by engineer. */ +"If checked, the drive image will be stored with the VM." = "Se attivo, l'immagine del disco sarà salvata insieme alla VM"; + +/* No comment provided by engineer. */ +"If checked, no drive image will be stored with the VM. Instead you can mount/unmount image while the VM is running." = "Se attivo, l'immagine del disco non sarà salvata insieme alla VM. Può essere, invece, montata e smontata quando la VM è in esecuzione"; + /* No comment provided by engineer. */ "Removable Drive" = "Disco Rimovibile"; @@ -1307,6 +1446,21 @@ /* No comment provided by engineer. */ "Resolution" = "Risoluzione"; +/* No comment provided by engineer. */ +"Width" = "Larghezza"; + +/* No comment provided by engineer. */ +"Height" = "Altezza"; + +/* No comment provided by engineer. */ +"Only available on macOS virtual machines." = "Disponibile solo su macchine virtuali macOS."; + +/* No comment provided by engineer. */ +"Dynamic Resolution" = "Risoluzione Dinamica"; + +/* No comment provided by engineer. */ +"Only available on macOS 14+ virtual machines." = "Disponibile solo su macchine virtuali macOS (versione 14 o superiore)."; + /* No comment provided by engineer. */ "Restart" = "Ricomincia"; @@ -1358,9 +1512,6 @@ /* Save VM overlay */ "Saving %@…" = "Salvataggio di %@..."; -/* No comment provided by engineer. */ -"Scaling" = "Scala"; - /* No comment provided by engineer. */ "Selected:" = "Selezionato:"; @@ -1748,14 +1899,23 @@ "USB Support" = "Supporto USB"; /* No comment provided by engineer. */ -"Use Apple Virtualization" = "Usa la Virtualizzazione di Apple"; +"Use Apple Virtualization" = "Usa la Virtualizzazione Apple"; /* No comment provided by engineer. */ "Use Command+Option (⌘+⌥) for input capture/release" = "Usa Command+Option (⌘+⌥) per catturare/rilasciare l'input"; +/* No comment provided by engineer. */ +"If disabled, the default combination Control+Option (⌃+⌥) will be used." = "Se disabilitato, verrà usata la combinazione di default Control+Option (⌃+⌥)."; + /* No comment provided by engineer. */ "Use Hypervisor" = "Usa Hypervisor"; +/* No comment provided by engineer. */ +"Use TSO" = "Usa TSO"; + +/* No comment provided by engineer. */ +"Only available when Hypervisor is used on supported hardware. TSO speeds up Intel emulation in the guest at the cost of decreased performance in general." = "Disponibile solo con un Hypervisor e hardware supportato. TSO incrementa le prestazioni di emulazione di un guest Intel, al costo di performance generali ridotte."; + /* No comment provided by engineer. */ "Use local time for base clock" = "Usa l'ora locale locale per l'orologio di sistema"; @@ -1779,7 +1939,7 @@ "Virtual Machine" = "Macchina Virtuale"; /* No comment provided by engineer. */ -"Virtual Machine Gallery" = "Libreria di VM (UTM Gallery)"; +"Virtual Machine Gallery" = "Libreria di Macchine Virtuali"; /* New VM window. */ "Virtualization Engine" = "Motore di Virtualizzazione"; @@ -1851,5 +2011,98 @@ /* No comment provided by engineer. */ "Options here only apply on next boot and are not saved." = "Queste impostazioni vengo applicate solo all'avvio successivo e non sono salvate."; +/* No comment provided by engineer. */ +"You can use this if your boot options are corrupted or if you wish to re-enroll in the default keys for secure boot." = "Puoi usare questa opzione se le impostazioni di boot sono compromesse o se desideri ricreare le chiavi di default per il secure boot."; + /* No comment provided by engineer. */ "Create a new VM" = "Crea una nuova VM"; + +/* No comment provided by engineer. */ +"Show UTM" = "Mostra UTM"; + +/* No comment provided by engineer. */ +"Show the main window." = "Mostra la finestra principale."; + +/* No comment provided by engineer. */ +"Hide dock icon on next launch" = "Nascondi icona del dock al prossimo avvio"; + +/* No comment provided by engineer. */ +"No virtual machines found." = "Nessuna macchina virtuale trovata."; + +/* No comment provided by engineer. */ +"Requires restarting UTM to take affect." = "Richiede il riavvio di UTM."; + +/* No comment provided by engineer. */ +"Choose" = "Seleziona"; + +/* No comment provided by engineer. */ +"Arguments" = "Argomenti"; + +/* No comment provided by engineer. */ +"Guest drivers are required for 3D acceleration." = "L'installazione dei driver guest è richiesta per l'accelerazione 3D."; + +/* No comment provided by engineer. */ +"GPU Acceleration supported" = "Accelerazione GPU Supportata"; + +/* No comment provided by engineer. */ +"Automatic" = "Automatica"; + +/* No comment provided by engineer. */ +"Read Only?" = "Sola Lettura"; + +/* No comment provided by engineer. */ +"Update Interface" = "Aggiorna Interfaccia"; + +/* No comment provided by engineer. */ +"Older versions of UTM added each IDE device to a separate bus. Check this to change the configuration to place two units on each bus." = "Le versioni precedenti di UTM aggiungevano ciascun dispositivo IDE ad un bus separato. Seleziona questa opzione per modificare la configurazione e impostare due unità su ciascun bus."; + +/* No comment provided by engineer. */ +"What's New" = "Novità di UTM"; + +/* No comment provided by engineer. */ +"Enable UTM Server" = "Abilita UTM Server"; + +/* No comment provided by engineer. */ +"Last Seen" = "Accesso più recente"; + +/* No comment provided by engineer. */ +"Reset Identity" = "Ripristina Identità"; + +/* No comment provided by engineer. */ +"Do you want to forget all clients and generate a new server identity? Any clients that previously paired with this server will be instructed to manually unpair with this server before they can connect again." = "Vuoi dimenticare tutti i client e generare una nuova identità del server? Verrà chiesto a tutti i client collegati di disassociarsi manualmente prima di poter stabile una nuova connessione."; + +/* No comment provided by engineer. */ +"Reset Identity" = "Ripristina Identità"; + +/* No comment provided by engineer. */ +"Startup" = "Avvio"; + +/* No comment provided by engineer. */ +"Automatically start UTM server" = "Avvia il Server UTM automaticamente"; + +/* No comment provided by engineer. */ +"Reject unknown connections by default" = "Rifiuta connessioni sconosciute"; + +/* No comment provided by engineer. */ +"If checked, you will not be prompted about any unknown connection and they will be rejected." = "Se attivo, non ti verrà chiesto di accettare richieste da origini sconosciute, che verranno automaticamente rifiutate."; + +/* No comment provided by engineer. */ +"Allow access from external clients" = "Permetti l'accesso da client esterni"; + +/* No comment provided by engineer. */ +"By default, the server is only available on LAN but setting this will use UPnP/NAT-PMP to port forward to WAN." = "Di default, il server è solo disponibile su rete locale, attivando questa opzione verrà attivato UPnP/NAT-PMP per il port forwarding su WAN"; + +/* No comment provided by engineer. */ +"Any" = "Qualunque"; + +/* No comment provided by engineer. */ +"Specify a port number to listen on. This is required if external clients are permitted." = "Specifica una porta d'ascolto. Richiesto se l'accesso da client esterni è attivato."; + +/* No comment provided by engineer. */ +"Authentication" = "Autenticazione"; + +/* No comment provided by engineer. */ +"Require Password" = "Richiedi Password"; + +/* No comment provided by engineer. */ +"If enabled, clients must enter a password. This is required if you want to access the server externally." = "Se abilitato, i client dovranno inserire una password. Richiesto se vuoi accedere da client esterni."; diff --git a/Platform/ja.lproj/Localizable.strings b/Platform/ja.lproj/Localizable.strings index 96d9abf03..2a264df13 100644 --- a/Platform/ja.lproj/Localizable.strings +++ b/Platform/ja.lproj/Localizable.strings @@ -136,6 +136,25 @@ // UTMDataExtension.swift "This virtual machine is already running. In order to run it from this device, you must stop it first." = "この仮想マシンはすでに実行されています。このデバイスから実行するには、まず停止する必要があります。"; +// UTMDonateView.swift +"Your support is the driving force that helps UTM stay independent. Your contribution, no matter the size, makes a significant difference. It enables us to develop new features and maintain existing ones. Thank you for considering a donation to support us." = "皆様のご支援がUTMの独立を維持する原動力となります。皆様からのご支援は、その大小にかかわらず、大きな違いをもたらします。これにより、新しい機能を開発し、既存の機能を維持することができます。ぜひご寄付をご検討のほどよろしくお願いいたします。"; +"GitHub Sponsors" = "GitHub Sponsors"; +"Support UTM" = "UTMを支援"; +"One Time Donation" = "1回限りの寄付"; +"Recurring Donation" = "定期寄付"; +"Manage Subscriptions…" = "サブスクリプションを管理…"; +"Restore Purchases" = "購入を復元"; +"%d days" = "%d日間"; +"day" = "1日間"; +"%d weeks" = "%d週間"; +"week" = "1週間"; +"%d months" = "%dか月間"; +"month" = "1か月間"; +"%d years" = "%d年間"; +"year" = "1年間"; +"period" = "期間"; +"Your purchase could not be verified by the App Store." = "App Storeで購入が確認できませんでした。"; + // UTMSingleWindowView.swift "Waiting for VM to connect to display..." = "仮想マシンがディスプレイに接続するのを待機中…"; @@ -266,7 +285,7 @@ "Serial %lld" = "シリアル%lld"; // Display/VMDisplayAppleDisplayWindowController.swift -"%@ (Terminal %lld)" = "%1$@ (ターミナル%2$lld)"; +"%@ (Terminal %lld)" = "%1$@(ターミナル%2$lld)"; // Display/VMDisplayQemuDisplayController.swift "Disposable Mode" = "使い捨てモード"; @@ -324,6 +343,8 @@ "Mouse/Keyboard" = "マウス/キーボード"; "Capture input automatically when entering full screen" = "フルスクリーンになったときに入力を自動的にキャプチャ"; "If enabled, input capture will toggle automatically when entering and exiting full screen mode." = "有効にすると、フルスクリーンモードの開始時と解除時に入力キャプチャが自動的に切り替わります。"; +"Capture input automatically when window is focused" = "ウインドウがフォーカスされたときに入力を自動的にキャプチャ"; +"If enabled, input capture will toggle automatically when the VM's window is focused." = "有効にすると、仮想マシンのウインドウがフォーカスされたときに入力キャプチャが自動的に切り替わります。"; "Console" = "コンソール"; "Option (⌥) is Meta key" = "メタキーとしてOption(⌥)を使用"; "If enabled, Option will be mapped to the Meta key which can be useful for emacs. Otherwise, option will work as the system intended (such as for entering international text)." = "有効にすると、OptionがEmacsの利用に便利なメタキーにマッピングされます。そうでない場合、Optionはシステムが意図したとおりに動作します(国際テキストを入力する場合など)。"; @@ -419,6 +440,8 @@ // VMConfigAppleDriveCreateView.swift "Removable" = "リムーバブル"; "If checked, the drive image will be stored with the VM." = "チェックを入れると、ドライブイメージが仮想マシンに保存されます。"; +"Use NVMe Interface" = "NVMeインターフェイスを使用"; +"If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors." = "チェックを入れると、Virtioの代わりにNVMeをディスクインターフェイスとして使用します。これは、macOS 14以降のLinuxゲストでのみ使用できます。このインターフェイスは低速ですが、ファイルシステムエラーが発生する可能性が低くなります。"; // VMConfigAppleDriveDetailsView.swift "Removable Drive" = "リムーバブルドライブ"; @@ -472,6 +495,8 @@ // VMSessionState.swift "Connection to the server was lost." = "サーバへの接続が切断されました。"; +"Background task is about to expire" = "バックグラウンドタスクがまもなく期限切れになります"; +"Switch back to UTM to avoid termination." = "終了を防ぐには、UTMに戻ってください。"; // VMConfigQEMUArgumentsView.swift "Arguments" = "引数"; @@ -541,11 +566,11 @@ // RAMSlider.swift "Size" = "サイズ"; -"MB" = "MB"; +"MiB" = "MiB"; // SizeTextField.swift "The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "このイメージに割り当てるストレージ領域です。イメージを読み込む場合は無視されます。生イメージの場合、このサイズの空のファイルが仮想マシンに保存されます。そうでない場合、ディスクイメージはこのサイズまで動的に拡張されます。"; -"GB" = "GB"; +"GiB" = "GiB"; // VMCardView.swift "Run" = "実行"; @@ -616,6 +641,71 @@ "Generic" = "一般"; "Notes" = "メモ"; "Icon" = "アイコン"; +"Choose" = "選択"; +"AIX" = "AIX"; +"iOS" = "iOS"; +"Windows 7" = "Windows 7"; +"AlmaLinux" = "AlmaLinux"; +"Alpine" = "Alpine"; +"AmigaOS" = "AmigaOS"; +"Android" = "Android"; +"Apple TV" = "Apple TV"; +"Arch Linux" = "Arch Linux"; +"BackTrack" = "BackTrack"; +"Bada" = "Bada"; +"BeOS" = "BeOS"; +"CentOS" = "CentOS"; +"Chrome OS" = "Chrome OS"; +"CyanogenMod" = "CyanogenMod"; +"Debian" = "Debian"; +"Elementary OS" = "elementary OS"; +"Fedora" = "Fedora"; +"Firefox OS" = "Firefox OS"; +"FreeBSD" = "FreeBSD"; +"Gentoo" = "Gentoo"; +"Haiku OS" = "Haiku OS"; +"HP-UX" = "HP-UX"; +"KaiOS" = "KaiOS"; +"Knoppix" = "Knoppix"; +"Kubuntu" = "Kubuntu"; +"Linux" = "Linux"; +"Lubuntu" = "Lubuntu"; +"macOS" = "macOS"; +"Maemo" = "Maemo"; +"Mandriva" = "Mandriva"; +"MeeGo" = "MeeGo"; +"Linux Mint" = "Linux Mint"; +"NetBSD" = "NetBSD"; +"Nintendo" = "任天堂"; +"NixOS" = "NixOS"; +"OpenBSD" = "OpenBSD"; +"OpenWrt" = "OpenWrt"; +"OS/2" = "OS/2"; +"Palm OS" = "Palm OS"; +"PlayStation Portable" = "PlayStation Portable"; +"PlayStation" = "PlayStation"; +"Pop!_OS" = "Pop!_OS"; +"Red Hat" = "Red Hat"; +"Remix OS" = "Remix OS"; +"RISC OS" = "RISC OS"; +"Sabayon" = "Sabayon"; +"Sailfish OS" = "Sailfish OS"; +"Slackware" = "Slackware"; +"Solaris" = "Solaris"; +"openSUSE" = "openSUSE"; +"Syllable" = "Syllable"; +"Symbian" = "Symbian"; +"ThreadX" = "ThreadX"; +"Tizen" = "Tizen"; +"Ubuntu" = "Ubuntu"; +"webOS" = "webOS"; +"Windows 11" = "Windows 11"; +"Windows 9x" = "Windows 9x系"; +"Windows XP" = "Windows XP"; +"Windows" = "Windows"; +"Xbox" = "Xbox"; +"Xubuntu" = "Xubuntu"; +"YunOS" = "YunOS"; // VMConfigInputView.swift "If enabled, the default input devices will be emulated on the USB bus." = "有効にすると、デフォルトの入力デバイスがUSBバス上でエミュレートされます。"; @@ -756,6 +846,7 @@ "Inactive" = "非アクティブ"; // VMNavigationListView.swift +"Donate" = "寄付"; "Pending" = "保留中"; "New VM" = "新規仮想マシン"; "Create a new VM" = "新規仮想マシンを作成します"; @@ -772,8 +863,8 @@ "%@ %@" = "%1$@ %2$@"; // VMSettingsAddDeviceMenuView.swift -"Import Drive" = "ドライブを読み込む"; -"New Drive" = "新規ドライブ"; +"Import Drive…" = "ドライブを読み込む…"; +"New Drive…" = "新規ドライブ…"; // VMToolbarModifier.swift "Remove selected shortcut" = "選択したショートカットを削除します"; @@ -827,8 +918,13 @@ // VMWizardOSOtherView.swift "Other" = "その他"; -"Skip ISO boot" = "ISO起動をスキップ"; -"Advanced" = "詳細"; +"Boot Device" = "起動デバイス"; +"CD/DVD Image" = "CD/DVDイメージ"; +"Floppy Image" = "フロッピーイメージ"; +"Boot IMG Image" = "起動IMGイメージ"; +"Legacy Hardware" = "レガシーハードウェア"; +"If checked, emulated devices with higher compatibility will be instantiated at the cost of performance." = "チェックを入れると、パフォーマンスを犠牲にして、より互換性の高い仮想デバイスがインスタンス化されます。"; +"Options" = "オプション"; // VMWizardOSView.swift "macOS 12+" = "macOS 12以降"; @@ -863,6 +959,10 @@ "Download prebuilt from UTM Gallery…" = "UTMギャラリーからビルド済みパッケージをダウンロード…"; "Existing" = "既存"; +// VMWizardStartViewTCI.swift +"New Machine" = "新規マシン"; +"Create a new emulated machine from scratch." = "新規仮想マシンをゼロから作成します。"; + // VMWizardState.swift "Please select a boot image." = "起動イメージを選択してください。"; "Please select a kernel file." = "カーネルファイルを選択してください。"; @@ -982,7 +1082,7 @@ "Remote Client Connected" = "リモートクライアント接続済み"; "Established connection from %@." = "%@からの接続を確立しました。"; "UTM Remote Server Error" = "UTMリモートサーバエラー"; -"Cannot reserve port %d for external access from NAT. Make sure no other device on the network has reserved it." = "NATからの外部アクセス用のポート“%d”を予約できません。ネットワーク上のほかのデバイスが予約していないことを確認してください。"; +"Cannot reserve port %d for external access from NAT. Make sure no other device on the network has reserved it." = "NATからの外部アクセス用のポート%dを予約できません。ネットワーク上のほかのデバイスが予約していないことを確認してください。"; "Not authenticated." = "認証されていません。"; "The client interface version does not match the server." = "クライアントインターフェイスのバージョンがサーバと一致しません。"; "Cannot find VM with ID: %@" = "指定されたIDの仮想マシンが見つかりません: %@"; diff --git a/Platform/ko.lproj/Localizable.strings b/Platform/ko.lproj/Localizable.strings index f62f6508d..b33febc92 100644 --- a/Platform/ko.lproj/Localizable.strings +++ b/Platform/ko.lproj/Localizable.strings @@ -398,7 +398,7 @@ "Manager being deallocated, killing pending RPC." = "Manager being deallocated, killing pending RPC."; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "메모리"; diff --git a/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift b/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift index 656e03c3a..9c346b992 100644 --- a/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift +++ b/Platform/macOS/Display/VMDisplayQemuMetalWindowController.swift @@ -62,6 +62,7 @@ class VMDisplayQemuMetalWindowController: VMDisplayQemuWindowController { @Setting("NoCursorCaptureAlert") private var isCursorCaptureAlertShown: Bool = false @Setting("NoFullscreenCursorCaptureAlert") private var isFullscreenCursorCaptureAlertShown: Bool = false @Setting("FullScreenAutoCapture") private var isFullScreenAutoCapture: Bool = false + @Setting("WindowFocusAutoCapture") private var isWindowFocusAutoCapture: Bool = false @Setting("CtrlRightClick") private var isCtrlRightClick: Bool = false @Setting("AlternativeCaptureKey") private var isAlternativeCaptureKey: Bool = false @Setting("IsCapsLockKey") private var isCapsLockKey: Bool = false @@ -149,6 +150,9 @@ class VMDisplayQemuMetalWindowController: VMDisplayQemuWindowController { } super.enterLive() resizeConsoleToolbarItem.isEnabled = false // disable item + if isWindowFocusAutoCapture { + captureMouse() + } } override func enterSuspended(isBusy busy: Bool) { @@ -403,7 +407,6 @@ extension VMDisplayQemuMetalWindowController { func windowDidEnterFullScreen(_ notification: Notification) { isFullScreen = true if isFullScreenAutoCapture { - captureMouseToolbarButton.state = .on captureMouse() } } @@ -411,11 +414,32 @@ extension VMDisplayQemuMetalWindowController { func windowDidExitFullScreen(_ notification: Notification) { isFullScreen = false if isFullScreenAutoCapture { - captureMouseToolbarButton.state = .off releaseMouse() } } + func windowDidBecomeMain(_ notification: Notification) { + // Do not capture mouse if user did not clicked inside the metalView because the window will be draged if user hold the mouse button. + guard let window = window, + window.mouseLocationOutsideOfEventStream.y < metalView.frame.height, + captureMouseToolbarButton.state == .off, + isWindowFocusAutoCapture else { + return + } + captureMouse() + } + + func windowDidResignMain(_ notification: Notification) { + releaseMouse() + } + + override func windowDidBecomeKey(_ notification: Notification) { + if isFullScreen && isFullScreenAutoCapture { + captureMouse() + } + super.windowDidBecomeKey(notification) + } + override func windowDidResignKey(_ notification: Notification) { releaseMouse() super.windowDidResignKey(notification) diff --git a/Platform/macOS/Display/VMDisplayWindowController.swift b/Platform/macOS/Display/VMDisplayWindowController.swift index 7686b008e..5cc3304cb 100644 --- a/Platform/macOS/Display/VMDisplayWindowController.swift +++ b/Platform/macOS/Display/VMDisplayWindowController.swift @@ -210,6 +210,7 @@ class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate { @MainActor func showErrorAlert(_ message: String, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil) { + window?.resignKey() let alert = NSAlert() alert.alertStyle = .critical alert.messageText = NSLocalizedString("Error", comment: "VMDisplayWindowController") @@ -219,6 +220,7 @@ class VMDisplayWindowController: NSWindowController, UTMVirtualMachineDelegate { @MainActor func showConfirmAlert(_ message: String, confirmHandler handler: (() -> Void)? = nil) { + window?.resignKey() let alert = NSAlert() alert.alertStyle = .informational alert.messageText = NSLocalizedString("Confirmation", comment: "VMDisplayWindowController") diff --git a/Platform/macOS/SettingsView.swift b/Platform/macOS/SettingsView.swift index 625cc7855..9225a95ea 100644 --- a/Platform/macOS/SettingsView.swift +++ b/Platform/macOS/SettingsView.swift @@ -129,6 +129,7 @@ struct SoundSettingsView: View { struct InputSettingsView: View { @AppStorage("FullScreenAutoCapture") var isFullScreenAutoCapture = false + @AppStorage("WindowFocusAutoCapture") var isWindowFocusAutoCapture = false @AppStorage("OptionAsMetaKey") var isOptionAsMetaKey = false @AppStorage("CtrlRightClick") var isCtrlRightClick = false @AppStorage("AlternativeCaptureKey") var isAlternativeCaptureKey = false @@ -143,6 +144,9 @@ struct InputSettingsView: View { Toggle(isOn: $isFullScreenAutoCapture) { Text("Capture input automatically when entering full screen") }.help("If enabled, input capture will toggle automatically when entering and exiting full screen mode.") + Toggle(isOn: $isWindowFocusAutoCapture) { + Text("Capture input automatically when window is focused") + }.help("If enabled, input capture will toggle automatically when the VM's window is focused.") } Section(header: Text("Console")) { @@ -219,9 +223,12 @@ struct ServerSettingsView: View { .multilineTextAlignment(.trailing) .help("Specify a port number to listen on. This is required if external clients are permitted.") .onChange(of: serverPort) { newValue in - if serverPort == 0 { + if newValue == 0 { isServerExternal = false } + if newValue < 0 || newValue >= UInt16.max { + serverPort = defaultPort + } } } Section(header: Text("Authentication")) { diff --git a/Platform/macOS/VMConfigAppleDriveCreateView.swift b/Platform/macOS/VMConfigAppleDriveCreateView.swift index 5d5244c74..74806d66b 100644 --- a/Platform/macOS/VMConfigAppleDriveCreateView.swift +++ b/Platform/macOS/VMConfigAppleDriveCreateView.swift @@ -33,11 +33,17 @@ struct VMConfigAppleDriveCreateView: View { if newValue { config.sizeMib = 0 config.isReadOnly = true + config.isNvme = false } else { config.sizeMib = 10240 config.isReadOnly = false } } + if #available(macOS 14, *), !config.isExternal { + Toggle(isOn: $config.isNvme.animation(), label: { + Text("Use NVMe Interface") + }).help("If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors.") + } if !config.isExternal { SizeTextField($config.sizeMib) } diff --git a/Platform/macOS/VMConfigAppleDriveDetailsView.swift b/Platform/macOS/VMConfigAppleDriveDetailsView.swift index b88cd0415..d96cfbbe1 100644 --- a/Platform/macOS/VMConfigAppleDriveDetailsView.swift +++ b/Platform/macOS/VMConfigAppleDriveDetailsView.swift @@ -28,6 +28,12 @@ struct VMConfigAppleDriveDetailsView: View { TextField("Name", text: .constant(config.imageURL?.lastPathComponent ?? NSLocalizedString("(New Drive)", comment: "VMConfigAppleDriveDetailsView"))) .disabled(true) Toggle("Read Only?", isOn: $config.isReadOnly) + if #available(macOS 14, *), !config.isExternal { + Toggle(isOn: $config.isNvme, + label: { + Text("Use NVMe Interface") + }).help("If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors.") + } if #unavailable(macOS 12) { Button { requestDriveDelete = config diff --git a/Platform/pl.lproj/Localizable.strings b/Platform/pl.lproj/Localizable.strings index 45fb3d35d..a97aa4fa0 100644 --- a/Platform/pl.lproj/Localizable.strings +++ b/Platform/pl.lproj/Localizable.strings @@ -151,11 +151,11 @@ "Delete" = "Usuń"; "Discovered" = "Odnalezione"; "Make sure the latest version of UTM is running on your Mac and UTM Server is enabled. You can download UTM from the Mac App Store." = "Upewnij się, że na twoim Macu zainstalowana jest najnowsza wersja UTM oraz Serwer UTM jest włączony. Możesz pobrać UTM z App Store dla twojego Maca."; -"Name (optional)" = "名前(オプション)"; +"Name (optional)" = "Nazwa (opcjonalne)"; "Hostname or IP address" = "Nazwa hosta lub adres IP"; "Port" = "Port"; "Host" = "Host"; -"Fingerprint" = "Fingerprint"; +"Fingerprint" = "Odcisk palca"; "Password" = "Hasło"; "Save Password" = "Zapisz hasło"; "Close" = "Zamknij"; @@ -345,13 +345,14 @@ "If enabled, num lock will always be on to the guest. Note this may make your keyboard's num lock indicator out of sync." = "Jeśli włączone, NumLock zawsze będzie włączony w systemie gościa. Pamiętaj, że to może sprawić mylne działanie detektora Num Locka na twojej klawiaturze."; "QEMU USB" = "QEMU USB"; "Do not show prompt when USB device is plugged in" = "Nie pokazuj powiadomienia gdy urządzenie USB zostanie podłączone"; -"Startup" = "起動時"; +"Startup" = "Uruchomienie"; "Automatically start UTM server" = "Automatycznie uruchamiaj serwer UTM"; "Reject unknown connections by default" = "Domyślnie odrzucaj nieznane połączenia"; "If checked, you will not be prompted about any unknown connection and they will be rejected." = "Jeśli zaznaczone, nie będziesz informowany o żadnych nieznanych połączeniach, a one będą odrzucane."; "Allow access from external clients" = "Pozwalaj na dostęp dla klientów zewnętrznych"; -"By default, the server is only available on LAN but setting this will use UPnP/NAT-PMP to port forward to WAN." = "デフォルトでは、サーバはLAN上でのみ利用可能ですが、これを設定すると、UPnP/NAT-PMPを使用してWANにポート転送します。"; -"Specify a port number to listen on. This is required if external clients are permitted." = "外部からの接続を受け入れるポート番号を指定します。これは、外部クライアントが許可されている場合に必要です。"; +"By default, the server is only available on LAN but setting this will use UPnP/NAT-PMP to port forward to WAN." = "Domyślnie serwer jest dostępny tylko w sieci LAN, ale ustawienie to spowoduje użycie UPnP/NAT-PMP do przekierowania portów do sieci WAN."; +"Specify a port number to listen on. This is required if external clients are permitted." = "Określ numer portu, na którym chcesz nasłuchiwać. Jest to wymagane, jeśli klienci zewnętrzni są dopuszczeni."; +"Any" = "Ktokolwiek"; "Authentication" = "Uwierzytelnianie"; "Require Password" = "Wymagaj hasła"; "If enabled, clients must enter a password. This is required if you want to access the server externally." = "Jeśli włączone, klienci muszą wprowadzić hasło. Jest to wymagane jeśli chcesz uzyskać dostęp do serwera zewnętrznie."; @@ -421,8 +422,8 @@ "If checked, the drive image will be stored with the VM." = "Jeśli zaznaczone, obraz dysku będzie przechowywany wraz z wirtualną maszyną."; "Size" = "Rozmiar"; "The amount of storage to allocate for this image. An empty file of this size will be stored with the VM." = "Ilość pamięci masowej przydzielonej dla tego obrazu. Pusty plik takiego rozmiaru będzie przechowywany wraz z maszyną wirtualną"; -"GB" = "GB"; -"MB" = "MB"; +"GiB" = "GiB"; +"MiB" = "MiB"; /* VMConfigAppleDriveDetailsView.swift */ "Name" = "Nazwa"; @@ -546,14 +547,14 @@ /* RAMSlider.swift */ "Size" = "Rozmiar"; -"MB" = "MB"; +"MiB" = "MiB"; /* FileBrowseField.swift */ "Path" = "Ścieżka"; /* SizeTextField.swift */ "The amount of storage to allocate for this image. Ignored if importing an image. If this is a raw image, then an empty file of this size will be stored with the VM. Otherwise, the disk image will dynamically expand up to this size." = "Ilość pamięci masowej do przydzielenia dla tego obrazu. Ignorowane, jeśli importujesz obraz. Jeśli jest to surowy obraz, wtedy plusty plik tego rozmiaru będzie przechowywany wraz z wirtualną maszyną. W przeciwnym wypadku, obraz dysku będzie dynamicznie się rozszerzał do docelowego rozmiaru."; -"GB" = "GB"; +"GiB" = "GiB"; /* VMCardView.swift */ "Run" = "Uruchom"; @@ -786,8 +787,8 @@ "%@ %@" = "%@ %@"; /* VMSettingsAddDeviceMenuView.swift */ -"Import Drive" = "Importuj dysk"; -"New Drive" = "Nowy dysk"; +"Import Drive…" = "Importuj dysk..."; +"New Drive…" = "Nowy dysk..."; /* VMToolbarModifier.swift */ "Remove selected shortcut" = "Uruchom wybrany skrót"; @@ -993,11 +994,11 @@ "New unknown remote client connection." = "Nowe nieznane zdalne połączenie."; "New trusted remote client connection." = "Nowe zaufane zdalne połączenie"; "Unknown Remote Client" = "Nieznany klient połączenia zdalnego"; -"A client with fingerprint '%@' is attempting to connect." = "Kilent z odciskiem '%@' próbuje się połączyć."; +"A client with fingerprint '%@' is attempting to connect." = "Kilent z odciskiem palca '%@' próbuje się połączyć."; "Remote Client Connected" = "Połączono ze zdalnym klientem"; "Established connection from %@." = "Próba nawiązenia połączenia od '%@'."; -"UTM Remote Server Error" = "UTMリモートサーバエラー"; -"Cannot reserve port %d for external access from NAT. Make sure no other device on the network has reserved it." = "NATからの外部アクセス用のポート“%d”を予約できません。ネットワーク上のほかのデバイスが予約していないことを確認してください。"; +"UTM Remote Server Error" = "Błąd serwera zdalnego UTM"; +"Cannot reserve port '%@' for external access from NAT. Make sure no other device on the network has reserved it." = "Nie można zarezerwować portu '%@' dla dostępu zewnętrznego z NAT. Upewnij się, że żadne inne urządzenie w sieci go nie zarezerwowało."; "Not authenticated." = "Nieuwiezytelniony"; "The client interface version does not match the server." = "Wersja interfejsu klienta nie zgadza się z wersją interfejsu serwera."; "Cannot find VM with ID: %@" = "Nie udało się znaleźć maszyny wirtualnej o danym identyfikatorze: %@"; @@ -1291,4 +1292,4 @@ "Floppy Drive" = "Napęd dyskietek"; "VirtIO Drive" = "Dysk VirtIO"; "NVMe Drive" = "Dysk NVMe"; -"USB Drive" = "Dysk USB"; \ No newline at end of file +"USB Drive" = "Dysk USB"; diff --git a/Platform/ru.lproj/Localizable.strings b/Platform/ru.lproj/Localizable.strings index f6f8eff9e..fe87d1ab1 100644 --- a/Platform/ru.lproj/Localizable.strings +++ b/Platform/ru.lproj/Localizable.strings @@ -244,7 +244,7 @@ "Change…" = "Изменить…"; /* No comment provided by engineer. */ -"Clear" = "Отчистить"; +"Clear" = "Очистить"; /* No comment provided by engineer. */ "Clipboard Sharing" = "Обмен буфером обмена"; @@ -681,7 +681,7 @@ "Full Graphics" = "Полная графика"; /* No comment provided by engineer. */ -"GB" = "ГБ"; +"GiB" = "ГиБ"; /* UTMQemuConstants */ "GDB Debug Stub" = "Плагин отладки GDB"; @@ -809,7 +809,7 @@ "Import…" = "Импорт…"; /* No comment provided by engineer. */ -"Import Drive" = "Импортировать диск"; +"Import Drive…" = "Импортировать диск…"; /* No comment provided by engineer. */ "Import VHDX Image" = "Импортировать образ VHDX"; @@ -1004,7 +1004,7 @@ "Maximum Shared USB Devices" = "Максимальное кол-во совместно используемых USB-устройств"; /* No comment provided by engineer. */ -"MB" = "МБ"; +"MiB" = "МиБ"; /* No comment provided by engineer. */ "Memory" = "Память"; @@ -1070,7 +1070,7 @@ "New…" = "Новый…"; /* No comment provided by engineer. */ -"New Drive" = "Новый диск"; +"New Drive…" = "Новый диск…"; /* No comment provided by engineer. */ "New from template…" = "Новая из шаблона…"; diff --git a/Platform/zh-HK.lproj/Localizable.strings b/Platform/zh-HK.lproj/Localizable.strings index 2b4483ced..83ac4577c 100644 --- a/Platform/zh-HK.lproj/Localizable.strings +++ b/Platform/zh-HK.lproj/Localizable.strings @@ -44,6 +44,18 @@ /* Format string for download progress and speed, e. g. 5 MB of 6 GB (200 kbit/s) */ "%1$@ of %2$@ (%3$@)" = "%1$@ / %2$@ (%3$@)"; +/* UTMDonateView */ +"%d days" = "%d 日"; + +/* UTMDonateView */ +"%d months" = "%d 月"; + +/* UTMDonateView */ +"%d weeks" = "%d 周"; + +/* UTMDonateView */ +"%d years" = "%d 年"; + /* UTMScriptingAppDelegate */ "A valid backend must be specified." = "必須指定有效的後端。"; @@ -57,14 +69,11 @@ "Add…" = "新增⋯"; /* No comment provided by engineer. */ -"Additional Options" = "附加項目"; +"Additional Options" = "附加選項"; /* No comment provided by engineer. */ "Additional Settings" = "附加設定"; -/* No comment provided by engineer. */ -"Advanced" = "進階"; - /* VMConfigSystemView */ "Allocating too much memory will crash the VM." = "分配過多記憶體會使虛擬電腦當機。"; @@ -122,6 +131,9 @@ /* UTMQemuConstants */ "Automatic Serial Device (max 4)" = "自動序列裝置 (最大值為 4)"; +/* VMSessionState */ +"Background task is about to expire" = "背景任務即將過期"; + /* UTMLegacyQemuConfiguration UTMQemuConstants */ "BIOS" = "BIOS"; @@ -144,6 +156,9 @@ /* No comment provided by engineer. */ "Boot Image Type" = "啟動映像檔種類"; +/* No comment provided by engineer. */ +"Boot IMG Image" = "啟動 IMG 映像檔"; + /* No comment provided by engineer. */ "Boot ISO Image" = "啟動 ISO 映像檔"; @@ -208,6 +223,9 @@ /* No comment provided by engineer. */ "Capture input automatically when entering full screen" = "進入全螢幕時自動擷取輸入"; +/* No comment provided by engineer. */ +"Capture input automatically when window is focused" = "於視窗聚焦時自動擷取輸入"; + /* VMDisplayQemuMetalWindowController */ "Captured mouse" = "已擷取滑鼠"; @@ -218,12 +236,18 @@ UTMQemuConstants */ "CD/DVD (ISO) Image" = "CD/DVD (ISO) 映像檔"; +/* No comment provided by engineer. */ +"CD/DVD Image" = "CD/DVD 映像檔"; + /* VMDisplayWindowController */ "Change" = "變更"; /* VMDisplayAppleController */ "Change…" = "變更⋯"; +/* No comment provided by engineer. */ +"Choose" = "選取"; + /* No comment provided by engineer. */ "Clear" = "清除"; @@ -276,6 +300,9 @@ /* No comment provided by engineer. */ "Create" = "製作"; +/* No comment provided by engineer. */ +"Create a new emulated machine from scratch." = "從頭開始製作一個新的虛擬電腦。"; + /* Welcome view */ "Create a New Virtual Machine" = "製作一個新虛擬電腦"; @@ -285,6 +312,9 @@ /* UTMSWTPM */ "Data not specified." = "未指定資料。"; +/* UTMDonateView */ +"day" = "日"; + /* No comment provided by engineer. */ "Debug Logging" = "除錯記錄"; @@ -327,7 +357,7 @@ "Disposable Mode" = "即拋式模式"; /* No comment provided by engineer. */ -"Do not save VM screenshot to disk" = "不要將虛擬電腦快照儲存至磁碟"; +"Do not save VM screenshot to disk" = "不要儲存虛擬電腦的螢幕截圖至磁碟"; /* No comment provided by engineer. */ "Do not show confirmation when closing a running VM" = "關閉正在執行的虛擬電腦時不要顯示確認"; @@ -461,9 +491,6 @@ /* UTMQemuConfigurationError */ "Failed to migrate configuration from a previous UTM version." = "無法由之前版本的 UTM 轉移設定。"; -/* UTMData */ -"Failed to parse download URL." = "無法解析已經下載的 URL。"; - /* UTMRemoteKeyManager */ "Failed to parse generated key pair." = "無法解析生成的密鑰對。"; @@ -496,6 +523,9 @@ UTMQemuConstants */ "Floppy" = "軟碟"; +/* No comment provided by engineer. */ +"Floppy Image" = "軟碟映像檔"; + /* No comment provided by engineer. */ "Font Size" = "字體大小"; @@ -512,7 +542,7 @@ "Force shut down" = "強行關機"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB Debug Stub"; @@ -695,7 +725,7 @@ "Maximum Shared USB Devices" = "最多分享 USB 裝置"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "記憶體"; @@ -706,6 +736,9 @@ /* No comment provided by engineer. */ "Minimum size: %@" = "最小大小:%@"; +/* UTMDonateView */ +"month" = "月"; + /* No comment provided by engineer. */ "Mouse/Keyboard" = "滑鼠/鍵盤"; @@ -733,6 +766,9 @@ /* No comment provided by engineer. */ "New" = "新增"; +/* No comment provided by engineer. */ +"New Machine" = "新增電腦"; + /* No comment provided by engineer. */ "New…" = "新增⋯"; @@ -809,6 +845,9 @@ /* No comment provided by engineer. */ "Option (⌥) is Meta key" = "將 Option (⌥) 作為 Meta 鍵"; +/* No comment provided by engineer. */ +"Options" = "選項"; + /* No comment provided by engineer. */ "Other" = "其他"; @@ -836,6 +875,9 @@ /* No comment provided by engineer. */ "Pending" = "待定"; +/* UTMDonateView */ +"period" = "期間"; + /* VMDisplayWindowController */ "Play" = "播放"; @@ -1114,6 +1156,9 @@ /* UTMSWTPM */ "SW TPM failed to start. %@" = "SW TPM 無法啟動。%@"; +/* VMSessionState */ +"Switch back to UTM to avoid termination." = "切換回至 UTM 以避免終止。"; + /* No comment provided by engineer. */ "System" = "系統"; @@ -1369,10 +1414,10 @@ "Virtualize" = "虛擬化"; /* No comment provided by engineer. */ -"VM display size is fixed" = "虛擬電腦顯示大小固定"; +"Waiting for VM to connect to display..." = "正在等待虛擬電腦連接至顯示⋯"; -/* No comment provided by engineer. */ -"Waiting for VM to connect to display..." = "正在等待虛擬電腦連接至顯示..."; +/* UTMDonateView */ +"week" = "周"; /* No comment provided by engineer. */ "Welcome to UTM" = "歡迎使用 UTM"; @@ -1395,6 +1440,9 @@ /* No comment provided by engineer. */ "Would you like to re-convert this disk image to reclaim unused space? Note this will require enough temporary space to perform the conversion. You are strongly encouraged to back-up this VM before proceeding." = "你要重新轉換此磁碟映像檔以回收未使用的空間嗎?請緊記,這將需要足夠的臨時空間來執行轉換。強烈建議你先備份此虛擬電腦,然後再繼續操作。"; +/* UTMDonateView */ +"year" = "年"; + /* No comment provided by engineer. */ "Yes" = "是"; @@ -1405,11 +1453,20 @@ VMWizardOSMacView */ "Your machine does not support running this IPSW." = "你的電腦不支援執行此 IPSW。"; +/* UTMDonateView */ +"Your purchase could not be verified by the App Store." = "App Store 無法驗證你的購買。"; + +/* No comment provided by engineer. */ +"Your support is the driving force that helps UTM stay independent. Your contribution, no matter the size, makes a significant difference. It enables us to develop new features and maintain existing ones. Thank you for considering a donation to support us." = "你的支援是幫助 UTM 保持獨立的動力。無論你的貢獻幾多,都會帶來重大影響。這可以令我們能夠開發新功能,並維護現有的功能。多謝你考慮捐贈來支援我們。"; + /* ContentView */ "Your version of iOS does not support running VMs while unmodified. You must either run UTM while jailbroken or with a remote debugger attached. See https://getutm.app/install/ for more details." = "你的 iOS 版本不支援在未作更動的情況下執行虛擬電腦,必須在越獄 (jailbreak) 時執行 UTM,或是在附加遠程除錯器的情況下執行 UTM。有關更多詳細訊息,請見 https://getutm.app/install/。"; // Additional Strings (These strings are unable to be extracted by Xcode) +/* No comment provided by engineer. */ +"" = ""; + /* No comment provided by engineer. */ "(Delete)" = "(刪除)"; @@ -1425,15 +1482,24 @@ /* No comment provided by engineer. */ "Add read only" = "加入唯讀"; +/* No comment provided by engineer. */ +"Advanced" = "進階"; + /* No comment provided by engineer. */ "Advanced. If checked, a raw disk image is used. Raw disk image does not support snapshots and will not dynamically expand in size." = "進階選項。如選取,將會使用 Raw 磁碟映像。Raw 磁碟映像不支援快照,也不會動態擴充套件大小。"; +/* No comment provided by engineer. */ +"Allow access from external clients" = "允許外部客戶端訪問"; + /* No comment provided by engineer. */ "Allow Remote Connection" = "允許遠端連線"; /* No comment provided by engineer. */ "Allows passing through additional input from trackpads. Only supported on macOS 13+ guests." = "允許透過觸控板額外輸入。僅支援 macOS 13+ 客戶端。"; +/* No comment provided by engineer. */ +"Any" = "任意"; + /* No comment provided by engineer. */ "Apple Virtualization is experimental and only for advanced use cases. Leave unchecked to use QEMU, which is recommended." = "Apple 虛擬化為試驗性質,僅可用作進階用例,不選取此剔選框以使用推介的 QEMU。"; @@ -1452,6 +1518,9 @@ /* No comment provided by engineer. */ "Automatic" = "自動"; +/* No comment provided by engineer. */ +"Automatically start UTM server" = "自動開始 UTM 伺服器"; + /* No comment provided by engineer. */ "Background Color" = "背景顏色"; @@ -1605,6 +1674,9 @@ /* No comment provided by engineer. */ "Downscaling" = "細化解像度"; +/* No comment provided by engineer. */ +"Dynamic Resolution" = "動態解像度"; + /* No comment provided by engineer. */ "Edit" = "編輯"; @@ -1650,6 +1722,9 @@ /* No comment provided by engineer. */ "Enable Sound" = "啟用聲音"; +/* No comment provided by engineer. */ +"Enable UTM Server" = "啟用 UTM 伺服器"; + /* No comment provided by engineer. */ "Engine" = "引擎"; @@ -1662,6 +1737,9 @@ /* No comment provided by engineer. */ "External Drive" = "外部磁碟"; +/* UTMData */ +"Failed to parse download URL." = "無法解析已經下載的 URL。"; + /* No comment provided by engineer. */ "Fetch latest Windows installer…" = "取得最新的 Windows 安裝工具⋯"; @@ -1747,7 +1825,7 @@ "If enabled, a virtiofs share tagged 'rosetta' will be available on the Linux guest for installing Rosetta for emulating x86_64 on ARM64." = "如啟用,標記為「rosetta」的 virtiofs 分享將會於 Linux 客戶端上可用,用作安裝 Rosetta,可以於 arm64 上仿真 x86_64。"; /* No comment provided by engineer. */ -"If enabled, any existing screenshot will be deleted the next time the VM is started." = "如啟用,下次啟動虛擬電腦時,任何現存的快照將會被刪除。"; +"If enabled, any existing screenshot will be deleted the next time the VM is started." = "如啟用,下次啟動虛擬電腦時,任何現存的螢幕截圖將會被刪除。"; /* No comment provided by engineer. */ "If enabled, caps lock will be handled like other keys. If disabled, it is treated as a toggle that is synchronized with the host." = "如啟用,Caps Lock 將會同其他鍵一樣處理。如停用,它將會被視為開關鍵,並與主機同步。"; @@ -1755,6 +1833,9 @@ /* No comment provided by engineer. */ "If enabled, input capture will toggle automatically when entering and exiting full screen mode." = "如啟用,於進入和離開全螢幕模式時,將會自動切換輸入擷取。"; +/* No comment provided by engineer. */ +"If enabled, input capture will toggle automatically when the VM's window is focused." = "如啟用,於虛擬電腦聚焦視窗時,將自動切換輸入擷取。"; + /* No comment provided by engineer. */ "If enabled, num lock will always be on to the guest. Note this may make your keyboard's num lock indicator out of sync." = "如啟用,Num Lock 將會始終對客戶端開啟。請緊記,這可能會令鍵盤的 Num Lock 指示器不同步。"; @@ -1780,7 +1861,7 @@ "Image Type" = "映像檔種類"; /* No comment provided by engineer. */ -"Import Drive" = "輸入磁碟機"; +"Import Drive…" = "輸入磁碟機⋯"; /* No comment provided by engineer. */ "Import VHDX Image" = "輸入 VHDX 映像檔"; @@ -1824,6 +1905,9 @@ /* No comment provided by engineer. */ "Keyboard" = "鍵盤"; +/* No comment provided by engineer. */ +"Last Seen" = "最後檢視時間"; + /* No comment provided by engineer. */ "MAC Address" = "MAC 位址"; @@ -1861,7 +1945,7 @@ "Network Mode" = "網絡模式"; /* No comment provided by engineer. */ -"New Drive" = "新增磁碟機"; +"New Drive…" = "新增磁碟機⋯"; /* No comment provided by engineer. */ "New from template…" = "由此範本新增⋯"; @@ -1896,6 +1980,9 @@ /* No comment provided by engineer. */ "Path" = "路徑"; +/* No comment provided by engineer. */ +"Pointer" = "指標"; + /* No comment provided by engineer. */ "Port" = "埠"; @@ -1932,12 +2019,18 @@ /* No comment provided by engineer. */ "Reclaim Space" = "釋放空間"; +/* No comment provided by engineer. */ +"Reject unknown connections by default" = "於預設情況下拒絕不明連線"; + /* No comment provided by engineer. */ "Remove selected shortcut" = "移除已選取的捷徑"; /* No comment provided by engineer. */ "Renderer Backend" = "渲染器後端"; +/* No comment provided by engineer. */ +"Require Password" = "需要密碼"; + /* No comment provided by engineer. */ "Requires restarting UTM to take affect." = "需要重新開啟 UTM 以生效。"; @@ -2172,6 +2265,9 @@ /* No comment provided by engineer. */ "Use Virtualization" = "使用虛擬化"; +/* No comment provided by engineer. */ +"UTM Server" = "UTM 伺服器"; + /* No comment provided by engineer. */ "VGA Device RAM (MB)" = "VGA 裝置記憶體 (MB)"; @@ -2181,6 +2277,9 @@ /* No comment provided by engineer. */ "Virtualization Engine" = "虛擬化引擎"; +/* No comment provided by engineer. */ +"VM display size is fixed" = "虛擬電腦顯示大小固定"; + /* No comment provided by engineer. */ "Wait for Connection" = "等待連線"; @@ -2198,3 +2297,8 @@ /* No comment provided by engineer. */ "Zoom" = "縮放"; + +/* VMConfigAppleDriveDetailsView + VMConfigAppleDriveCreateView*/ +"Use NVMe Interface" = "使用 NVMe 磁碟介面"; +"If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors." = "如果勾選,將使用 NVMe 而非 virtio 作為磁碟介面,僅在 macOS 14+ 中適用於 Linux 客戶機器。這個介面速度較慢,但較不容易遇到檔案系統錯誤。"; diff --git a/Platform/zh-Hans.lproj/Localizable.strings b/Platform/zh-Hans.lproj/Localizable.strings index 4e2cb9034..def7f4512 100644 --- a/Platform/zh-Hans.lproj/Localizable.strings +++ b/Platform/zh-Hans.lproj/Localizable.strings @@ -44,6 +44,18 @@ /* Format string for download progress and speed, e. g. 5 MB of 6 GB (200 kbit/s) */ "%1$@ of %2$@ (%3$@)" = "共 %2$@,已下载 %1$@ (%3$@)"; +/* UTMDonateView */ +"%d days" = "%d 天"; + +/* UTMDonateView */ +"%d months" = "%d 月"; + +/* UTMDonateView */ +"%d weeks" = "%d 周"; + +/* UTMDonateView */ +"%d years" = "%d 年"; + /* UTMScriptingAppDelegate */ "A valid backend must be specified." = "必须指定有效的后端。"; @@ -62,9 +74,6 @@ /* No comment provided by engineer. */ "Additional Settings" = "附加设置"; -/* No comment provided by engineer. */ -"Advanced" = "高级"; - /* VMConfigSystemView */ "Allocating too much memory will crash the VM." = "分配过多内存会使虚拟机崩溃。"; @@ -122,6 +131,9 @@ /* UTMQemuConstants */ "Automatic Serial Device (max 4)" = "自动串行设备 (最大值 4)"; +/* VMSessionState */ +"Background task is about to expire" = "后台任务即将终止"; + /* UTMLegacyQemuConfiguration UTMQemuConstants */ "BIOS" = "BIOS"; @@ -144,6 +156,9 @@ /* No comment provided by engineer. */ "Boot Image Type" = "启动映像类型"; +/* No comment provided by engineer. */ +"Boot IMG Image" = "启动 IMG 映像"; + /* No comment provided by engineer. */ "Boot ISO Image" = "启动 ISO 映像"; @@ -208,6 +223,9 @@ /* No comment provided by engineer. */ "Capture input automatically when entering full screen" = "进入全屏时自动捕获输入"; +/* No comment provided by engineer. */ +"Capture input automatically when window is focused" = "聚焦窗口时自动捕获输入"; + /* VMDisplayQemuMetalWindowController */ "Captured mouse" = "已捕获鼠标"; @@ -218,12 +236,18 @@ UTMQemuConstants */ "CD/DVD (ISO) Image" = "CD/DVD (ISO) 映像"; +/* No comment provided by engineer. */ +"CD/DVD Image" = "CD/DVD 映像"; + /* VMDisplayWindowController */ "Change" = "更改"; /* VMDisplayAppleController */ "Change…" = "更改…"; +/* No comment provided by engineer. */ +"Choose" = "选择"; + /* No comment provided by engineer. */ "Clear" = "清除"; @@ -276,6 +300,9 @@ /* No comment provided by engineer. */ "Create" = "创建"; +/* No comment provided by engineer. */ +"Create a new emulated machine from scratch." = "从头开始创建一个新的虚拟机。"; + /* Welcome view */ "Create a New Virtual Machine" = "创建一个新虚拟机"; @@ -285,6 +312,9 @@ /* UTMSWTPM */ "Data not specified." = "未指定数据。"; +/* UTMDonateView */ +"day" = "天"; + /* No comment provided by engineer. */ "Debug Logging" = "调试日志记录"; @@ -327,7 +357,7 @@ "Disposable Mode" = "一次性模式"; /* No comment provided by engineer. */ -"Do not save VM screenshot to disk" = "不将虚拟机的屏幕截图保存到磁盘"; +"Do not save VM screenshot to disk" = "不将虚拟机截图保存到磁盘"; /* No comment provided by engineer. */ "Do not show confirmation when closing a running VM" = "关闭正在运行的虚拟机时不显示确认"; @@ -461,9 +491,6 @@ /* UTMQemuConfigurationError */ "Failed to migrate configuration from a previous UTM version." = "无法从以前的 UTM 版本迁移配置。"; -/* UTMData */ -"Failed to parse download URL." = "无法解析下载 URL。"; - /* UTMRemoteKeyManager */ "Failed to parse generated key pair." = "无法解析生成的密钥对。"; @@ -496,6 +523,9 @@ UTMQemuConstants */ "Floppy" = "软盘"; +/* No comment provided by engineer. */ +"Floppy Image" = "软盘映像"; + /* No comment provided by engineer. */ "Font Size" = "字体大小"; @@ -512,7 +542,7 @@ "Force shut down" = "强制关机"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB 调试存根"; @@ -695,7 +725,7 @@ "Maximum Shared USB Devices" = "最大共享 USB 设备数"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "内存"; @@ -706,6 +736,9 @@ /* No comment provided by engineer. */ "Minimum size: %@" = "最小文件大小:%@"; +/* UTMDonateView */ +"month" = "月"; + /* No comment provided by engineer. */ "Mouse/Keyboard" = "鼠标/键盘"; @@ -733,6 +766,9 @@ /* No comment provided by engineer. */ "New" = "新建"; +/* No comment provided by engineer. */ +"New Machine" = "新建虚拟机"; + /* No comment provided by engineer. */ "New…" = "新建…"; @@ -809,6 +845,9 @@ /* No comment provided by engineer. */ "Option (⌥) is Meta key" = "Option (⌥) 键作为 Meta 键"; +/* No comment provided by engineer. */ +"Options" = "选项"; + /* No comment provided by engineer. */ "Other" = "其他"; @@ -836,6 +875,9 @@ /* No comment provided by engineer. */ "Pending" = "等待中"; +/* UTMDonateView */ +"period" = "周期"; + /* VMDisplayWindowController */ "Play" = "启动"; @@ -1114,6 +1156,9 @@ /* UTMSWTPM */ "SW TPM failed to start. %@" = "SW TPM 无法启动。%@"; +/* VMSessionState */ +"Switch back to UTM to avoid termination." = "切换回 UTM 以避免终止。"; + /* No comment provided by engineer. */ "System" = "系统"; @@ -1341,7 +1386,7 @@ /* UTMScriptingAppDelegate UTMScriptingUSBDeviceImpl */ -"UTM is not ready to accept commands." = "UTM 未准备好接受命令。"; +"UTM is not ready to accept commands." = "UTM 尚未准备好接受命令。"; /* No comment provided by engineer. */ "Version" = "版本号"; @@ -1369,10 +1414,10 @@ "Virtualize" = "虚拟化"; /* No comment provided by engineer. */ -"VM display size is fixed" = "虚拟机显示大小为固定"; +"Waiting for VM to connect to display..." = "等待虚拟机连接到显示…"; -/* No comment provided by engineer. */ -"Waiting for VM to connect to display..." = "等待虚拟机连接到显示..."; +/* UTMDonateView */ +"week" = "周"; /* No comment provided by engineer. */ "Welcome to UTM" = "欢迎使用 UTM"; @@ -1395,6 +1440,9 @@ /* No comment provided by engineer. */ "Would you like to re-convert this disk image to reclaim unused space? Note this will require enough temporary space to perform the conversion. You are strongly encouraged to back-up this VM before proceeding." = "要重新转换此磁盘映像以回收未使用的空间吗?请注意,这将需要足够的临时空间来执行转换。在继续操作之前,强烈建议你备份此虚拟机。"; +/* UTMDonateView */ +"year" = "年"; + /* No comment provided by engineer. */ "Yes" = "是"; @@ -1405,11 +1453,20 @@ VMWizardOSMacView */ "Your machine does not support running this IPSW." = "你的机器不支持运行此 IPSW。"; +/* UTMDonateView */ +"Your purchase could not be verified by the App Store." = "App Store 无法验证你的购买。"; + +/* No comment provided by engineer. */ +"Your support is the driving force that helps UTM stay independent. Your contribution, no matter the size, makes a significant difference. It enables us to develop new features and maintain existing ones. Thank you for considering a donation to support us." = "你的支持是 UTM 保持独立的动力。你的贡献无论或大或小,都会产生重大的影响。它可以使我们能开发出功能,并维护现有的功能。感谢你考虑捐赠支持我们。"; + /* ContentView */ "Your version of iOS does not support running VMs while unmodified. You must either run UTM while jailbroken or with a remote debugger attached. See https://getutm.app/install/ for more details." = "你的 iOS 版本不支持在未经修改的情况下运行虚拟机,必须在越狱时运行 UTM,或者连接远程调试器。有关更多详细信息,请参阅 https://getutm.app/install/。"; // Additional Strings (These strings are unable to be extracted by Xcode) +/* No comment provided by engineer. */ +"" = ""; + /* No comment provided by engineer. */ "(Delete)" = "(删除)"; @@ -1425,15 +1482,24 @@ /* No comment provided by engineer. */ "Add read only" = "添加只读"; +/* No comment provided by engineer. */ +"Advanced" = "高级"; + /* No comment provided by engineer. */ "Advanced. If checked, a raw disk image is used. Raw disk image does not support snapshots and will not dynamically expand in size." = "高级选项。若选中,将使用 Raw 磁盘映像。Raw 磁盘映像不支持快照,也不会动态地扩充大小。"; +/* No comment provided by engineer. */ +"Allow access from external clients" = "允许外部客户机访问"; + /* No comment provided by engineer. */ "Allow Remote Connection" = "允许远程连接"; /* No comment provided by engineer. */ "Allows passing through additional input from trackpads. Only supported on macOS 13+ guests." = "允许通过触控板额外输入。仅支持 macOS 13 及以上的客户机。"; +/* No comment provided by engineer. */ +"Any" = "任意"; + /* No comment provided by engineer. */ "Apple Virtualization is experimental and only for advanced use cases. Leave unchecked to use QEMU, which is recommended." = "Apple 虚拟化属于实验性功能,仅适用于高级用例。推荐不选中此复选框,以使用 QEMU。"; @@ -1452,6 +1518,9 @@ /* No comment provided by engineer. */ "Automatic" = "自动"; +/* No comment provided by engineer. */ +"Automatically start UTM server" = "自动启动 UTM 服务器"; + /* No comment provided by engineer. */ "Background Color" = "背景颜色"; @@ -1605,6 +1674,9 @@ /* No comment provided by engineer. */ "Downscaling" = "细化"; +/* No comment provided by engineer. */ +"Dynamic Resolution" = "动态"; + /* No comment provided by engineer. */ "Edit" = "编辑"; @@ -1650,6 +1722,9 @@ /* No comment provided by engineer. */ "Enable Sound" = "启用声音"; +/* No comment provided by engineer. */ +"Enable UTM Server" = "启用 UTM 服务器"; + /* No comment provided by engineer. */ "Engine" = "引擎"; @@ -1662,6 +1737,9 @@ /* No comment provided by engineer. */ "External Drive" = "外部磁盘"; +/* UTMData */ +"Failed to parse download URL." = "无法解析下载 URL。"; + /* No comment provided by engineer. */ "Fetch latest Windows installer…" = "获取最新的 Windows 安装程序…"; @@ -1756,7 +1834,10 @@ "If enabled, input capture will toggle automatically when entering and exiting full screen mode." = "若启用,输入捕捉会在进入和退出全屏模式时自动切换。"; /* No comment provided by engineer. */ -"If enabled, num lock will always be on to the guest. Note this may make your keyboard's num lock indicator out of sync." = "若启用,Num Lock 将始终对客户机开启。注意,这可能会使键盘的 Num Lock 指示灯不同步。"; +"If enabled, input capture will toggle automatically when the VM's window is focused." = "若启用,输入捕捉将在虚拟机窗口聚焦时自动切换。"; + +/* No comment provided by engineer. */ +"If enabled, num lock will always be on to the guest. Note this may make your keyboard's num lock indicator out of sync." = "若启用,Num Lock 将始终对客户机开启。注意,这可能会使键盘的 Num Lock 指示灯不同步。"; /* No comment provided by engineer. */ "If enabled, Option will be mapped to the Meta key which can be useful for emacs. Otherwise, option will work as the system intended (such as for entering international text)." = "若启用,Option 键将映射到 Meta 键,这对 Emacs 很有用。否则,Option 键将按照系统默认方式工作 (例如输入国际文本)。"; @@ -1780,7 +1861,7 @@ "Image Type" = "映像类型"; /* No comment provided by engineer. */ -"Import Drive" = "导入驱动器"; +"Import Drive…" = "导入驱动器…"; /* No comment provided by engineer. */ "Import VHDX Image" = "导入 VHDX 映像"; @@ -1824,6 +1905,9 @@ /* No comment provided by engineer. */ "Keyboard" = "键盘"; +/* No comment provided by engineer. */ +"Last Seen" = "最后上线于"; + /* No comment provided by engineer. */ "MAC Address" = "MAC 地址"; @@ -1861,7 +1945,7 @@ "Network Mode" = "网络模式"; /* No comment provided by engineer. */ -"New Drive" = "新建驱动器"; +"New Drive…" = "新建驱动器…"; /* No comment provided by engineer. */ "New from template…" = "从此模板新建…"; @@ -1896,6 +1980,9 @@ /* No comment provided by engineer. */ "Path" = "路径"; +/* No comment provided by engineer. */ +"Pointer" = "鼠标指针"; + /* No comment provided by engineer. */ "Port" = "端口"; @@ -1932,12 +2019,18 @@ /* No comment provided by engineer. */ "Reclaim Space" = "释放空间"; +/* No comment provided by engineer. */ +"Reject unknown connections by default" = "默认情况下拒绝未知连接"; + /* No comment provided by engineer. */ "Remove selected shortcut" = "移除选中的快捷方式"; /* No comment provided by engineer. */ "Renderer Backend" = "渲染器后端"; +/* No comment provided by engineer. */ +"Require Password" = "需要密码"; + /* No comment provided by engineer. */ "Requires restarting UTM to take affect." = "需要重新打开 UTM 以生效。"; @@ -2172,6 +2265,9 @@ /* No comment provided by engineer. */ "Use Virtualization" = "使用虚拟化"; +/* No comment provided by engineer. */ +"UTM Server" = "UTM 服务器"; + /* No comment provided by engineer. */ "VGA Device RAM (MB)" = "VGA 设备内存 (MB)"; @@ -2181,6 +2277,9 @@ /* No comment provided by engineer. */ "Virtualization Engine" = "虚拟化引擎"; +/* No comment provided by engineer. */ +"VM display size is fixed" = "虚拟机显示大小为固定"; + /* No comment provided by engineer. */ "Wait for Connection" = "等待连接"; @@ -2198,3 +2297,8 @@ /* No comment provided by engineer. */ "Zoom" = "缩放"; + +/* VMConfigAppleDriveDetailsView + VMConfigAppleDriveCreateView*/ +"Use NVMe Interface" = "使用 NVMe 磁盘接口"; +"If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors." = "如果选中,使用 NVMe 而不是 virtio 作为磁盘接口,仅适用于 macOS 14+ 上的 Linux 客户机。此接口速度较慢,但不太容易遇到文件系统错误。"; diff --git a/Platform/zh-Hant.lproj/Localizable.strings b/Platform/zh-Hant.lproj/Localizable.strings index ee2dd422b..39729e922 100644 --- a/Platform/zh-Hant.lproj/Localizable.strings +++ b/Platform/zh-Hant.lproj/Localizable.strings @@ -720,7 +720,7 @@ "FPS Limit" = "FPS 上限"; /* No comment provided by engineer. */ -"GB" = "GB"; +"GiB" = "GiB"; /* UTMQemuConstants */ "GDB Debug Stub" = "GDB 除錯 stub"; @@ -858,7 +858,7 @@ "Image Type" = "映像檔類型"; /* No comment provided by engineer. */ -"Import Drive" = "匯入磁碟機"; +"Import Drive…" = "匯入磁碟機⋯"; /* No comment provided by engineer. */ "Import IPSW" = "匯入 IPSW"; @@ -1023,7 +1023,7 @@ "Maximum Shared USB Devices" = "最大共享 USB 裝置"; /* No comment provided by engineer. */ -"MB" = "MB"; +"MiB" = "MiB"; /* No comment provided by engineer. */ "Memory" = "記憶體"; @@ -1080,7 +1080,7 @@ "New" = "新增"; /* No comment provided by engineer. */ -"New Drive" = "新增磁碟機"; +"New Drive…" = "新增磁碟機…"; /* No comment provided by engineer. */ "New from template…" = "從樣板建立…"; @@ -2002,3 +2002,8 @@ /* No comment provided by engineer. */ "Zoom" = "縮放"; +/* VMConfigAppleDriveDetailsView + VMConfigAppleDriveCreateView*/ +"Use NVMe Interface" = "使用 NVMe 磁碟介面"; +"If checked, use NVMe instead of virtio as the disk interface, available on macOS 14+ for Linux guests only. This interface is slower but less likely to encounter filesystem errors." = "如果選取,將使用 NVMe 而非 virtio 作為磁碟介面,僅在 macOS 14+ 中適用於 Linux 客戶機器。這個介面速度較慢,但較不容易遇到檔案系統錯誤。"; + diff --git a/README.bn.md b/README.bn.md new file mode 100644 index 000000000..6ad88255f --- /dev/null +++ b/README.bn.md @@ -0,0 +1,83 @@ +# UTM +[![Build](https://github.com/utmapp/UTM/workflows/Build/badge.svg?branch=master&event=push)][1] + +> এমন মেশিন আবিষ্কার করা সম্ভব যা যেকোনো ধরনের সিকোয়েনশিয়াল গণনা করতে পারে তা যত কঠিন গণনাই হোক না কেন + +-- অ্যালান টুরিং, ১৯৩৬ + +UTM হল iOS এবং macOS-এর জন্য সমস্ত ফিচার বিশিষ্ট সিস্টেম ইমিউলেটর এবং ভার্চুয়াল মেশিন হোস্ট। এটি QEMU এর উপর বেজ করে বানানো হয়েছে । সংক্ষেপে বলা যায় যে, UTM আপনাকে আপনার ম্যাক, আইফোন এবং আইপ্যাডে উইন্ডোজ, লিনাক্স এবং আরও অনেক অপারেটিং সিস্টেম চালানোর অনুমতি দেয়। লিঙ্ক গুলোতে আরও বিস্তারিত জানতে পারবেনঃ +১। https://getutm.app/ এবং +২। https://mac.getutm.app/ + +

+ একটি আইফোনে UTM চলছে +
+ একটি ম্যাকবুকে UTM চলছে +

+ +## UTM এ যা যা ফিচার আছেঃ + +* QEMU ব্যবহার করে সম্পূর্ণ সিস্টেম ইমিউলেশন (MMU, ডিভাইস, ইত্যাদি) +* x86_64, ARM64, এবং RISC-V সহ 30+ প্রসেসর সাপোর্টেড +* SPICE এবং QXL ব্যবহার করে VGA গ্রাফিক্স মোড +* টেক্সট টার্মিনাল মোড +* ইউএসবি ডিভাইস +* QEMU TCG ব্যবহার করে JIT ভিত্তিক এক্সিলারেশন +* সবচেয়ে লেটেস্ট এবং সেরা API গুলো ব্যবহার করে macOS 11 এবং iOS 11+ এর জন্য স্ক্র্যাচ থেকে ডিজাইন করা ফ্রন্টেন্ড +* আপনার ডিভাইস থেকে সরাসরি VM তৈরি করুন, ম্যানেজ করুন, চালান + +## macOS এর ক্ষেত্রে অতিরিক্ত যা যা ফিচার আছে + +* Hypervisor.framework এবং QEMU ব্যবহার করে হার্ডওয়্যার এক্সিলারেটেড ভার্চুয়ালাইজেশন +* macOS 12+ এ Virtualization.framework সহ macOS গেস্ট বুট করুন + +## UTM SE + +UTM/QEMU-এর সর্বোচ্চ পারফরমেন্স এর জন্য ডায়নামিক কোড জেনারেশন (JIT) প্রয়োজন। iOS ডিভাইসে JIT-এর জন্য দরকার একটি জেলব্রোকেন ডিভাইস, অথবা iOS-এর নির্দিষ্ট ভার্শন এর জন্য যেকোনো ওয়ার্ক এরাউন্ড (আরো বিশদ বিবরণের জন্য "ইনস্টল" পার্ট টি দেখুন)। + +UTM SE ("স্লো এডিশন") একটি [থ্রেডেড ইন্টারপ্রেটার][3] ব্যবহার করে যা একটি ট্র্যাডিশনাল ইন্টারপ্রেটার এর চেয়ে যদিও ভাল পারফর্ম করে কিন্তু এখনও JIT এর চেয়ে স্লো। এই টেকনিকটি ডাইনামিক এক্সিকিউশন এর জন্য [iSH][4] যা করে তার মতোই। ফলস্বরূপ, UTM SE-এর জন্য জেলব্রেকিং বা কোনো JIT সমাধানের প্রয়োজন নেই এবং রেগুলার অ্যাপ হিসেবে সাইডলোড করা যায়। + +সাইজ এবং বিল্ড এর সময় অপ্টিমাইজ করার জন্য, শুধুমাত্র নিচের আর্কিটেকচারগুলি UTM SE-তে ইনক্লুড করা হয়েছে: ARM, PPC, RISC-V, এবং x86 (সমস্তই 32-বিট এবং 64-বিট ভেরিয়েন্টের সাথে)। + +## ইনস্টল + +iOS এর জন্য UTM (SE): https://getutm.app/install/ + + macOS এর জন্য UTM নামাতে পারবেন এখান থেকে: https://mac.getutm.app/ + +## ডেভেলপমেন্ট + +### [macOS ডেভেলপমেন্ট](Documentation/MacDevelopment.md) + +### [iOS ডেভেলপমেন্ট](Documentation/iOSDevelopment.md) + +## রিলেটেড + +* [iSH][4]: iOS এ x86 Linux অ্যাপ্লিকেশন চালানোর জন্য একটি usermode Linux টার্মিনাল ইন্টারফেস ইমিউলেট করে +* [a-shell][5]: সাধারণ ইউনিক্স কমান্ড এবং ইউটিলিটি যেগুলো iOS এর জন্য নেটিভ সেগুলো এটি প্যাকেজ করে দেয় এবং এটি টার্মিনাল ইন্টারফেসের মাধ্যমে অ্যাক্সেস করা যায়। + +## লাইসেন্স + +UTM পারমিসিভ Apache 2.0 লাইসেন্সের অধীনে ডিস্ট্রিবিউট করা হচ্ছে। সেই সাথে এটি বেশ কয়েকটি (L)GPL কম্পোনেন্ট ব্যবহার করছে। বেশিরভাগই ডাইন্যামিক্যালি লিঙ্কড কিন্তু gstreamer প্লাগইনগুলি স্ট্যাটিকভাবে লিঙ্ক করা এবং কোডের কিছু অংশ qemu থেকে নেওয়া। আপনি যদি এই অ্যাপ্লিকেশনটি পুনরায় ডিস্ট্রিবিউট করতে চান তবে দয়া করে এই বিষয় গুলো সম্পর্কে খেয়াল রাখবেন৷ +Some icons made by [Freepik](https://www.freepik.com) [www.flaticon.com](https://www.flaticon.com/). + +[www.flaticon.com](https://www.flaticon.com/) থেকে [ফ্রিপিকের](https://www.freepik.com) তৈরি কিছু আইকন এখানে ব্যবহার করা হয়েছে। + + +এছাড়াও, UTM ফ্রন্টএন্ড নিম্নলিখিত MIT/BSD লাইসেন্স কম্পোনেন্ট এর উপর নির্ভর করে: + +* [IQKeyboardManager](https://github.com/hackiftekhar/IQKeyboardManager) +* [SwiftTerm](https://github.com/migueldeicaza/SwiftTerm) +* [ZIP Foundation](https://github.com/weichsel/ZIPFoundation) +* [InAppSettingsKit](https://github.com/futuretap/InAppSettingsKit) + +কন্টিনিউয়াস ইন্টিগ্রেশন হোস্টিং টি [MacStadium](https://www.macstadium.com/opensource) প্রোভাইড করছে + + +[MacStadium logo](https://www.macstadium.com) + + [1]: https://github.com/utmapp/UTM/actions?query=event%3Arelease+workflow%3ABuild + [2]: screen.png + [3]: https://github.com/ktemkin/qemu/blob/with_tcti/tcg/aarch64-tcti/README.md + [4]: https://github.com/ish-app/ish + [5]: https://github.com/holzschu/a-shell diff --git a/README.zh-HK.md b/README.zh-HK.md index bebba9d9b..1c0b11f0d 100644 --- a/README.zh-HK.md +++ b/README.zh-HK.md @@ -1,7 +1,7 @@ # UTM [![Build](https://github.com/utmapp/UTM/workflows/Build/badge.svg?branch=master&event=push)][1] -> 發明一台可用於計算任何可計算序列的機器是可行的。 +> 發明一個可用於計算任何可計算序列的機器是可行的。 -- 艾倫·圖靈(Alan Turing), 1936 年 UTM 是一個功能完备的系統模擬工具與虛擬电脑主機,適用於 iOS 和 macOS。它基於 QEMU。簡言之,它允許你在 Mac、iPhone 和 iPad 上執行 Windows、Linux 等。更多訊息請見 https://getutm.app/ 與 https://mac.getutm.app/。 @@ -14,33 +14,33 @@ UTM 是一個功能完备的系統模擬工具與虛擬电脑主機,適用於 ## 特性 -* 使用 QEMU 進行全作業系統模擬(MMU、裝置等) +* 使用 QEMU 進行全作業系統仿真(MMU、裝置等) * 支援逾三十種體系結構 CPU,包括 x86_64、ARM64 和 RISC-V * 使用 SPICE 與 QXL 的 VGA 圖形模式 -* 文本終端機模式 +* 文本終端機(TTY)模式 * USB 裝置 * 使用 QEMU TCG 進行基於 JIT 的加速 -* 採用了最新最靚的 API,由頭開始設計前端,支援 macOS 11+ 與 iOS 11+ +* 採用最新最靚的 API,由頭開始設計前端,支援 macOS 11+ 與 iOS 11+ * 於你的裝置上直接製作、管理與執行虛擬機 ## 於 macOS 的附加功能 * 使用 Hypervisor.framework 與 QEMU 實現硬件加速虛擬化 -* 在 macOS 12+ 上使用 Virtualization.framework 來啟動 macOS 客戶端 +* 在 macOS 12+ 上使用 Virtualization.framework 啟動 macOS 客戶端 ## UTM SE UTM/QEMU 需要動態程式碼生成(JIT)以得到最大性能。iOS 上的 JIT 需要已經越獄(Jailbreak)的裝置(iOS 11.0~14.3 無需越獄,iOS 14.4+ 需要),或者為特定版本的 iOS 找到其他變通方法之一(有關更多詳細訊息,請見「安裝」)。 -UTM SE(「較慢版」)使用了「[執行緒解釋器][3]」,其性能優於傳統解釋器,但仍然比 JIT 要慢。此種技術類似 [iSH][4] 的動態執行。因此,UTM SE 無需越獄或任何 JIT 的變通方法,可以作為常規應用程式側載(Sideload)。 +UTM SE(「較慢版」)使用了「[執行緒解釋器][3]」,其性能優於傳統解釋器,但仍然慢過 JIT。此種技術類似 [iSH][4] 的動態執行。因此,UTM SE 無需越獄或者任何 JIT 的其他變通方法,可以作為常規的應用程式側載(Sideload)。 -為了最佳化大小與構建時間,UTM SE 當中只包含以下的體系結構:ARM、PPC、RISC-V 和 x86(均包含 32 位元和 64 位元)。 +為了最佳化大小與構建時間,UTM SE 當中僅包括以下的體系結構:ARM、PPC、RISC-V 和 x86(均包括 32 位元與 64 位元)。 ## 安裝 iOS 版本 UTM(SE):https://getutm.app/install/ -UTM 同時支援 macOS:https://mac.getutm.app/ +UTM 亦支援 macOS:https://mac.getutm.app/ ## 開發 @@ -55,7 +55,7 @@ UTM 同時支援 macOS:https://mac.getutm.app/ ## 許可證 -UTM 於 Apache 2.0 許可證下發佈,但它採用了若干 GPL 與 LGPL 元件,當中大多數元件為動態連接,但 gstreamer 元件為靜態連接,部分程式碼來自 QEMU。如你打算重新分發此應用程式,請務必緊記這一點。 +UTM 於 Apache 2.0 許可證下發佈,但它採用了若干 GPL 與 LGPL 元件,當中大多數元件為動態連接,但是 gstreamer 元件為靜態連接,部分程式碼來自 QEMU。如你打算重新分發此應用程式,請務必緊記這一點。 某些图示由 [Freepik](https://www.freepik.com) 從 [www.flaticon.com](https://www.flaticon.com/) 製作。 diff --git a/Remote/UTMRemoteServer.swift b/Remote/UTMRemoteServer.swift index 7a50dede6..0dd017167 100644 --- a/Remote/UTMRemoteServer.swift +++ b/Remote/UTMRemoteServer.swift @@ -131,7 +131,7 @@ actor UTMRemoteServer { registerNotifications() listener = Task { await withErrorNotification { - if isServerExternal && serverPort > 0 { + if isServerExternal && serverPort > 0 && serverPort <= UInt16.max { natPort = Port.TCP(internalPort: UInt16(serverPort)) natPort!.mappingChangedHandler = { port in Task { @@ -146,7 +146,7 @@ actor UTMRemoteServer { } } } - let port = serverPort > 0 ? NWEndpoint.Port(integerLiteral: UInt16(serverPort)) : .any + let port = serverPort > 0 && serverPort <= UInt16.max ? NWEndpoint.Port(integerLiteral: UInt16(serverPort)) : .any for try await connection in Connection.advertise(on: port, forServiceType: service, txtRecord: metadata, connectionQueue: connectionQueue, identity: keyManager.identity) { let connection = try? await Connection(connection: connection, connectionQueue: connectionQueue) { connection, error in Task { @@ -683,13 +683,16 @@ extension UTMRemoteServer { } @MainActor - private func findVM(withId id: UUID) throws -> VMData { + private func findVM(withId id: UUID, allowNotLoaded: Bool = false) throws -> VMData { let vm = data.virtualMachines.first(where: { $0.id == id }) - if let vm = vm, let _ = vm.wrapped { - return vm - } else { - throw UTMRemoteServer.ServerError.notFound(id) + if let vm = vm { + if let _ = vm.wrapped { + return vm + } else if allowNotLoaded { + return vm + } } + throw UTMRemoteServer.ServerError.notFound(id) } @MainActor @@ -735,7 +738,7 @@ extension UTMRemoteServer { private func _getVirtualMachineInformation(parameters: M.GetVirtualMachineInformation.Request) async throws -> M.GetVirtualMachineInformation.Reply { let informations = try await Task { @MainActor in try parameters.ids.map { id in - let vm = try findVM(withId: id) + let vm = try findVM(withId: id, allowNotLoaded: true) let mountedDrives = vm.registryEntry?.externalDrives.mapValues({ $0.path }) ?? [:] let isTakeoverAllowed = data.vmWindows[vm] is VMRemoteSessionState && (vm.state == .started || vm.state == .paused) return M.VirtualMachineInformation(id: vm.id, diff --git a/Services/UTMAppleVirtualMachine.swift b/Services/UTMAppleVirtualMachine.swift index f42008659..71ba4b53a 100644 --- a/Services/UTMAppleVirtualMachine.swift +++ b/Services/UTMAppleVirtualMachine.swift @@ -170,6 +170,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { } } } + try? updateLastModified() } func start(options: UTMVirtualMachineStartOptions = []) async throws { @@ -358,6 +359,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { } } } + try? updateLastModified() } #endif @@ -393,6 +395,7 @@ final class UTMAppleVirtualMachine: UTMVirtualMachine { } await registryEntry.setIsSuspended(false) try FileManager.default.removeItem(at: vmSavedStateURL) + try? updateLastModified() } #if arch(arm64) diff --git a/Services/UTMQemuImage.swift b/Services/UTMQemuImage.swift index b835ccec8..5ad8353db 100644 --- a/Services/UTMQemuImage.swift +++ b/Services/UTMQemuImage.swift @@ -18,9 +18,12 @@ import Foundation import QEMUKitInternal @objc class UTMQemuImage: UTMProcess { + typealias ProgressCallback = (Float) -> Void + private var logOutput: String = "" private var processExitContinuation: CheckedContinuation? - + private var onProgress: ProgressCallback? + private init() { super.init(arguments: []) } @@ -52,11 +55,14 @@ import QEMUKitInternal } } - static func convert(from url: URL, toQcow2 dest: URL, withCompression compressed: Bool = false) async throws { + static func convert(from url: URL, toQcow2 dest: URL, withCompression compressed: Bool = false, onProgress: ProgressCallback? = nil) async throws { let qemuImg = UTMQemuImage() let srcBookmark = try url.bookmarkData() let dstBookmark = try dest.deletingLastPathComponent().bookmarkData() qemuImg.pushArgv("convert") + if onProgress != nil { + qemuImg.pushArgv("-p") + } if compressed { qemuImg.pushArgv("-c") qemuImg.pushArgv("-o") @@ -69,8 +75,10 @@ import QEMUKitInternal qemuImg.accessData(withBookmark: dstBookmark) qemuImg.pushArgv(dest.path) let logging = QEMULogging() + logging.delegate = qemuImg qemuImg.standardOutput = logging.standardOutput qemuImg.standardError = logging.standardError + qemuImg.onProgress = onProgress try await qemuImg.start() } @@ -175,8 +183,34 @@ extension UTMQemuImageError: LocalizedError { extension UTMQemuImage: QEMULoggingDelegate { func logging(_ logging: QEMULogging, didRecieveOutputLine line: String) { logOutput += line + if let onProgress = onProgress, line.contains("100%") { + if let progress = parseProgress(line) { + onProgress(progress) + } + } } func logging(_ logging: QEMULogging, didRecieveErrorLine line: String) { } } + +extension UTMQemuImage { + private func parseProgress(_ line: String) -> Float? { + let pattern = "\\(([0-9]+\\.[0-9]+)/100\\%\\)" + do { + let regex = try NSRegularExpression(pattern: pattern) + if let match = regex.firstMatch(in: line, range: NSRange(location: 0, length: line.count)) { + let range = match.range(at: 1) + if let swiftRange = Range(range, in: line) { + let floatValueString = line[swiftRange] + if let floatValue = Float(floatValueString) { + return floatValue + } + } + } + } catch { + + } + return nil + } +} diff --git a/Services/UTMQemuVirtualMachine.swift b/Services/UTMQemuVirtualMachine.swift index 122acac91..2994fea08 100644 --- a/Services/UTMQemuVirtualMachine.swift +++ b/Services/UTMQemuVirtualMachine.swift @@ -437,6 +437,11 @@ extension UTMQemuVirtualMachine { self.spicePort = spicePort } #endif + + // update timestamp + if !isRunningAsDisposible { + try? updateLastModified() + } } func start(options: UTMVirtualMachineStartOptions = []) async throws { @@ -549,6 +554,7 @@ extension UTMQemuVirtualMachine { if result.localizedCaseInsensitiveContains("Error") { throw UTMQemuVirtualMachineError.qemuError(result) } + try? updateLastModified() } func saveSnapshot(name: String? = nil) async throws { @@ -580,6 +586,7 @@ extension UTMQemuVirtualMachine { if result.localizedCaseInsensitiveContains("Error") { throw UTMQemuVirtualMachineError.qemuError(result) } + try? updateLastModified() } } @@ -784,18 +791,20 @@ extension UTMQemuVirtualMachine { } private func changeMedium(_ drive: UTMQemuConfigurationDrive, to url: URL, isAccessOnly: Bool) async throws { - _ = url.startAccessingSecurityScopedResource() + let isScopedAccess = url.startAccessingSecurityScopedResource() defer { - url.stopAccessingSecurityScopedResource() + if isScopedAccess { + url.stopAccessingSecurityScopedResource() + } } let tempBookmark = try url.bookmarkData() try await eject(drive, isForced: true) let file = try UTMRegistryEntry.File(url: url, isReadOnly: drive.isReadOnly) await registryEntry.setExternalDrive(file, forId: drive.id) - try await changeMedium(drive, with: tempBookmark, url: url, isSecurityScoped: false, isAccessOnly: isAccessOnly) + try await changeMedium(drive, with: tempBookmark, isSecurityScoped: false, isAccessOnly: isAccessOnly) } - private func changeMedium(_ drive: UTMQemuConfigurationDrive, with bookmark: Data, url: URL?, isSecurityScoped: Bool, isAccessOnly: Bool) async throws { + private func changeMedium(_ drive: UTMQemuConfigurationDrive, with bookmark: Data, isSecurityScoped: Bool, isAccessOnly: Bool) async throws { let system = await system ?? UTMProcess() let (success, bookmark, path) = await system.accessData(withBookmark: bookmark, securityScoped: isSecurityScoped) guard let bookmark = bookmark, let path = path, success else { @@ -803,7 +812,7 @@ extension UTMQemuVirtualMachine { } await registryEntry.updateExternalDriveRemoteBookmark(bookmark, forId: drive.id) if let qemu = await monitor, qemu.isConnected && !isAccessOnly { - try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path) + try qemu.changeMedium(forDrive: "drive\(drive.id)", path: path, locking: false) } } @@ -818,7 +827,7 @@ extension UTMQemuVirtualMachine { let id = drive.id if let bookmark = await registryEntry.externalDrives[id]?.remoteBookmark { // an image bookmark was saved while QEMU was running - try await changeMedium(drive, with: bookmark, url: nil, isSecurityScoped: true, isAccessOnly: !isMounting) + try await changeMedium(drive, with: bookmark, isSecurityScoped: true, isAccessOnly: !isMounting) } else if let localBookmark = await registryEntry.externalDrives[id]?.bookmark { // an image bookmark was saved while QEMU was NOT running let url = try URL(resolvingPersistentBookmarkData: localBookmark) diff --git a/Services/UTMSpiceVirtualMachine.swift b/Services/UTMSpiceVirtualMachine.swift index 7cd50a5b5..0be031bc3 100644 --- a/Services/UTMSpiceVirtualMachine.swift +++ b/Services/UTMSpiceVirtualMachine.swift @@ -105,9 +105,11 @@ extension UTMSpiceVirtualMachine { func changeSharedDirectory(to url: URL) async throws { await clearSharedDirectory() - _ = url.startAccessingSecurityScopedResource() + let isScopedAccess = url.startAccessingSecurityScopedResource() defer { - url.stopAccessingSecurityScopedResource() + if isScopedAccess { + url.stopAccessingSecurityScopedResource() + } } let file = try await UTMRegistryEntry.File(url: url, isReadOnly: config.sharing.isDirectoryShareReadOnly) await registryEntry.setSingleSharedDirectory(file) diff --git a/Services/UTMVirtualMachine.swift b/Services/UTMVirtualMachine.swift index b7eec04e1..14ae742b2 100644 --- a/Services/UTMVirtualMachine.swift +++ b/Services/UTMVirtualMachine.swift @@ -401,6 +401,14 @@ extension UTMVirtualMachine { try reload(from: newPath) try await updateRegistryBasics() // update bookmark } + // update last modified date + try? updateLastModified() + } + + /// Set the package's last modified time + /// - Parameter date: Last modified date + nonisolated func updateLastModified(to date: Date = Date()) throws { + try FileManager.default.setAttributes([.modificationDate: date], ofItemAtPath: pathUrl.path) } } diff --git a/UTM.xcodeproj/project.pbxproj b/UTM.xcodeproj/project.pbxproj index 46ed05f0e..75bbd388f 100644 --- a/UTM.xcodeproj/project.pbxproj +++ b/UTM.xcodeproj/project.pbxproj @@ -426,6 +426,14 @@ CE19392826DCB094005CEC17 /* RAMSlider.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE19392526DCB093005CEC17 /* RAMSlider.swift */; }; CE1AEC3F2B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; }; CE1AEC402B78B30700992AFC /* MacDeviceLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */; }; + CE231D422BDDF280006D6DC3 /* UTMDonateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE231D412BDDF280006D6DC3 /* UTMDonateView.swift */; }; + CE231D432BDDF280006D6DC3 /* UTMDonateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE231D412BDDF280006D6DC3 /* UTMDonateView.swift */; }; + CE231D462BDDFD03006D6DC3 /* UTMDonateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE231D452BDDFD03006D6DC3 /* UTMDonateStore.swift */; }; + CE231D472BDDFD03006D6DC3 /* UTMDonateStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE231D452BDDFD03006D6DC3 /* UTMDonateStore.swift */; }; + CE231D522BE03617006D6DC3 /* SwiftCopyfile in Frameworks */ = {isa = PBXBuildFile; productRef = CE231D512BE03617006D6DC3 /* SwiftCopyfile */; }; + CE231D542BE03630006D6DC3 /* SwiftCopyfile in Frameworks */ = {isa = PBXBuildFile; productRef = CE231D532BE03630006D6DC3 /* SwiftCopyfile */; }; + CE231D562BE03636006D6DC3 /* SwiftCopyfile in Frameworks */ = {isa = PBXBuildFile; productRef = CE231D552BE03636006D6DC3 /* SwiftCopyfile */; }; + CE231D5A2BE03791006D6DC3 /* SwiftCopyfile in Frameworks */ = {isa = PBXBuildFile; productRef = CE231D592BE03791006D6DC3 /* SwiftCopyfile */; }; CE25124729BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */; }; CE25124929BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */; }; CE25124B29BFE273000790AB /* UTMScriptable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE25124A29BFE273000790AB /* UTMScriptable.swift */; }; @@ -884,6 +892,10 @@ CEC794BD2949663C00121A9F /* UTMScripting.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEC794BB2949663C00121A9F /* UTMScripting.swift */; }; CED234ED254796E500ED0A57 /* NumberTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED234EC254796E500ED0A57 /* NumberTextField.swift */; }; CED234EE254796E500ED0A57 /* NumberTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED234EC254796E500ED0A57 /* NumberTextField.swift */; }; + CED779E52C78C82A00EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; }; + CED779E62C78C82A00EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; }; + CED779E72C78C82A00EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; }; + CED779E82C79062500EB82AE /* UTMTips.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED779E42C78C82A00EB82AE /* UTMTips.swift */; }; CED814E924C79F070042F0F1 /* VMConfigDriveCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */; }; CED814EA24C79F070042F0F1 /* VMConfigDriveCreateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */; }; CED814EC24C7C2850042F0F1 /* VMConfigInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */; }; @@ -1208,6 +1220,7 @@ CEFE96772B69A7CC000F00C9 /* VMRemoteSessionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE96762B69A7CC000F00C9 /* VMRemoteSessionState.swift */; }; CEFE98DF29485237007CB7A8 /* UTM.sdef in Resources */ = {isa = PBXBuildFile; fileRef = CEFE98DE29485237007CB7A8 /* UTM.sdef */; }; CEFE98E129485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFE98E029485776007CB7A8 /* UTMScriptingVirtualMachineImpl.swift */; }; + F6056EF32BE642F500FAEED8 /* Info-Remote-InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = F6056EF12BE642F500FAEED8 /* Info-Remote-InfoPlist.strings */; }; FF0307552A84E3B70049979B /* QEMULauncher-InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FF0307532A84E3B70049979B /* QEMULauncher-InfoPlist.strings */; }; FFB02A8C266CB09C006CD71A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFB02A8A266CB09C006CD71A /* InfoPlist.strings */; }; FFB02A8D266CB09C006CD71A /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = FFB02A8A266CB09C006CD71A /* InfoPlist.strings */; }; @@ -1786,6 +1799,9 @@ CE1AEC3E2B78B30700992AFC /* MacDeviceLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacDeviceLabel.swift; sourceTree = ""; }; CE20FAE62448D2BE0059AE11 /* VMScroll.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VMScroll.h; sourceTree = ""; }; CE20FAE72448D2BE0059AE11 /* VMScroll.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VMScroll.m; sourceTree = ""; }; + CE231D412BDDF280006D6DC3 /* UTMDonateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMDonateView.swift; sourceTree = ""; }; + CE231D442BDDFA61006D6DC3 /* Donation.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Donation.storekit; sourceTree = ""; }; + CE231D452BDDFD03006D6DC3 /* UTMDonateStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMDonateStore.swift; sourceTree = ""; }; CE25124629BFDB87000790AB /* UTMScriptingGuestProcessImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingGuestProcessImpl.swift; sourceTree = ""; }; CE25124829BFDBA6000790AB /* UTMScriptingGuestFileImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptingGuestFileImpl.swift; sourceTree = ""; }; CE25124A29BFE273000790AB /* UTMScriptable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMScriptable.swift; sourceTree = ""; }; @@ -2006,6 +2022,7 @@ CECF02572B70909900409FC0 /* Info-Remote.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "Info-Remote.plist"; sourceTree = ""; }; CED234EC254796E500ED0A57 /* NumberTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberTextField.swift; sourceTree = ""; }; CED779E92C7938D500EB82AE /* ar */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ar; path = ar.lproj/Localizable.strings; sourceTree = ""; }; + CED779E42C78C82A00EB82AE /* UTMTips.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UTMTips.swift; sourceTree = ""; }; CED814E824C79F070042F0F1 /* VMConfigDriveCreateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigDriveCreateView.swift; sourceTree = ""; }; CED814EB24C7C2850042F0F1 /* VMConfigInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VMConfigInfoView.swift; sourceTree = ""; }; CED814EE24C7EB760042F0F1 /* ImagePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = ""; }; @@ -2063,6 +2080,7 @@ E68D492228AC018E00D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = ""; }; E68D492328AC018E00D34C54 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = ""; }; E6F791192903EEC6000BAAC9 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/InfoPlist.strings"; sourceTree = ""; }; + F6056EF22BE642F500FAEED8 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/Info-Remote-InfoPlist.strings"; sourceTree = ""; }; F6DA2DA52AAFED5F0070DCD1 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/VMDisplayMetalViewInputAccessory.strings"; sourceTree = ""; }; F6DA2DA62AAFED5F0070DCD1 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/VMDisplayWindow.strings"; sourceTree = ""; }; F6DA2DA72AAFED5F0070DCD1 /* zh-HK */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-HK"; path = "zh-HK.lproj/InfoPlist.strings"; sourceTree = ""; }; @@ -2113,6 +2131,7 @@ CE2D933024AD46670059923A /* libgstvideoscale.a in Frameworks */, CE93759924BB821F0074066F /* IQKeyboardManagerSwift in Frameworks */, CE2D933124AD46670059923A /* MetalKit.framework in Frameworks */, + CE231D542BE03630006D6DC3 /* SwiftCopyfile in Frameworks */, CE2D933224AD46670059923A /* libgstvolume.a in Frameworks */, CE2D933324AD46670059923A /* libgstcoreelements.a in Frameworks */, CE2D933424AD46670059923A /* libgstvideorate.a in Frameworks */, @@ -2255,6 +2274,7 @@ CE0B6F0224AD677200FE012D /* libgstjpeg.a in Frameworks */, CE0B6EFC24AD677200FE012D /* libgstaudiotestsrc.a in Frameworks */, CE0B6EF824AD677200FE012D /* gstsdp-1.0.0.framework in Frameworks */, + CE231D522BE03617006D6DC3 /* SwiftCopyfile in Frameworks */, CE0B6EEA24AD677200FE012D /* libgstcoreelements.a in Frameworks */, 83993290272F4A400059355F /* ZIPFoundation in Frameworks */, ); @@ -2334,6 +2354,7 @@ CEA45F58263519B5002FA97D /* crypto.1.1.framework in Frameworks */, CEA45F59263519B5002FA97D /* gstpbutils-1.0.0.framework in Frameworks */, CEA45F5A263519B5002FA97D /* gstallocators-1.0.0.framework in Frameworks */, + CE231D562BE03636006D6DC3 /* SwiftCopyfile in Frameworks */, CEA45F5B263519B5002FA97D /* gstcheck-1.0.0.framework in Frameworks */, CEA45F5C263519B5002FA97D /* iconv.2.framework in Frameworks */, CEA45F5D263519B5002FA97D /* gstsdp-1.0.0.framework in Frameworks */, @@ -2418,6 +2439,7 @@ CEF7F66F2AEEDCC400E34952 /* gstallocators-1.0.0.framework in Frameworks */, CEF7F6702AEEDCC400E34952 /* gstcheck-1.0.0.framework in Frameworks */, CEF7F6712AEEDCC400E34952 /* iconv.2.framework in Frameworks */, + CE231D5A2BE03791006D6DC3 /* SwiftCopyfile in Frameworks */, CEF7F6722AEEDCC400E34952 /* gstsdp-1.0.0.framework in Frameworks */, CEF7F6742AEEDCC400E34952 /* ssl.1.1.framework in Frameworks */, CEF7F6762AEEDCC400E34952 /* pixman-1.0.framework in Frameworks */, @@ -2675,6 +2697,8 @@ CE08334A2B784FD400522C03 /* RemoteContentView.swift */, 841E58D02893AF5400137A20 /* UTMApp.swift */, CEBBF1A424B56A2900C15049 /* UTMDataExtension.swift */, + CE231D452BDDFD03006D6DC3 /* UTMDonateStore.swift */, + CE231D412BDDF280006D6DC3 /* UTMDonateView.swift */, 841E58CA28937EE200137A20 /* UTMExternalSceneDelegate.swift */, 841E58CD28937FED00137A20 /* UTMSingleWindowView.swift */, 842B9F8C28CC58B700031EE7 /* UTMPatches.swift */, @@ -2695,10 +2719,12 @@ CE95877426D74C2A0086BDE8 /* iOS.entitlements */, CE2D954F24AD4F980059923A /* Info.plist */, CECF02572B70909900409FC0 /* Info-Remote.plist */, + F6056EF12BE642F500FAEED8 /* Info-Remote-InfoPlist.strings */, FFB02A8A266CB09C006CD71A /* InfoPlist.strings */, CEB5C1192B8C4CD4008AAE5C /* Info-RemotePlist.strings */, CEC1B00A2BBB211C0088119D /* PrivacyInfo.xcprivacy */, 5286EC91243748AC007E6CBC /* Settings.bundle */, + CE231D442BDDFA61006D6DC3 /* Donation.storekit */, ); path = iOS; sourceTree = ""; @@ -2952,6 +2978,7 @@ 83034C0626AB630F006B4BAF /* UTMPendingVMView.swift */, 84909A8C27CACD5C005605F1 /* UTMPlaceholderVMView.swift */, 84909A9027CADAE0005605F1 /* UTMUnavailableVMView.swift */, + CED779E42C78C82A00EB82AE /* UTMTips.swift */, ); path = Shared; sourceTree = ""; @@ -3070,6 +3097,7 @@ 84A0A8892A47D5D10038F329 /* QEMUKit */, CE9B15372B11A4A7003A32DD /* SwiftConnect */, CE89CB0D2B8B1B5A006B2CC2 /* VisionKeyboardKit */, + CE231D532BE03630006D6DC3 /* SwiftCopyfile */, ); productName = UTM; productReference = CE2D93BE24AD46670059923A /* UTM.app */; @@ -3103,6 +3131,7 @@ 84A0A8872A47D5C50038F329 /* QEMUKit */, CE9B15352B11A491003A32DD /* SwiftConnect */, CEDD11C02B7C74D7004DDAC6 /* SwiftPortmap */, + CE231D512BE03617006D6DC3 /* SwiftCopyfile */, ); productName = UTM; productReference = CE2D951C24AD48BE0059923A /* UTM.app */; @@ -3153,6 +3182,7 @@ 84A0A88B2A47D5D70038F329 /* QEMUKit */, CE9B15392B11A4AE003A32DD /* SwiftConnect */, CE89CB0F2B8B1B6A006B2CC2 /* VisionKeyboardKit */, + CE231D552BE03636006D6DC3 /* SwiftCopyfile */, ); productName = UTM; productReference = CEA45FB9263519B5002FA97D /* UTM SE.app */; @@ -3202,6 +3232,7 @@ CEF7F6D52AEEEF7D00E34952 /* CocoaSpiceNoUsb */, CE9B153B2B11A4B4003A32DD /* SwiftConnect */, CE89CB112B8B1B7A006B2CC2 /* VisionKeyboardKit */, + CE231D592BE03791006D6DC3 /* SwiftCopyfile */, ); productName = UTM; productReference = CEF7F6D32AEEDCC400E34952 /* UTM Remote.app */; @@ -3276,6 +3307,7 @@ CE9B15342B11A491003A32DD /* XCRemoteSwiftPackageReference "SwiftConnect" */, CEDD11BF2B7C74D7004DDAC6 /* XCRemoteSwiftPackageReference "SwiftPortmap" */, CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */, + CE231D502BE03617006D6DC3 /* XCRemoteSwiftPackageReference "SwiftCopyfile" */, ); productRefGroup = CE550BCA225947990063E575 /* Products */; projectDirPath = ""; @@ -3373,6 +3405,7 @@ CEF7F6792AEEDCC400E34952 /* Icons in Resources */, CEB5C1172B8C4CD4008AAE5C /* Info-RemotePlist.strings in Resources */, CEF7F67B2AEEDCC400E34952 /* Localizable.strings in Resources */, + F6056EF32BE642F500FAEED8 /* Info-Remote-InfoPlist.strings in Resources */, CEF7F67C2AEEDCC400E34952 /* qemu in Resources */, CEF7F67D2AEEDCC400E34952 /* VMDisplayMetalViewInputAccessory.xib in Resources */, CEF7F67E2AEEDCC400E34952 /* Localizable.stringsdict in Resources */, @@ -3495,6 +3528,7 @@ buildActionMask = 2147483647; files = ( CE2D926A24AD46670059923A /* VMDisplayMetalViewController+Pointer.h in Sources */, + CE231D462BDDFD03006D6DC3 /* UTMDonateStore.swift in Sources */, 84C4D9022880CA8A00EC3B2B /* VMSettingsAddDeviceMenuView.swift in Sources */, CE2D956F24AD4F990059923A /* VMRemovableDrivesView.swift in Sources */, 8443EFF22845641600B2E6E2 /* UTMQemuConfigurationDrive.swift in Sources */, @@ -3621,6 +3655,7 @@ 848F71EC277A2F47006A0240 /* UTMSerialPortDelegate.swift in Sources */, CE2D955D24AD4F990059923A /* VMConfigSoundView.swift in Sources */, CE2D930424AD46670059923A /* UTMLegacyQemuConfiguration.m in Sources */, + CE231D422BDDF280006D6DC3 /* UTMDonateView.swift in Sources */, 841E999828AC817D003C6CB6 /* UTMQemuVirtualMachine.swift in Sources */, CE2D957924AD4F990059923A /* VMDetailsView.swift in Sources */, CE2D930524AD46670059923A /* VMDisplayMetalViewController.m in Sources */, @@ -3630,6 +3665,7 @@ CE2D958F24AD4FF00059923A /* VMCardView.swift in Sources */, 8432329028C2CDAD00CFBC97 /* VMNavigationListView.swift in Sources */, 841E58CE28937FED00137A20 /* UTMSingleWindowView.swift in Sources */, + CED779E52C78C82A00EB82AE /* UTMTips.swift in Sources */, CE2D930B24AD46670059923A /* UTMLegacyQemuConfiguration+Sharing.m in Sources */, 84C505AC28C588EC007CE8FF /* SizeTextField.swift in Sources */, 8471770627CC974F00D3A50B /* DefaultTextField.swift in Sources */, @@ -3693,6 +3729,7 @@ 8432329628C2ED9000CFBC97 /* FileBrowseField.swift in Sources */, 848A98C2286A2257006F0550 /* UTMAppleConfigurationMacPlatform.swift in Sources */, 84B36D2B27B790BE00C22685 /* DestructiveButton.swift in Sources */, + CED779E82C79062500EB82AE /* UTMTips.swift in Sources */, CE9B154A2B12A87E003A32DD /* GenerateKey.c in Sources */, CE020BAC24AEE00000B44AB6 /* UTMLoggingSwift.swift in Sources */, 848D99BA28630A780055C215 /* VMConfigSerialView.swift in Sources */, @@ -3858,6 +3895,7 @@ 84C505AD28C588EC007CE8FF /* SizeTextField.swift in Sources */, CEA45E27263519B5002FA97D /* VMRemovableDrivesView.swift in Sources */, CEF0306E26A2AFDF00667B63 /* VMWizardOSView.swift in Sources */, + CE231D432BDDF280006D6DC3 /* UTMDonateView.swift in Sources */, CE65BABF26A4D8DD0001BD6B /* VMConfigDisplayConsoleView.swift in Sources */, 848F71ED277A2F47006A0240 /* UTMSerialPortDelegate.swift in Sources */, 83A004BA26A8CC95001AC09E /* UTMDownloadTask.swift in Sources */, @@ -3896,12 +3934,14 @@ CEA45E63263519B5002FA97D /* UTMLegacyViewState.m in Sources */, CEA45E64263519B5002FA97D /* UTMLoggingSwift.swift in Sources */, 841619A7284315C1000034B2 /* UTMQemuConfiguration.swift in Sources */, + CED779E62C78C82A00EB82AE /* UTMTips.swift in Sources */, 84C2E8662AA429E800B17308 /* VMWizardContent.swift in Sources */, CEF0305F26A2AFDF00667B63 /* VMWizardState.swift in Sources */, CEA45E69263519B5002FA97D /* VMConfigQEMUView.swift in Sources */, CEA45E6A263519B5002FA97D /* VMDisplayMetalViewController+Touch.m in Sources */, CEA45E6B263519B5002FA97D /* UTMLegacyQemuConfiguration+Display.m in Sources */, CEA45E6C263519B5002FA97D /* UTMVirtualMachine.swift in Sources */, + CE231D472BDDFD03006D6DC3 /* UTMDonateStore.swift in Sources */, 84B36D2A27B790BE00C22685 /* DestructiveButton.swift in Sources */, CEF469EF2BD2D165005A0B68 /* VMWizardStartViewTCI.swift in Sources */, 842B9F8E28CC58B700031EE7 /* UTMPatches.swift in Sources */, @@ -4123,6 +4163,7 @@ CEF7F5F92AEEDCC400E34952 /* VMToolbarDriveMenuView.swift in Sources */, CE08334B2B784FD400522C03 /* RemoteContentView.swift in Sources */, CEF7F5FA2AEEDCC400E34952 /* VMSettingsView.swift in Sources */, + CED779E72C78C82A00EB82AE /* UTMTips.swift in Sources */, CEF7F5FB2AEEDCC400E34952 /* VMDisplayViewController.swift in Sources */, CEF7F5FC2AEEDCC400E34952 /* VMWizardStartView.swift in Sources */, CEF7F5FD2AEEDCC400E34952 /* QEMUConstantGenerated.swift in Sources */, @@ -4304,6 +4345,14 @@ name = Localizable.stringsdict; sourceTree = ""; }; + F6056EF12BE642F500FAEED8 /* Info-Remote-InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + F6056EF22BE642F500FAEED8 /* zh-HK */, + ); + name = "Info-Remote-InfoPlist.strings"; + sourceTree = ""; + }; FF0307532A84E3B70049979B /* QEMULauncher-InfoPlist.strings */ = { isa = PBXVariantGroup; children = ( @@ -5180,6 +5229,14 @@ minimumVersion = 1.5.3; }; }; + CE231D502BE03617006D6DC3 /* XCRemoteSwiftPackageReference "SwiftCopyfile" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/osy/SwiftCopyfile.git"; + requirement = { + branch = main; + kind = branch; + }; + }; CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/utmapp/VisionKeyboardKit.git"; @@ -5393,6 +5450,26 @@ package = CE020BA524AEDEF000B44AB6 /* XCRemoteSwiftPackageReference "swift-log" */; productName = Logging; }; + CE231D512BE03617006D6DC3 /* SwiftCopyfile */ = { + isa = XCSwiftPackageProductDependency; + package = CE231D502BE03617006D6DC3 /* XCRemoteSwiftPackageReference "SwiftCopyfile" */; + productName = SwiftCopyfile; + }; + CE231D532BE03630006D6DC3 /* SwiftCopyfile */ = { + isa = XCSwiftPackageProductDependency; + package = CE231D502BE03617006D6DC3 /* XCRemoteSwiftPackageReference "SwiftCopyfile" */; + productName = SwiftCopyfile; + }; + CE231D552BE03636006D6DC3 /* SwiftCopyfile */ = { + isa = XCSwiftPackageProductDependency; + package = CE231D502BE03617006D6DC3 /* XCRemoteSwiftPackageReference "SwiftCopyfile" */; + productName = SwiftCopyfile; + }; + CE231D592BE03791006D6DC3 /* SwiftCopyfile */ = { + isa = XCSwiftPackageProductDependency; + package = CE231D502BE03617006D6DC3 /* XCRemoteSwiftPackageReference "SwiftCopyfile" */; + productName = SwiftCopyfile; + }; CE89CB0D2B8B1B5A006B2CC2 /* VisionKeyboardKit */ = { isa = XCSwiftPackageProductDependency; package = CE89CB0C2B8B1B49006B2CC2 /* XCRemoteSwiftPackageReference "VisionKeyboardKit" */; diff --git a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 3d420c8b7..d5f72bb46 100644 --- a/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/UTM.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "c9482d61e795c27df5a7c772a0750aefc9164d93bd871138a9b229c9f08c6fab", "pins" : [ { "identity" : "altkit", @@ -51,7 +52,7 @@ "location" : "https://github.com/utmapp/QEMUKit.git", "state" : { "branch" : "main", - "revision" : "06b806f61aeeea8efff99a98b058defcf3632e2e" + "revision" : "c0c978d5566928b2f2b93005c2aa720bba01157a" } }, { @@ -81,6 +82,15 @@ "revision" : "af855e47ca222da163cc7f4f185230f36ba8694a" } }, + { + "identity" : "swiftcopyfile", + "kind" : "remoteSourceControl", + "location" : "https://github.com/osy/SwiftCopyfile.git", + "state" : { + "branch" : "main", + "revision" : "4da383bceaa5a5aec0ceb5789eed9bef2fa93f26" + } + }, { "identity" : "swiftportmap", "kind" : "remoteSourceControl", @@ -127,5 +137,5 @@ } } ], - "version" : 2 + "version" : 3 } diff --git a/UTM.xcodeproj/xcshareddata/xcschemes/iOS-SE.xcscheme b/UTM.xcodeproj/xcshareddata/xcschemes/iOS-SE.xcscheme index 00a0fb665..e1403330a 100644 --- a/UTM.xcodeproj/xcshareddata/xcschemes/iOS-SE.xcscheme +++ b/UTM.xcodeproj/xcshareddata/xcschemes/iOS-SE.xcscheme @@ -51,6 +51,9 @@ ReferencedContainer = "container:UTM.xcodeproj"> + + +Date: Thu, 22 Aug 2024 16:42:50 -0500 +Subject: [PATCH] block: support locking on change medium + +New optional argument for 'blockdev-change-medium' QAPI command to allow +the caller to specify if they wish to enable file locking. +--- + block/qapi-sysemu.c | 22 ++++++++++++++++++++++ + monitor/hmp-cmds.c | 1 + + qapi/block.json | 23 ++++++++++++++++++++++- + ui/cocoa/app_controller.m | 1 + + 4 files changed, 46 insertions(+), 1 deletion(-) + +diff --git a/block/qapi-sysemu.c b/block/qapi-sysemu.c +index 680c7ee342..888e13e539 100644 +--- a/block/qapi-sysemu.c ++++ b/block/qapi-sysemu.c +@@ -321,6 +321,8 @@ void qmp_blockdev_change_medium(bool has_device, const char *device, + bool has_force, bool force, + bool has_read_only, + BlockdevChangeReadOnlyMode read_only, ++ bool has_file_locking_mode, ++ BlockdevChangeFileLockingMode file_locking_mode, + Error **errp) + { + BlockBackend *blk; +@@ -374,6 +376,26 @@ void qmp_blockdev_change_medium(bool has_device, const char *device, + qdict_put_str(options, "driver", format); + } + ++ if (!has_file_locking_mode) { ++ file_locking_mode = BLOCKDEV_CHANGE_FILE_LOCKING_MODE_AUTO; ++ } ++ ++ switch (file_locking_mode) { ++ case BLOCKDEV_CHANGE_FILE_LOCKING_MODE_AUTO: ++ break; ++ ++ case BLOCKDEV_CHANGE_FILE_LOCKING_MODE_OFF: ++ qdict_put_str(options, "file.locking", "off"); ++ break; ++ ++ case BLOCKDEV_CHANGE_FILE_LOCKING_MODE_ON: ++ qdict_put_str(options, "file.locking", "on"); ++ break; ++ ++ default: ++ abort(); ++ } ++ + medium_bs = bdrv_open(filename, NULL, options, bdrv_flags, errp); + if (!medium_bs) { + goto fail; +diff --git a/monitor/hmp-cmds.c b/monitor/hmp-cmds.c +index 01b789a79e..9e42634c17 100644 +--- a/monitor/hmp-cmds.c ++++ b/monitor/hmp-cmds.c +@@ -1499,6 +1499,7 @@ void hmp_change(Monitor *mon, const QDict *qdict) + qmp_blockdev_change_medium(true, device, false, NULL, target, + !!arg, arg, true, force, + !!read_only, read_only_mode, ++ false, 0, + &err); + } + +diff --git a/qapi/block.json b/qapi/block.json +index 5fe068f903..0034ebe941 100644 +--- a/qapi/block.json ++++ b/qapi/block.json +@@ -303,6 +303,23 @@ + { 'enum': 'BlockdevChangeReadOnlyMode', + 'data': ['retain', 'read-only', 'read-write'] } + ++## ++# @BlockdevChangeFileLockingMode: ++# ++# Specifies the new locking mode of a file image passed to the ++# @blockdev-change-medium command. ++# ++# @auto: Use locking if API is available ++# ++# @off: Disable file image locking ++# ++# @on: Enable file image locking ++# ++# Since: 9.2 ++## ++{ 'enum': 'BlockdevChangeFileLockingMode', ++ 'data': ['auto', 'off', 'on'] } ++ + ## + # @blockdev-change-medium: + # +@@ -324,6 +341,9 @@ + # @read-only-mode: change the read-only mode of the device; defaults + # to 'retain' + # ++# @file-locking-mode: change the locking mode of the file image; defaults ++# to 'auto' ++# + # @force: if false (the default), an eject request through blockdev-open-tray + # will be sent to the guest if it has locked the tray (and the tray + # will not be opened immediately); if true, the tray will be opened +@@ -371,7 +391,8 @@ + 'filename': 'str', + '*format': 'str', + '*force': 'bool', +- '*read-only-mode': 'BlockdevChangeReadOnlyMode' } } ++ '*read-only-mode': 'BlockdevChangeReadOnlyMode', ++ '*file-locking-mode': 'BlockdevChangeFileLockingMode' } } + + ## + # @DEVICE_TRAY_MOVED: +diff --git a/ui/cocoa/app_controller.m b/ui/cocoa/app_controller.m +index 496036162d..0a062950cf 100644 +--- a/ui/cocoa/app_controller.m ++++ b/ui/cocoa/app_controller.m +@@ -373,6 +373,7 @@ - (void)changeDeviceMedia:(id)sender + "raw", + true, false, + false, 0, ++ false, 0, + &err); + qemu_mutex_unlock_iothread(); + handleAnyDeviceErrors(err); +-- +2.41.0 + diff --git a/scripts/build_utm.sh b/scripts/build_utm.sh index 00fa6a5b3..155eaa149 100755 --- a/scripts/build_utm.sh +++ b/scripts/build_utm.sh @@ -7,7 +7,7 @@ command -v realpath >/dev/null 2>&1 || realpath() { BASEDIR="$(dirname "$(realpath $0)")" usage () { - echo "Usage: $(basename $0) [-t teamid] [-p platform] [-s scheme] [-a architecture] [-t targetversion] [-o output]" + echo "Usage: $(basename $0) [-t teamid] [-k SDK] [-s scheme] [-a architecture] [-o output]" echo "" echo " -t teamid Team Identifier for app groups. Optional for iOS. Required for macOS." echo " -k sdk Target SDK. Default iphoneos. [iphoneos|iphonesimulator|xros|xrsimulator|macosx]" diff --git a/scripts/package.sh b/scripts/package.sh index b08062675..6465f9bd0 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -142,7 +142,7 @@ cat >"$DEB_TMP/DEBIAN/control" <=14.0), firmware-sbin, net.angelxwind.appsyncunified Installed-Size: ${SIZE_KIB} Maintainer: osy