Fix refactor

This commit is contained in:
John Burwell 2026-05-25 14:07:35 -05:00
parent 23db26dd43
commit 7d51075631
5 changed files with 241 additions and 66 deletions

View File

@ -5,55 +5,111 @@
// Created by John Burwell on 2026-05-25. // Created by John Burwell on 2026-05-25.
// //
import AppKit
import SwiftUI import SwiftUI
import SwiftData
struct ContentView: View { struct ContentView: View {
@Environment(\.modelContext) private var modelContext @StateObject private var scanner = WorldScanner()
@Query private var items: [Item] @State private var folderURL: URL?
@State private var selectedWorld: MinecraftWorld?
var body: some View { var body: some View {
NavigationSplitView { NavigationSplitView {
List { VStack(alignment: .leading, spacing: 12) {
ForEach(items) { item in Button("Choose Minecraft Worlds Folder...") {
NavigationLink { pickFolder()
Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
} label: {
Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
}
} }
.onDelete(perform: deleteItems)
if let folderURL {
Text(folderURL.path)
.font(.footnote)
.foregroundStyle(.secondary)
.textSelection(.enabled)
} else {
Text("No folder selected")
.font(.footnote)
.foregroundStyle(.secondary)
}
if scanner.isScanning {
ProgressView(scanner.scanStatus)
} else if !scanner.scanStatus.isEmpty {
Text(scanner.scanStatus)
.font(.footnote)
.foregroundStyle(.secondary)
}
if let scanError = scanner.scanError {
Text(scanError)
.font(.footnote)
.foregroundStyle(.red)
}
Spacer()
} }
.navigationSplitViewColumnWidth(min: 180, ideal: 200) .padding()
.toolbar { .navigationTitle("Folder")
ToolbarItem { } content: {
Button(action: addItem) { List(scanner.worlds, selection: $selectedWorld) { world in
Label("Add Item", systemImage: "plus") VStack(alignment: .leading, spacing: 4) {
} Text(world.displayName)
Text(world.folderName)
.font(.caption)
.foregroundStyle(.secondary)
} }
} }
.navigationTitle("Worlds")
} detail: { } detail: {
Text("Select an item") if let selectedWorld {
} VStack(alignment: .leading, spacing: 12) {
} Text(selectedWorld.displayName)
.font(.title2)
private func addItem() { Text(selectedWorld.folderName)
withAnimation { .font(.headline)
let newItem = Item(timestamp: Date()) .foregroundStyle(.secondary)
modelContext.insert(newItem)
}
}
private func deleteItems(offsets: IndexSet) { if let modifiedDate = selectedWorld.modifiedDate {
withAnimation { Text("Modified: \(modifiedDate.formatted(date: .abbreviated, time: .shortened))")
for index in offsets { }
modelContext.delete(items[index])
if let sizeBytes = selectedWorld.sizeBytes {
Text("Size: \(ByteCountFormatter.string(fromByteCount: sizeBytes, countStyle: .file))")
}
Spacer()
}
.padding()
} else {
Text("Select a world to see details")
.foregroundStyle(.secondary)
} }
} }
.navigationTitle("Minecraft World Manager")
}
private func pickFolder() {
let panel = NSOpenPanel()
panel.allowsMultipleSelection = false
panel.canChooseDirectories = true
panel.canChooseFiles = false
panel.title = "Choose Minecraft Worlds Folder"
guard panel.runModal() == .OK, let pickedURL = panel.url else {
return
}
folderURL = pickedURL
selectedWorld = nil
Task {
await scanner.scan(at: pickedURL)
}
} }
} }
#Preview { struct ContentView_Previews: PreviewProvider {
ContentView() static var previews: some View {
.modelContainer(for: Item.self, inMemory: true) ContentView()
}
} }

View File

@ -1,18 +0,0 @@
//
// Item.swift
// World Manager for Minecraft
//
// Created by John Burwell on 2026-05-25.
//
import Foundation
import SwiftData
@Model
final class Item {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}

View File

@ -0,0 +1,19 @@
//
// MinecraftWorld.swift
// World Manager for Minecraft
//
// Created by John Burwell on 2026-05-25.
//
import Foundation
struct MinecraftWorld: Identifiable, Hashable {
let id = UUID()
let folderURL: URL
let folderName: String
let displayName: String
let iconURL: URL?
let modifiedDate: Date?
let sizeBytes: Int64?
let isValidWorld: Bool
}

