Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #5937 - improved VM icon selector UX #5969

Merged
merged 1 commit into from
Apr 30, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 140 additions & 51 deletions Platform/Shared/VMConfigInfoView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,16 +123,20 @@ 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
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)
Expand All @@ -141,18 +145,31 @@ 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)
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
Expand Down Expand Up @@ -190,17 +207,23 @@ private struct IconPreview: View {
Spacer()
#endif
Logo(logo: PlatformImage(contentsOfURL: url))
.padding()
#if !os(macOS)
Spacer()
#endif
}
}
}

#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) })
Expand All @@ -210,6 +233,7 @@ private struct IconSelect: View {
}

#if os(macOS)

typealias PlatformImage = NSImage
#else
typealias PlatformImage = UIImage
Expand All @@ -218,44 +242,35 @@ private struct IconSelect: View {
struct IconSelectModifier: ViewModifier {
@Environment(\.presentationMode) private var presentationMode: Binding<PresentationMode>

#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 {
Logo(logo: PlatformImage(contentsOfURL: icon))
Text(iconToTitle(icon))
.lineLimit(2)
.font(.footnote)
}
.padding(8)
.frame(width: iconGridSize, height: iconGridSize)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(current == icon ? Color.accentColor : Color.clear, lineWidth: 2)
)
}).buttonStyle(.plain)
}
}.modifier(IconSelectModifier())
Expand All @@ -271,9 +286,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",
]