Fix refactor
This commit is contained in:
parent
23db26dd43
commit
7d51075631
@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
World Manager for Minecraft/Models/MinecraftWorld.swift
Normal file
19
World Manager for Minecraft/Models/MinecraftWorld.swift
Normal 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
|
||||||
|
}
|
||||||
133
World Manager for Minecraft/Services/WorldScanner.swift
Normal file
133
World Manager for Minecraft/Services/WorldScanner.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user