UI and behavior changes
This commit is contained in:
parent
2932ac2f48
commit
6d2ee05786
@ -399,6 +399,7 @@
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@ -429,6 +430,7 @@
|
||||
ENABLE_PREVIEWS = YES;
|
||||
ENABLE_USER_SELECTED_FILES = readwrite;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
||||
@ -1,6 +1,15 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.240",
|
||||
"green" : "0.630",
|
||||
"red" : "0.360"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
|
||||
@ -22,13 +22,31 @@ struct ContentView: View {
|
||||
var body: some View {
|
||||
NavigationSplitView {
|
||||
VStack(spacing: 0) {
|
||||
SidebarSourcesHeaderView(addSourceAction: pickFolder)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 4)
|
||||
|
||||
List(selection: $selectedSidebarSelection) {
|
||||
ForEach(library.sources) { source in
|
||||
Section(source.displayName) {
|
||||
ForEach(sidebarFilters(for: source)) { filter in
|
||||
SidebarFilterRow(filter: filter)
|
||||
.tag(filter.selection as SidebarSelection?)
|
||||
SourceHeaderRow(title: source.displayName)
|
||||
.listRowSeparator(.hidden)
|
||||
.padding(.top, 6)
|
||||
.contextMenu {
|
||||
Button("Rescan \"\(source.displayName)\"") {
|
||||
library.rescanSource(withID: source.id)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Remove \"\(source.displayName)\"", role: .destructive) {
|
||||
removeSource(source.id)
|
||||
}
|
||||
}
|
||||
|
||||
ForEach(sidebarFilters(for: source)) { filter in
|
||||
SidebarFilterRow(filter: filter, isIndented: true)
|
||||
.tag(filter.selection as SidebarSelection?)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -94,7 +112,6 @@ struct ContentView: View {
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.tint(.minecraftAccent)
|
||||
.searchable(text: $searchText, placement: .toolbar, prompt: searchPrompt)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .primaryAction) {
|
||||
@ -122,29 +139,6 @@ struct ContentView: View {
|
||||
}
|
||||
.disabled(isPerformingItemAction)
|
||||
}
|
||||
|
||||
Button {
|
||||
pickFolder()
|
||||
} label: {
|
||||
Label("Add Source", systemImage: "plus")
|
||||
}
|
||||
|
||||
if let currentSource = currentSource {
|
||||
Menu {
|
||||
Button("Rescan \"\(currentSource.displayName)\"") {
|
||||
library.rescanSource(withID: currentSource.id)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Remove \"\(currentSource.displayName)\"", role: .destructive) {
|
||||
removeSource(currentSource.id)
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis.circle")
|
||||
}
|
||||
.help("Source actions")
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: filteredItems.map(\.id)) { _, filteredIDs in
|
||||
@ -625,6 +619,7 @@ private struct SidebarFilter: Identifiable, Hashable {
|
||||
|
||||
private struct SidebarFilterRow: View {
|
||||
let filter: SidebarFilter
|
||||
let isIndented: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
@ -639,6 +634,37 @@ private struct SidebarFilterRow: View {
|
||||
Text(filter.count, format: .number)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
.padding(.leading, isIndented ? 16 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SidebarSourcesHeaderView: View {
|
||||
let addSourceAction: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text("Sources")
|
||||
.font(.headline)
|
||||
.foregroundStyle(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: addSourceAction) {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
.help("Add Source")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SourceHeaderRow: View {
|
||||
let title: String
|
||||
|
||||
var body: some View {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@ -688,7 +714,7 @@ private struct SidebarFooterView: View {
|
||||
case .failure:
|
||||
return .red
|
||||
case .success:
|
||||
return .minecraftAccent
|
||||
return .appAccent
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -765,6 +791,7 @@ private struct ItemDetailView: View {
|
||||
let primaryAction: () -> Void
|
||||
let shareAction: (NSView) -> Void
|
||||
let revealAction: () -> Void
|
||||
@State private var isTechnicalDetailsExpanded = false
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
@ -851,7 +878,7 @@ private struct ItemDetailView: View {
|
||||
}
|
||||
|
||||
detailCard {
|
||||
DisclosureGroup("Technical Details") {
|
||||
DisclosureGroup(isExpanded: $isTechnicalDetailsExpanded) {
|
||||
VStack(alignment: .leading, spacing: 18) {
|
||||
detailRow(title: "Folder ID", value: item.folderID)
|
||||
detailRow(title: "Folder Path", value: item.folderURL.path)
|
||||
@ -885,6 +912,15 @@ private struct ItemDetailView: View {
|
||||
}
|
||||
}
|
||||
.padding(.top, 8)
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Technical Details")
|
||||
Spacer()
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
isTechnicalDetailsExpanded.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -908,6 +944,9 @@ private struct ItemDetailView: View {
|
||||
.font(.subheadline.weight(.semibold))
|
||||
|
||||
ForEach(packs) { pack in
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
PackReferenceIconView(iconURL: pack.iconURL)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(pack.name)
|
||||
|
||||
@ -920,6 +959,7 @@ private struct ItemDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func detailRow(title: String, value: String) -> some View {
|
||||
@ -1013,6 +1053,29 @@ private struct SharingPickerButton: NSViewRepresentable {
|
||||
}
|
||||
}
|
||||
|
||||
private struct PackReferenceIconView: View {
|
||||
let iconURL: URL?
|
||||
|
||||
var body: some View {
|
||||
if let image = loadImage(from: iconURL) {
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: 34, height: 34)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(.quaternary)
|
||||
.frame(width: 34, height: 34)
|
||||
.overlay(
|
||||
Image(systemName: "shippingbox")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct EmptySourcesView: View {
|
||||
let isDropTargeted: Bool
|
||||
let chooseFolder: () -> Void
|
||||
@ -1022,12 +1085,12 @@ private struct EmptySourcesView: View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 24)
|
||||
.strokeBorder(style: StrokeStyle(lineWidth: 2, dash: [10, 10]))
|
||||
.foregroundStyle(isDropTargeted ? Color.minecraftAccent : Color.secondary.opacity(0.25))
|
||||
.foregroundStyle(isDropTargeted ? Color.appAccent : Color.secondary.opacity(0.25))
|
||||
.frame(width: 220, height: 160)
|
||||
|
||||
Image(systemName: "folder.badge.plus")
|
||||
.font(.system(size: 56, weight: .regular))
|
||||
.foregroundStyle(isDropTargeted ? Color.minecraftAccent : Color.secondary)
|
||||
.foregroundStyle(isDropTargeted ? Color.appAccent : Color.secondary)
|
||||
}
|
||||
|
||||
VStack(spacing: 8) {
|
||||
@ -1080,8 +1143,8 @@ private struct LargeItemThumbnailView: View {
|
||||
if let image = loadImage(from: iconURL) {
|
||||
Image(nsImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: 420, minHeight: 260, maxHeight: 340)
|
||||
.aspectRatio(image.size, contentMode: .fit)
|
||||
.frame(maxWidth: 420, maxHeight: 340)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 28))
|
||||
} else {
|
||||
RoundedRectangle(cornerRadius: 28)
|
||||
@ -1120,7 +1183,7 @@ private func loadImage(from url: URL?) -> NSImage? {
|
||||
}
|
||||
|
||||
private extension Color {
|
||||
static let minecraftAccent = Color(red: 0.36, green: 0.63, blue: 0.24)
|
||||
static let appAccent = Color("AccentColor")
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
|
||||
@ -66,6 +66,7 @@ struct ContentPackReference: Identifiable, Hashable, Sendable {
|
||||
let id: String
|
||||
let name: String
|
||||
let type: MinecraftContentType
|
||||
let iconURL: URL?
|
||||
let uuid: String?
|
||||
let version: String?
|
||||
let source: PackSource
|
||||
@ -73,11 +74,13 @@ struct ContentPackReference: Identifiable, Hashable, Sendable {
|
||||
nonisolated init(
|
||||
name: String,
|
||||
type: MinecraftContentType,
|
||||
iconURL: URL? = nil,
|
||||
uuid: String? = nil,
|
||||
version: String? = nil,
|
||||
source: PackSource
|
||||
) {
|
||||
self.type = type
|
||||
self.iconURL = iconURL
|
||||
self.uuid = uuid?.lowercased()
|
||||
self.version = version
|
||||
self.source = source
|
||||
|
||||
@ -134,18 +134,36 @@ enum ContentPackageExporter {
|
||||
}
|
||||
|
||||
nonisolated private static func sanitizedFilename(_ value: String) -> String {
|
||||
let transliterated = portableASCIIString(from: value)
|
||||
let invalidCharacters = CharacterSet(charactersIn: "/:\\?%*|\"<>")
|
||||
let components = value.components(separatedBy: invalidCharacters)
|
||||
let components = transliterated.components(separatedBy: invalidCharacters)
|
||||
let collapsed = components.joined(separator: " ")
|
||||
.replacingOccurrences(of: "\n", with: " ")
|
||||
.replacingOccurrences(of: "\r", with: " ")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
let normalizedWhitespace = collapsed.replacingOccurrences(
|
||||
let allowedCharacters = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: " -_().,'&+"))
|
||||
let filteredScalars = collapsed.unicodeScalars.map { scalar in
|
||||
allowedCharacters.contains(scalar) ? Character(scalar) : " "
|
||||
}
|
||||
let filtered = String(filteredScalars)
|
||||
|
||||
let normalizedWhitespace = filtered.replacingOccurrences(
|
||||
of: "\\s+",
|
||||
with: " ",
|
||||
options: .regularExpression
|
||||
)
|
||||
let trimmedPunctuation = normalizedWhitespace.trimmingCharacters(in: CharacterSet(charactersIn: " .-_"))
|
||||
|
||||
return normalizedWhitespace.isEmpty ? "Minecraft Content" : normalizedWhitespace
|
||||
return trimmedPunctuation.isEmpty ? "Minecraft Content" : trimmedPunctuation
|
||||
}
|
||||
|
||||
nonisolated private static func portableASCIIString(from value: String) -> String {
|
||||
let mutable = NSMutableString(string: value) as CFMutableString
|
||||
|
||||
CFStringTransform(mutable, nil, kCFStringTransformToLatin, false)
|
||||
CFStringTransform(mutable, nil, kCFStringTransformStripCombiningMarks, false)
|
||||
|
||||
return mutable as String
|
||||
}
|
||||
}
|
||||
|
||||
@ -133,66 +133,64 @@ final class SourceLibrary: ObservableObject {
|
||||
refreshSidebarFooterState()
|
||||
|
||||
do {
|
||||
let discoveredItems = try await Task.detached(priority: .userInitiated) {
|
||||
try WorldScanner.discoverItems(in: sourceID)
|
||||
}.value
|
||||
|
||||
guard !Task.isCancelled else {
|
||||
return
|
||||
let enrichmentTracker = PendingEnrichmentTracker()
|
||||
let applyEnrichedItem: @MainActor (MinecraftContentItem) -> Void = { [weak self] enrichedItem in
|
||||
self?.handleEnrichedItem(enrichedItem, for: sourceID)
|
||||
}
|
||||
let discoveryStream = AsyncThrowingStream<MinecraftContentItem, Error> { continuation in
|
||||
let discoveryTask = Task.detached(priority: .userInitiated) {
|
||||
do {
|
||||
_ = try WorldScanner.discoverItems(in: sourceID) { item in
|
||||
continuation.yield(item)
|
||||
}
|
||||
continuation.finish()
|
||||
} catch {
|
||||
continuation.finish(throwing: error)
|
||||
}
|
||||
}
|
||||
|
||||
continuation.onTermination = { @Sendable _ in
|
||||
discoveryTask.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
var discoveredCount = 0
|
||||
|
||||
for try await item in discoveryStream {
|
||||
guard !Task.isCancelled else {
|
||||
break
|
||||
}
|
||||
|
||||
discoveredCount += 1
|
||||
updateSource(sourceID) { source in
|
||||
source.items = discoveredItems
|
||||
source.indexedItemCount = discoveredItems.count
|
||||
source.scanStatus = discoveredItems.isEmpty
|
||||
? "No Minecraft content found."
|
||||
: "Found \(discoveredItems.count) items. Loading details..."
|
||||
source.items.append(item)
|
||||
source.indexedItemCount = discoveredCount
|
||||
source.scanStatus = "Found \(discoveredCount) items. Loading details..."
|
||||
}
|
||||
refreshSidebarFooterState()
|
||||
|
||||
var loadedCount = 0
|
||||
await enrichmentTracker.beginEnrichment()
|
||||
let tracker = enrichmentTracker
|
||||
|
||||
await withTaskGroup(of: MinecraftContentItem.self) { group in
|
||||
for item in discoveredItems {
|
||||
group.addTask {
|
||||
WorldScanner.enrich(item: item)
|
||||
Task.detached(priority: .utility) {
|
||||
let enrichedItem = WorldScanner.enrich(item: item)
|
||||
await applyEnrichedItem(enrichedItem)
|
||||
await tracker.finishEnrichment()
|
||||
}
|
||||
}
|
||||
|
||||
for await enrichedItem in group {
|
||||
guard !Task.isCancelled else {
|
||||
return
|
||||
}
|
||||
await enrichmentTracker.markDiscoveryFinished()
|
||||
await enrichmentTracker.waitForCompletion()
|
||||
|
||||
loadedCount += 1
|
||||
updateSource(sourceID) { source in
|
||||
guard let index = source.items.firstIndex(where: { $0.id == enrichedItem.id }) else {
|
||||
return
|
||||
}
|
||||
|
||||
source.items[index] = enrichedItem
|
||||
source.indexedDetailCount = loadedCount
|
||||
source.items.sort(by: WorldScanner.sortItems)
|
||||
|
||||
if loadedCount == discoveredItems.count {
|
||||
source.scanStatus = "Loaded \(loadedCount) items."
|
||||
source.isScanning = false
|
||||
source.lastScanDate = Date()
|
||||
} else {
|
||||
source.scanStatus = "Loaded details for \(loadedCount) of \(discoveredItems.count) items..."
|
||||
}
|
||||
}
|
||||
refreshSidebarFooterState()
|
||||
}
|
||||
}
|
||||
|
||||
if discoveredItems.isEmpty {
|
||||
updateSource(sourceID) { source in
|
||||
source.scanStatus = source.indexedItemCount == 0
|
||||
? "No Minecraft content found."
|
||||
: "Loaded \(source.indexedDetailCount) items."
|
||||
source.isScanning = false
|
||||
source.lastScanDate = Date()
|
||||
}
|
||||
refreshSidebarFooterState()
|
||||
}
|
||||
} catch {
|
||||
guard !Task.isCancelled else {
|
||||
return
|
||||
@ -209,6 +207,22 @@ final class SourceLibrary: ObservableObject {
|
||||
scanTasks[sourceID] = nil
|
||||
}
|
||||
|
||||
private func handleEnrichedItem(_ enrichedItem: MinecraftContentItem, for sourceID: URL) {
|
||||
updateSource(sourceID) { source in
|
||||
guard let index = source.items.firstIndex(where: { $0.id == enrichedItem.id }) else {
|
||||
return
|
||||
}
|
||||
|
||||
source.items[index] = enrichedItem
|
||||
source.indexedDetailCount += 1
|
||||
|
||||
if source.indexedDetailCount < source.indexedItemCount {
|
||||
source.scanStatus = "Loaded details for \(source.indexedDetailCount) of \(source.indexedItemCount) items..."
|
||||
}
|
||||
}
|
||||
refreshSidebarFooterState()
|
||||
}
|
||||
|
||||
private func updateSource(_ sourceID: URL, mutate: (inout MinecraftSource) -> Void) {
|
||||
guard let index = sources.firstIndex(where: { $0.id == sourceID }) else {
|
||||
return
|
||||
@ -268,3 +282,42 @@ final class SourceLibrary: ObservableObject {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private actor PendingEnrichmentTracker {
|
||||
private var pendingCount = 0
|
||||
private var discoveryFinished = false
|
||||
private var continuation: CheckedContinuation<Void, Never>?
|
||||
|
||||
func beginEnrichment() {
|
||||
pendingCount += 1
|
||||
}
|
||||
|
||||
func finishEnrichment() {
|
||||
pendingCount -= 1
|
||||
resumeIfNeeded()
|
||||
}
|
||||
|
||||
func markDiscoveryFinished() {
|
||||
discoveryFinished = true
|
||||
resumeIfNeeded()
|
||||
}
|
||||
|
||||
func waitForCompletion() async {
|
||||
guard !(discoveryFinished && pendingCount == 0) else {
|
||||
return
|
||||
}
|
||||
|
||||
await withCheckedContinuation { continuation in
|
||||
self.continuation = continuation
|
||||
}
|
||||
}
|
||||
|
||||
private func resumeIfNeeded() {
|
||||
guard discoveryFinished, pendingCount == 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
continuation?.resume()
|
||||
continuation = nil
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,7 +8,10 @@
|
||||
import Foundation
|
||||
|
||||
enum WorldScanner {
|
||||
nonisolated static func discoverItems(in searchRootURL: URL) throws -> [MinecraftContentItem] {
|
||||
nonisolated static func discoverItems(
|
||||
in searchRootURL: URL,
|
||||
onDiscovered: @Sendable (MinecraftContentItem) -> Void = { _ in }
|
||||
) throws -> [MinecraftContentItem] {
|
||||
let fileManager = FileManager.default
|
||||
let resourceKeys: [URLResourceKey] = [.isDirectoryKey]
|
||||
|
||||
@ -40,15 +43,15 @@ enum WorldScanner {
|
||||
}
|
||||
|
||||
if isCandidateItem(at: childDirectory, type: contentType, fileManager: fileManager) {
|
||||
seenItemURLs.insert(itemURL)
|
||||
discoveredItems.append(
|
||||
MinecraftContentItem(
|
||||
let item = MinecraftContentItem(
|
||||
folderURL: childDirectory,
|
||||
folderName: childDirectory.lastPathComponent,
|
||||
contentType: contentType,
|
||||
collectionRootURL: directoryURL
|
||||
)
|
||||
)
|
||||
seenItemURLs.insert(itemURL)
|
||||
discoveredItems.append(item)
|
||||
onDiscovered(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -157,6 +160,19 @@ enum WorldScanner {
|
||||
return name
|
||||
}
|
||||
|
||||
nonisolated private static func packIconURL(in directoryURL: URL, fileManager: FileManager) -> URL? {
|
||||
let candidateNames = ["pack_icon.png", "pack_icon.jpeg", "pack_icon.jpg"]
|
||||
|
||||
for candidateName in candidateNames {
|
||||
let candidateURL = directoryURL.appendingPathComponent(candidateName)
|
||||
if fileManager.fileExists(atPath: candidateURL.path) {
|
||||
return candidateURL
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
nonisolated private static func iconURL(for item: MinecraftContentItem, fileManager: FileManager) -> URL? {
|
||||
let candidateNames: [String]
|
||||
|
||||
@ -286,20 +302,21 @@ enum WorldScanner {
|
||||
return jsonObject.compactMap { entry in
|
||||
let uuid = (entry["pack_id"] as? String)?.lowercased()
|
||||
let version = versionString(from: entry["version"])
|
||||
let resolvedName = uuid.flatMap {
|
||||
resolvedPackName(
|
||||
let resolvedPack = uuid.flatMap {
|
||||
resolvedPackReference(
|
||||
uuid: $0,
|
||||
type: type,
|
||||
worldCollectionRootURL: worldFolderURL.deletingLastPathComponent(),
|
||||
fileManager: fileManager
|
||||
)
|
||||
}
|
||||
let fallbackName = resolvedName ?? uuid ?? "Referenced Pack"
|
||||
let fallbackName = resolvedPack?.name ?? uuid ?? "Referenced Pack"
|
||||
return ContentPackReference(
|
||||
name: fallbackName,
|
||||
type: type,
|
||||
iconURL: resolvedPack?.iconURL,
|
||||
uuid: uuid,
|
||||
version: version,
|
||||
version: resolvedPack?.version ?? version,
|
||||
source: .referencedByWorld
|
||||
)
|
||||
}
|
||||
@ -352,18 +369,19 @@ enum WorldScanner {
|
||||
return ContentPackReference(
|
||||
name: name,
|
||||
type: type,
|
||||
iconURL: packIconURL(in: directoryURL, fileManager: fileManager),
|
||||
uuid: uuid,
|
||||
version: version,
|
||||
source: source
|
||||
)
|
||||
}
|
||||
|
||||
nonisolated private static func resolvedPackName(
|
||||
nonisolated private static func resolvedPackReference(
|
||||
uuid: String,
|
||||
type: MinecraftContentType,
|
||||
worldCollectionRootURL: URL,
|
||||
fileManager: FileManager
|
||||
) -> String? {
|
||||
) -> ContentPackReference? {
|
||||
let siblingCollectionURL = worldCollectionRootURL
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent(type.collectionFolderName, isDirectory: true)
|
||||
@ -388,7 +406,7 @@ enum WorldScanner {
|
||||
continue
|
||||
}
|
||||
|
||||
return reference.name
|
||||
return reference
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -12,6 +12,8 @@ struct World_Manager_for_MinecraftApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
.tint(Color("AccentColor"))
|
||||
}
|
||||
.windowToolbarStyle(.unifiedCompact(showsTitle: false))
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user