View File

@ -0,0 +1,133 @@
//
// WorldScanner.swift
// World Manager for Minecraft
//
// Created by John Burwell on 2026-05-25.
//
import Combine
import Foundation
@MainActor
final class WorldScanner: ObservableObject {
@Published var worlds: [MinecraftWorld] = []
@Published var isScanning = false
@Published var scanStatus = ""
@Published var scanError: String?
func scan(at parentFolderURL: URL) async {
isScanning = true
scanError = nil
scanStatus = "Scanning worlds..."
worlds = []
do {
let worlds = try await Task.detached(priority: .userInitiated) {
try Self.scanWorlds(at: parentFolderURL)
}.value
self.worlds = worlds
self.scanStatus = worlds.isEmpty ? "No worlds found." : "Found \(worlds.count) worlds."
} catch {
scanError = "Failed to scan folder: \(error.localizedDescription)"
scanStatus = ""
}
isScanning = false
}
nonisolated private static func scanWorlds(at parentFolderURL: URL) throws -> [MinecraftWorld] {
let fileManager = FileManager.default
let resourceKeys: Set<URLResourceKey> = [.isDirectoryKey, .contentModificationDateKey]
let children = try fileManager.contentsOfDirectory(
at: parentFolderURL,
includingPropertiesForKeys: Array(resourceKeys),
options: [.skipsHiddenFiles]
)
return try children.compactMap { childURL in
let values = try childURL.resourceValues(forKeys: resourceKeys)
guard values.isDirectory == true else {
return nil
}
let isValidWorld = isLikelyWorldFolder(childURL, fileManager: fileManager)
guard isValidWorld else {
return nil
}
let folderName = childURL.lastPathComponent
let displayName = readDisplayName(in: childURL, fallback: folderName)
let iconURL = iconURL(in: childURL, fileManager: fileManager)
let sizeBytes = folderSize(at: childURL, fileManager: fileManager)
return MinecraftWorld(
folderURL: childURL,
folderName: folderName,
displayName: displayName,
iconURL: iconURL,
modifiedDate: values.contentModificationDate,
sizeBytes: sizeBytes,
isValidWorld: true
)
}
.sorted { lhs, rhs in
lhs.displayName.localizedCaseInsensitiveCompare(rhs.displayName) == .orderedAscending
}
}
nonisolated private static func isLikelyWorldFolder(_ url: URL, fileManager: FileManager) -> Bool {
let levelDatURL = url.appendingPathComponent("level.dat")
let dbURL = url.appendingPathComponent("db", isDirectory: true)
let levelNameURL = url.appendingPathComponent("levelname.txt")
return fileManager.fileExists(atPath: levelDatURL.path)
|| fileManager.fileExists(atPath: dbURL.path)
|| fileManager.fileExists(atPath: levelNameURL.path)
}
nonisolated private static func readDisplayName(in worldURL: URL, fallback: String) -> String {
let levelNameURL = worldURL.appendingPathComponent("levelname.txt")
guard
let name = try? String(contentsOf: levelNameURL, encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines),
!name.isEmpty
else {
return fallback
}
return name
}
nonisolated private static func iconURL(in worldURL: URL, fileManager: FileManager) -> URL? {
let iconURL = worldURL.appendingPathComponent("world_icon.jpeg")
return fileManager.fileExists(atPath: iconURL.path) ? iconURL : nil
}
nonisolated private static func folderSize(at folderURL: URL, fileManager: FileManager) -> Int64? {
guard let enumerator = fileManager.enumerator(
at: folderURL,
includingPropertiesForKeys: [.isRegularFileKey, .fileSizeKey],
options: [.skipsHiddenFiles]
) else {
return nil
}
var totalSize: Int64 = 0
for case let fileURL as URL in enumerator {
guard
let values = try? fileURL.resourceValues(forKeys: [.isRegularFileKey, .fileSizeKey]),
values.isRegularFile == true,
let fileSize = values.fileSize
else {
continue
}
totalSize += Int64(fileSize)
}
return totalSize
}
}

View File

@ -6,27 +6,12 @@
// //
import SwiftUI import SwiftUI
import SwiftData
@main @main
struct World_Manager_for_MinecraftApp: App { struct World_Manager_for_MinecraftApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Item.self,
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene { var body: some Scene {
WindowGroup { WindowGroup {
ContentView() ContentView()
} }
.modelContainer(sharedModelContainer)
} }
} }