StikDebug/StikJIT/MiniToolSupport/MiniToolModels.swift

144 lines
5 KiB
Swift

import Foundation
struct ToolInfo: Decodable {
struct Capabilities: Decodable {
let internetAccess: Bool?
enum CodingKeys: String, CodingKey {
case internetAccess = "internet_access"
}
}
let author: String
let desc: String
let capabilities: Capabilities?
let requiredIDeviceFunctions: [String]?
enum CodingKeys: String, CodingKey {
case author
case desc
case capabilities
case requiredIDeviceFunctions = "required_idevice_functions"
}
}
class MiniToolBundle: ObservableObject, Identifiable {
static func == (lhs: MiniToolBundle, rhs: MiniToolBundle) -> Bool {
return lhs.name == rhs.name
}
@Published var url: URL
init(url: URL) {
self.url = url
}
var id: String { url.lastPathComponent }
var name: String { url.deletingPathExtension().lastPathComponent }
var indexURL: URL { url.appendingPathComponent("index.html") }
var backgroundURL: URL { url.appendingPathComponent("background.js") }
var isValid: Bool {
let fm = FileManager.default
return fm.fileExists(atPath: indexURL.path) && fm.fileExists(atPath: backgroundURL.path)
}
func getHostName() -> String {
let d = name.data(using: .utf8)
let b = d!.base64EncodedString().replacingOccurrences(of:"+", with:"").replacingOccurrences(of:"/", with:"").replacingOccurrences(of:"=", with:"")
return "\(b).stiktool"
}
}
final class MiniToolStore: ObservableObject {
@Published private(set) var tools: [MiniToolBundle] = []
@Published var isBusy: Bool = false
@Published var lastError: String?
func refresh() {
let directory = toolsDirectory()
let contents = (try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil)) ?? []
let bundles = contents
.filter { url in
let ext = url.pathExtension.lowercased()
return ext == "stiktool" || url.hasDirectoryPath
}
.map { MiniToolBundle(url: $0) }
.filter { $0.isValid }
.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
DispatchQueue.main.async {
self.tools = bundles
}
}
func delete(_ tool: MiniToolBundle) {
do {
try FileManager.default.removeItem(at: tool.url)
refresh()
} catch {
lastError = error.localizedDescription
}
}
func importTool(from externalURL: URL) {
isBusy = true
DispatchQueue.global(qos: .userInitiated).async {
defer { DispatchQueue.main.async { self.isBusy = false } }
do {
let accessing = externalURL.startAccessingSecurityScopedResource()
defer { if accessing { externalURL.stopAccessingSecurityScopedResource() } }
let destination = self.toolsDirectory().appendingPathComponent(externalURL.lastPathComponent)
let fm = FileManager.default
if fm.fileExists(atPath: destination.path) {
try fm.removeItem(at: destination)
}
try fm.copyItem(at: externalURL, to: destination)
DispatchQueue.main.async { self.refresh() }
} catch {
DispatchQueue.main.async { self.lastError = error.localizedDescription }
}
}
}
func toolsDirectory() -> URL {
let dir = URL.documentsDirectory.appendingPathComponent("tools", isDirectory: true)
var isDir: ObjCBool = false
let fm = FileManager.default
if fm.fileExists(atPath: dir.path, isDirectory: &isDir) {
if !isDir.boolValue {
try? fm.removeItem(at: dir)
}
}
if !fm.fileExists(atPath: dir.path) {
try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
}
return dir
}
static func toolsDataDirectory() -> URL {
let dir = URL.documentsDirectory.appendingPathComponent("MiniToolData", isDirectory: true)
var isDir: ObjCBool = false
let fm = FileManager.default
if fm.fileExists(atPath: dir.path, isDirectory: &isDir) {
if !isDir.boolValue {
try? fm.removeItem(at: dir)
}
}
if !fm.fileExists(atPath: dir.path) {
try? fm.createDirectory(at: dir, withIntermediateDirectories: true)
}
return dir
}
private func sanitizedToolName(_ raw: String) -> String {
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
let allowed = CharacterSet.alphanumerics.union(CharacterSet(charactersIn: "-_"))
let filtered = trimmed.unicodeScalars.map { allowed.contains($0) ? Character($0) : "_" }
let name = String(filtered).replacingOccurrences(of: "__+", with: "_", options: .regularExpression)
return name.isEmpty ? "Untitled" : name
}
}