Init commit

This commit is contained in:
世界
2023-07-15 15:03:45 +08:00
commit f441b89efb
122 changed files with 7355 additions and 0 deletions
@@ -0,0 +1,149 @@
import Foundation
import Library
import SwiftUI
public struct EditProfileContentView: View {
#if os(macOS)
public static let windowID = "edit-profile-content"
#endif
public struct Context: Codable, Hashable {
public let profileID: Int64
public let readOnly: Bool
}
private let profileID: Int64?
private let readOnly: Bool
public init(_ context: Context?) {
profileID = context?.profileID
readOnly = context?.readOnly == true
}
@Environment(\.dismiss) private var dismiss
@State private var isLoading = true
@State private var profile: Profile!
@State private var profileContent: String = ""
@State private var isChanged = false
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var fatalError = false
public var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task.detached {
loadContent()
}
}
} else {
viewBuilder {
if readOnly {
TextEditor(text: .constant(profileContent))
} else {
TextEditor(text: $profileContent)
}
}
.font(Font.system(.caption2, design: .monospaced))
.disableAutocorrection(true)
#if os(iOS)
.textInputAutocapitalization(.none)
.background(Color(UIColor.secondarySystemGroupedBackground))
#elseif os(macOS)
.padding()
#endif
.onChange(of: profileContent) { _ in
isChanged = true
}
}
}
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"), action: {
if fatalError {
dismiss()
}
})
)
}
.navigationTitle(navigationTitle)
#if os(macOS)
.toolbar {
ToolbarItemGroup(placement: .navigation) {
if !readOnly {
Button(action: {
Task.detached {
saveContent()
}
}, label: {
Image("save", label: Text("Save"))
})
.disabled(!isChanged)
}
}
}
#elseif os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
if !readOnly {
Button("Save") {
Task.detached {
saveContent()
}
}.disabled(!isChanged)
}
}
}
.navigationBarTitleDisplayMode(.inline)
#endif
}
private var navigationTitle: String {
if readOnly {
return "View Content"
} else {
return "Edit Content"
}
}
private func loadContent() {
do {
try loadContent0()
} catch {
errorMessage = error.localizedDescription
fatalError = true
errorPresented = true
}
}
private func loadContent0() throws {
guard let profileID else {
throw NSError(domain: "Context destroyed", code: 0)
}
guard let profile = try ProfileManager.get(profileID) else {
throw NSError(domain: "Profile missing", code: 0)
}
profileContent = try profile.read()
self.profile = profile
isLoading = false
}
private func saveContent() {
guard let profile else {
return
}
do {
try profile.write(profileContent)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
return
}
isChanged = false
}
}
@@ -0,0 +1,172 @@
import Library
import SwiftUI
public struct EditProfileView: View {
#if os(macOS)
@Environment(\.openWindow) private var openWindow
#endif
@EnvironmentObject private var profile: Profile
@State private var isLoading = false
@State private var isChanged = false
@State private var errorPresented = false
@State private var errorMessage = ""
public init() {}
public var body: some View {
FormView {
FormItem("Name") {
TextField("Name", text: $profile.name, prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
Picker(selection: $profile.type) {
Text("Local").tag(ProfileType.local)
Text("iCloud").tag(ProfileType.icloud)
Text("Remote").tag(ProfileType.remote)
} label: {
Text("Type")
}
.disabled(true)
if profile.type == .icloud {
FormItem("Path") {
TextField("Path", text: $profile.path, prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
} else if profile.type == .remote {
FormItem("URL") {
TextField("URL", text: $profile.remoteURL.unwrapped(""), prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
Toggle("Auto Update", isOn: $profile.autoUpdate)
}
if profile.type == .remote {
Section("Status") {
FormTextItem("Last Updated", profile.lastUpdatedString)
}
}
#if os(iOS)
Section("Action") {
if profile.type != .remote {
NavigationLink {
EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: false))
} label: {
Text("Edit Content").foregroundColor(.accentColor)
}
} else {
NavigationLink {
EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true))
} label: {
Text("View Content").foregroundColor(.accentColor)
}
Button("Update") {
isLoading = true
Task.detached {
await updateProfile()
}
}
.disabled(isLoading)
}
}
#endif
}
.onChange(of: profile.name, perform: { _ in
isChanged = true
})
.onChange(of: profile.remoteURL, perform: { _ in
isChanged = true
})
.onChange(of: profile.autoUpdate, perform: { _ in
isChanged = true
})
.disabled(isLoading)
#if os(macOS)
.toolbar {
ToolbarItemGroup(placement: .navigation) {
Button(action: {
isLoading = true
Task.detached {
await saveProfile()
}
}, label: {
Image("save", bundle: ApplicationLibrary.bundle, label: Text("Save"))
})
.disabled(isLoading || !isChanged)
if profile.type != .remote {
Button(action: {
openWindow(id: EditProfileContentView.windowID, value: EditProfileContentView.Context(profileID: profile.id!, readOnly: false))
}, label: {
Label("Edit Content", systemImage: "pencil")
})
.disabled(isLoading)
} else {
Button(action: {
isLoading = true
Task.detached {
await updateProfile()
}
}, label: {
Label("Update", systemImage: "arrow.clockwise")
})
.disabled(isLoading)
Button(action: {
openWindow(id: EditProfileContentView.windowID, value: EditProfileContentView.Context(profileID: profile.id!, readOnly: true))
}, label: {
Label("View Content", systemImage: "doc.text.fill")
})
.disabled(isLoading)
}
}
}
#elseif os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
isLoading = true
Task.detached {
await saveProfile()
}
}.disabled(!isChanged)
}
}
#endif
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
.navigationTitle("Edit Profile")
}
private func updateProfile() async {
defer {
isLoading = false
}
do {
try await Task.sleep(nanoseconds: UInt64(100 * Double(NSEC_PER_MSEC)))
try profile.updateRemoteProfile()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
}
}
private func saveProfile() async {
do {
_ = try ProfileManager.update(profile)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
return
}
isChanged = false
isLoading = false
await MainActor.run {
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
}
}
}
@@ -0,0 +1,69 @@
import Library
import SwiftUI
#if os(macOS)
public struct EditProfileWindowView: View {
public static let windowID = "edit-profile"
private var profileID: Int64?
public init(_ profileID: Int64?) {
self.profileID = profileID
}
@Environment(\.dismiss) private var dismiss
@State private var isLoading = true
@State private var profile: Profile!
@State private var errorPresented = false
@State private var errorMessage = ""
public var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task.detached {
await doReload()
}
}
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"), action: {
dismiss()
})
)
}
} else {
EditProfileView().environmentObject(profile!)
}
}
.onExitCommand {
dismiss()
}
}
private func doReload() async {
guard let profileID else {
errorMessage = "Context destroyed"
errorPresented = true
return
}
do {
profile = try ProfileManager.get(profileID)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
return
}
if profile == nil {
errorMessage = "Profile deleted"
errorPresented = true
return
}
isLoading = false
}
}
#endif
@@ -0,0 +1,223 @@
import Foundation
import Libbox
import Library
import SwiftUI
public struct NewProfileView: View {
#if os(macOS)
public static let windowID = "new-profile"
#endif
@Environment(\.dismiss) private var dismiss
@State private var isSaving = false
@State private var profileName = ""
@State private var profileType = ProfileType.local
@State private var fileImport = false
@State private var fileURL: URL!
@State private var remotePath = ""
@State private var pickerPresented = false
@State private var errorPresented = false
@State private var errorMessage = ""
private let callback: (() -> Void)?
public init(_ callback: (() -> Void)? = nil) {
self.callback = callback
}
public var body: some View {
FormView {
FormItem("Name") {
TextField("Name", text: $profileName, prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
Picker(selection: $profileType) {
Text("Local").tag(ProfileType.local)
Text("iCloud").tag(ProfileType.icloud)
Text("Remote").tag(ProfileType.remote)
} label: {
Text("Type")
}
if profileType == .local {
Picker(selection: $fileImport) {
Text("Create New").tag(false)
Text("Import").tag(true)
} label: {
Text("File")
}
viewBuilder {
if fileImport {
HStack {
Text("File Path")
Spacer()
Spacer()
if let fileURL {
Button(fileURL.fileName) {
pickerPresented = true
}
} else {
Button("Choose") {
pickerPresented = true
}
}
}
}
}
} else if profileType == .icloud {
FormItem("Path") {
TextField("Path", text: $remotePath, prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
} else if profileType == .remote {
FormItem("URL") {
TextField("URL", text: $remotePath, prompt: Text("Required"))
.multilineTextAlignment(.trailing)
}
}
Section {
if !isSaving {
Button("Create") {
isSaving = true
Task.detached {
await createProfile()
}
}
} else {
ProgressView()
}
}
}
.navigationTitle("New Profile")
.alert(isPresented: $errorPresented) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("Ok"))
)
}
.fileImporter(
isPresented: $pickerPresented,
allowedContentTypes: [.json],
allowsMultipleSelection: false
) { result in
do {
let urls = try result.get()
if !urls.isEmpty {
fileURL = urls[0]
}
} catch {
errorMessage = error.localizedDescription
errorPresented = true
return
}
}
}
private func createProfile() async {
defer {
isSaving = false
}
if profileName.isEmpty {
errorMessage = "Missing profile name"
errorPresented = true
return
}
if remotePath.isEmpty {
if profileType == .icloud {
errorMessage = "Missing path"
errorPresented = true
return
} else if profileType == .remote {
errorMessage = "Missing URL"
errorPresented = true
return
}
}
do {
try createProfile0()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
return
}
await MainActor.run {
dismiss()
if let callback {
callback()
}
#if os(macOS)
NotificationCenter.default.post(name: ProfileView.notificationName, object: nil)
resetFields()
#endif
}
}
private func resetFields() {
profileName = ""
profileType = .local
fileImport = false
fileURL = nil
remotePath = ""
}
private func createProfile0() throws {
let nextProfileID = try ProfileManager.nextID()
var savePath = ""
var remoteURL: String? = nil
if profileType == .local {
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
if fileImport {
guard let fileURL else {
errorMessage = "Missing file"
errorPresented = true
return
}
if !fileURL.startAccessingSecurityScopedResource() {
errorMessage = "Missing access to selected file"
errorPresented = true
return
}
defer {
fileURL.stopAccessingSecurityScopedResource()
}
try String(contentsOf: fileURL).write(to: profileConfig, atomically: true, encoding: .utf8)
} else {
try "{}".write(to: profileConfig, atomically: true, encoding: .utf8)
}
savePath = profileConfig.relativePath
} else if profileType == .icloud {
if !FileManager.default.fileExists(atPath: FilePath.iCloudDirectory.path) {
try FileManager.default.createDirectory(at: FilePath.iCloudDirectory, withIntermediateDirectories: true)
}
let saveURL = FilePath.iCloudDirectory.appendingPathComponent(remotePath, isDirectory: false)
_ = saveURL.startAccessingSecurityScopedResource()
defer {
saveURL.stopAccessingSecurityScopedResource()
}
do {
_ = try String(contentsOf: saveURL)
} catch {
try "{}".write(to: saveURL, atomically: true, encoding: .utf8)
}
savePath = remotePath
} else if profileType == .remote {
let remoteContent = try HTTPClient().getString(remotePath)
var error: NSError?
LibboxCheckConfig(remoteContent, &error)
if let error {
throw error
}
let profileConfigDirectory = FilePath.sharedDirectory.appendingPathComponent("configs", isDirectory: true)
try FileManager.default.createDirectory(at: profileConfigDirectory, withIntermediateDirectories: true)
let profileConfig = profileConfigDirectory.appendingPathComponent("config_\(nextProfileID).json")
try remoteContent.write(to: profileConfig, atomically: true, encoding: .utf8)
savePath = profileConfig.relativePath
remoteURL = remotePath
}
try ProfileManager.create(Profile(name: profileName, type: profileType, path: savePath, remoteURL: remoteURL))
}
}
@@ -0,0 +1,232 @@
import Foundation
import Library
import SwiftUI
public struct ProfileView: View {
public static let notificationName = Notification.Name("\(FilePath.packageName).update-profile")
@State private var isLoading = true
@State private var isUpdating = false
@State private var errorPresented = false
@State private var errorMessage = ""
@State private var profileList: [Profile] = []
#if os(iOS)
@State private var editMode = EditMode.inactive
#elseif os(macOS)
@Environment(\.openWindow) private var openWindow
#endif
@State private var observer: Any?
public init() {}
public var body: some View {
viewBuilder {
if isLoading {
ProgressView().onAppear {
Task.detached {
doReload()
}
}
} else {
#if os(iOS)
FormView {
NavigationLink {
NewProfileView {
Task.detached {
doReload()
}
}
} label: {
Text("New Profile").foregroundColor(.accentColor)
}
.disabled(editMode.isEditing)
if profileList.isEmpty {
Text("Empty Profiles")
} else {
List {
ForEach(profileList, id: \.mustID) { profile in
viewBuilder {
if editMode.isEditing == true {
Text(profile.name)
} else {
NavigationLink {
EditProfileView().environmentObject(profile)
} label: {
Text(profile.name)
}
}
}
}
.onMove(perform: moveProfile)
.onDelete(perform: deleteProfile)
}
}
}
#elseif os(macOS)
if profileList.isEmpty {
Text("Empty Profiles")
} else {
FormView {
List {
ForEach(profileList, id: \.mustID) { profile in
HStack {
VStack(alignment: .leading) {
Text(profile.name)
if profile.type == .remote {
Spacer(minLength: 4)
Text("Last Updated: \(profile.lastUpdatedString)").font(.caption)
}
}
HStack {
if profile.type == .remote {
Button(action: {
isUpdating = true
Task.detached {
updateProfile(profile)
}
}, label: {
Image(systemName: "arrow.clockwise")
})
}
Button(action: {
openWindow(id: EditProfileWindowView.windowID, value: profile.mustID)
}, label: {
Image(systemName: "pencil")
})
Button(action: {
deleteProfile(profile)
}, label: {
Image(systemName: "trash.fill")
})
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(.vertical, 8)
.frame(maxWidth: .infinity, alignment: .leading)
}
.onMove(perform: moveProfile)
.onDelete(perform: deleteProfile)
}
}
}
#endif
}
}
.disabled(isUpdating)
.navigationTitle("Profiles")
#if os(macOS)
.onAppear {
if observer == nil {
observer = NotificationCenter.default.addObserver(forName: ProfileView.notificationName, object: nil, queue: .main) { _ in
Task.detached {
doReload()
}
}
}
}
.onDisappear {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
observer = nil
}
.toolbar {
ToolbarItem {
Button(action: {
openWindow(id: NewProfileView.windowID)
}, label: {
Label("New Profile", systemImage: "plus.square.fill")
})
}
}
#elseif os(iOS)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
EditButton().disabled(profileList.isEmpty)
}
}
.environment(\.editMode, $editMode)
#endif
}
private func deleteSelectedProfiles(_ profileID: [Int64]) {
do {
if try ProfileManager.delete(by: profileID) > 0 {
isLoading = true
}
} catch {
errorMessage = error.localizedDescription
errorPresented = true
}
}
private func doReload() {
defer {
isLoading = false
}
do {
profileList = try ProfileManager.list()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
return
}
}
private func updateProfile(_ profile: Profile) {
do {
_ = try profile.updateRemoteProfile()
} catch {
errorMessage = error.localizedDescription
errorPresented = true
}
isUpdating = false
}
private func deleteProfile(_ profile: Profile) {
Task.detached {
do {
_ = try ProfileManager.delete(profile)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
return
}
isLoading = true
}
}
private func moveProfile(from source: IndexSet, to destination: Int) {
profileList.move(fromOffsets: source, toOffset: destination)
for (index, profile) in profileList.enumerated() {
profile.order = UInt32(index)
}
do {
try ProfileManager.update(profileList)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
return
}
}
private func deleteProfile(where profileIndex: IndexSet) {
let profileToDelete = profileIndex.map { index in
profileList[index]
}
profileList.remove(atOffsets: profileIndex)
Task.detached {
do {
_ = try ProfileManager.delete(profileToDelete)
} catch {
errorMessage = error.localizedDescription
errorPresented = true
}
}
}
}