diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8117bd3 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "Frameworks/Runestone"] + path = Frameworks/Runestone + url = https://github.com/nekohasekai/Runestone.git diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..dd5184e --- /dev/null +++ b/.swiftformat @@ -0,0 +1 @@ +--exclude Frameworks \ No newline at end of file diff --git a/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift b/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift index 9531ce8..983c0d6 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/CardManagementSheet.swift @@ -11,20 +11,65 @@ import SwiftUI } public var body: some View { - NavigationStackCompat { - Group { - if configuration.isLoading { - ProgressView() - } else { - listContent + #if os(macOS) + macOSBody + #else + iOSBody + #endif + } + + #if os(macOS) + private var macOSBody: some View { + VStack(alignment: .leading, spacing: 0) { + Text("Dashboard Items") + .font(.headline) + .padding(.horizontal, 20) + .padding(.top, 20) + .padding(.bottom, 12) + + Group { + if configuration.isLoading { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + listContent + } } } - .navigationTitle("Dashboard Items") - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - .toolbar { - #if os(iOS) || os(tvOS) + .toolbar { + ToolbarItem(placement: .destructiveAction) { + Button("Reset", role: .destructive) { + Task { + await configuration.resetToDefault() + configurationVersion += 1 + } + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + dismiss() + } + } + } + .onExitCommand { + dismiss() + } + } + #else + private var iOSBody: some View { + NavigationStackCompat { + Group { + if configuration.isLoading { + ProgressView() + } else { + listContent + } + } + .navigationTitle("Dashboard Items") + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif + .toolbar { ToolbarItem(placement: .topBarTrailing) { Button("Reset", role: .destructive) { Task { @@ -33,24 +78,10 @@ import SwiftUI } } } - #else - ToolbarItem(placement: .cancellationAction) { - Button("Reset", role: .destructive) { - Task { - await configuration.resetToDefault() - configurationVersion += 1 - } - } - } - ToolbarItem(placement: .confirmationAction) { - Button("Done") { - dismiss() - } - } - #endif - } + } + } } - } + #endif private var listContent: some View { List { diff --git a/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift b/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift index 4267b5e..73debf7 100644 --- a/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift +++ b/ApplicationLibrary/Views/Dashboard/Cards/ProfileCard.swift @@ -216,7 +216,7 @@ public struct ProfileCard: View { private var manageProfilesSheet: some View { NavigationSheet( - title: "Profiles", + title: String(localized: "Manage profiles"), showDoneButton: true, onDismiss: { viewModel.showManageProfiles = false } ) { @@ -227,11 +227,20 @@ public struct ProfileCard: View { @ViewBuilder private func editProfileSheet(for profile: Profile) -> some View { - NavigationSheet(title: "Edit Profile") { - EditProfileView() - .environmentObject(profile) - .environmentObject(environments) - } + #if os(macOS) + NavigationSheet { + EditProfileView() + .environmentObject(profile) + .environmentObject(environments) + } + .frame(minWidth: 500, minHeight: 400) + #else + NavigationSheet(title: "Edit Profile") { + EditProfileView() + .environmentObject(profile) + .environmentObject(environments) + } + #endif } } @@ -272,7 +281,16 @@ extension ProfileCard { @State private var createdProfile: Profile? var body: some View { - NavigationStackCompat { + #if os(macOS) + macOSBody + #else + iOSBody + #endif + } + + #if os(macOS) + @ViewBuilder + private var macOSBody: some View { if let profile = createdProfile { EditProfileView() .environmentObject(profile) @@ -282,15 +300,28 @@ extension ProfileCard { createdProfile = profile } .environmentObject(environments) - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif } } - #if os(iOS) || os(tvOS) - .presentationDetentsIfAvailable() - #endif - } + #else + private var iOSBody: some View { + NavigationStackCompat { + if let profile = createdProfile { + EditProfileView() + .environmentObject(profile) + .environmentObject(environments) + } else { + NewProfileView { profile in + createdProfile = profile + } + .environmentObject(environments) + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif + } + } + .presentationDetentsIfAvailable() + } + #endif } } @@ -305,12 +336,14 @@ extension ProfileCard { var body: some View { VStack { if viewModel.isLoading { - ProgressView().onAppear { - viewModel.setEnvironments(environments) - Task { - await viewModel.doReload() + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onAppear { + viewModel.setEnvironments(environments) + Task { + await viewModel.doReload() + } } - } } else { FormView { if viewModel.profileList.isEmpty { diff --git a/ApplicationLibrary/Views/Profile/EditProfileContentView.swift b/ApplicationLibrary/Views/Profile/EditProfileContentView.swift index 2f557c4..abada04 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileContentView.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileContentView.swift @@ -19,6 +19,7 @@ } @Environment(\.dismiss) private var dismiss + @Environment(\.profileEditor) private var profileEditor public var body: some View { viewBuilder { @@ -29,30 +30,10 @@ } } } else { - #if os(iOS) - RunestoneTextView( - text: readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent, - isEditable: !readOnly - ) + editorView .onChangeCompat(of: viewModel.profileContent) { viewModel.markAsChanged() } - #elseif os(macOS) - viewBuilder { - if readOnly { - TextEditor(text: .constant(viewModel.profileContent)) - } else { - TextEditor(text: $viewModel.profileContent) - } - } - .font(Font.system(.caption2, design: .monospaced)) - .autocorrectionDisabled(true) - .textContentType(.init(rawValue: "")) - .padding() - .onChangeCompat(of: viewModel.profileContent) { - viewModel.markAsChanged() - } - #endif } } .alertBinding($viewModel.alert) @@ -105,6 +86,38 @@ return String(localized: "Edit Content") } } + + @ViewBuilder + private var editorView: some View { + if let profileEditor { + profileEditor( + readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent, + !readOnly + ) + #if os(macOS) + .frame(maxWidth: .infinity, maxHeight: .infinity) + #endif + } else { + defaultEditorView + } + } + + @ViewBuilder + private var defaultEditorView: some View { + viewBuilder { + if readOnly { + TextEditor(text: .constant(viewModel.profileContent)) + } else { + TextEditor(text: $viewModel.profileContent) + } + } + .font(Font.system(.caption2, design: .monospaced)) + .autocorrectionDisabled(true) + #if os(macOS) + .textContentType(.init(rawValue: "")) + .padding() + #endif + } } #endif diff --git a/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift b/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift index b8e9fd5..ffbeaa0 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileContentViewModel.swift @@ -21,6 +21,14 @@ isChanged = true } + public func reset() { + isLoading = true + profile = nil + profileContent = "" + isChanged = false + alert = nil + } + public func loadContent() async { do { try await loadContentBackground() diff --git a/ApplicationLibrary/Views/Profile/EditProfileView.swift b/ApplicationLibrary/Views/Profile/EditProfileView.swift index 791ea6e..656280c 100644 --- a/ApplicationLibrary/Views/Profile/EditProfileView.swift +++ b/ApplicationLibrary/Views/Profile/EditProfileView.swift @@ -10,7 +10,16 @@ public struct EditProfileView: View { @StateObject private var viewModel = EditProfileViewModel() public init() {} + public var body: some View { + #if os(macOS) + macOSBody + #else + iOSBody + #endif + } + + private var formContent: some View { FormView { FormItem(String(localized: "Name")) { TextField("Name", text: $profile.name, prompt: Text("Required")) @@ -60,48 +69,61 @@ public struct EditProfileView: View { ProfileActionToolbar(profile: profile, viewModel: viewModel) #endif } - #if os(macOS) - .safeAreaInset(edge: .bottom) { - ProfileActionToolbar(profile: profile, viewModel: viewModel) - } - #endif - .onChangeCompat(of: profile.name) { - viewModel.markAsChanged() - } - .onChangeCompat(of: profile.remoteURL) { - viewModel.markAsChanged() - } - .onChangeCompat(of: profile.autoUpdate) { - viewModel.markAsChanged() - } - .disabled(viewModel.isLoading) - #if os(macOS) - .toolbar { - ToolbarItemGroup(placement: .navigation) { - Button { - viewModel.isLoading = true - Task { - await viewModel.saveProfile(profile, environments: environments) - } - } label: { - Image("save", bundle: ApplicationLibrary.bundle, label: Text("Save")) - } - .disabled(viewModel.isLoading || !viewModel.isChanged) - } - } - #elseif os(iOS) - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Save") { - viewModel.isLoading = true - Task { - await viewModel.saveProfile(profile, environments: environments) - } - }.disabled(!viewModel.isChanged) - } - } - #endif - .alertBinding($viewModel.alert) - .navigationTitle("Edit Profile") } + + #if os(macOS) + private var macOSBody: some View { + VStack(alignment: .leading, spacing: 0) { + Text("Edit Profile") + .font(.headline) + .padding(.horizontal, 20) + .padding(.top, 20) + .padding(.bottom, 12) + + formContent + } + .safeAreaInset(edge: .bottom) { + ProfileActionToolbar(profile: profile, viewModel: viewModel) + } + .onChangeCompat(of: profile.name) { + viewModel.markAsChanged() + } + .onChangeCompat(of: profile.remoteURL) { + viewModel.markAsChanged() + } + .onChangeCompat(of: profile.autoUpdate) { + viewModel.markAsChanged() + } + .disabled(viewModel.isLoading) + .alertBinding($viewModel.alert) + } + #else + private var iOSBody: some View { + formContent + .onChangeCompat(of: profile.name) { + viewModel.markAsChanged() + } + .onChangeCompat(of: profile.remoteURL) { + viewModel.markAsChanged() + } + .onChangeCompat(of: profile.autoUpdate) { + viewModel.markAsChanged() + } + .disabled(viewModel.isLoading) + #if os(iOS) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Save") { + viewModel.isLoading = true + Task { + await viewModel.saveProfile(profile, environments: environments) + } + }.disabled(!viewModel.isChanged) + } + } + #endif + .alertBinding($viewModel.alert) + .navigationTitle("Edit Profile") + } + #endif } diff --git a/ApplicationLibrary/Views/Profile/NewProfileView.swift b/ApplicationLibrary/Views/Profile/NewProfileView.swift index 88fe783..8c8dbc3 100644 --- a/ApplicationLibrary/Views/Profile/NewProfileView.swift +++ b/ApplicationLibrary/Views/Profile/NewProfileView.swift @@ -25,6 +25,14 @@ public struct NewProfileView: View { } public var body: some View { + #if os(macOS) + macOSBody + #else + iOSBody + #endif + } + + private var formContent: some View { FormView { FormItem(String(localized: "Name")) { TextField("Name", text: $viewModel.profileName, prompt: Text("Required")) @@ -113,8 +121,19 @@ public struct NewProfileView: View { } #endif } - .navigationTitle("New Profile") - #if os(macOS) + } + + #if os(macOS) + private var macOSBody: some View { + VStack(alignment: .leading, spacing: 0) { + Text("New Profile") + .font(.headline) + .padding(.horizontal, 20) + .padding(.top, 20) + .padding(.bottom, 12) + + formContent + } .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { @@ -138,10 +157,8 @@ public struct NewProfileView: View { } } } - #endif .disabled(viewModel.isSaving) .alertBinding($viewModel.alert) - #if os(iOS) || os(macOS) .fileImporter( isPresented: $viewModel.pickerPresented, allowedContentTypes: [.json], @@ -157,6 +174,30 @@ public struct NewProfileView: View { return } } - #endif - } + } + #else + private var iOSBody: some View { + formContent + .navigationTitle("New Profile") + .disabled(viewModel.isSaving) + .alertBinding($viewModel.alert) + #if os(iOS) + .fileImporter( + isPresented: $viewModel.pickerPresented, + allowedContentTypes: [.json], + allowsMultipleSelection: false + ) { result in + do { + let urls = try result.get() + if !urls.isEmpty { + viewModel.fileURL = urls[0] + } + } catch { + viewModel.alert = Alert(error) + return + } + } + #endif + } + #endif } diff --git a/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift b/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift index 8c45e4a..333e296 100644 --- a/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift +++ b/ApplicationLibrary/Views/Profile/ProfileActionToolbar.swift @@ -6,6 +6,9 @@ import SwiftUI public struct ProfileActionToolbar: View { @EnvironmentObject private var environments: ExtensionEnvironments @Environment(\.dismiss) private var dismiss + #if os(macOS) + @Environment(\.openWindow) private var openWindow + #endif @ObservedObject private var profile: Profile @ObservedObject private var viewModel: EditProfileViewModel @@ -69,16 +72,12 @@ public struct ProfileActionToolbar: View { HStack(spacing: 12) { if profile.type != .remote { - NavigationLink { - EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: false)) - } label: { - Text("Edit Content") + Button("Edit Content") { + openWindow(value: EditProfileContentView.Context(profileID: profile.id!, readOnly: false)) } } else { - NavigationLink { - EditProfileContentView(EditProfileContentView.Context(profileID: profile.id!, readOnly: true)) - } label: { - Text("View Content") + Button("View Content") { + openWindow(value: EditProfileContentView.Context(profileID: profile.id!, readOnly: true)) } Button { @@ -92,14 +91,27 @@ public struct ProfileActionToolbar: View { .disabled(viewModel.isLoading) } - Spacer() - Button("Delete", role: .destructive) { Task { await viewModel.deleteProfile(profile, environments: environments, dismiss: dismiss) } } .foregroundColor(.red) + + Spacer() + + Button("Cancel") { + dismiss() + } + + Button("Save") { + viewModel.isLoading = true + Task { + await viewModel.saveProfile(profile, environments: environments) + } + } + .buttonStyle(.borderedProminent) + .disabled(viewModel.isLoading || !viewModel.isChanged) } .padding() .background(Color(NSColor.controlBackgroundColor)) diff --git a/ApplicationLibrary/Views/Profile/ProfileEditorEnvironment.swift b/ApplicationLibrary/Views/Profile/ProfileEditorEnvironment.swift new file mode 100644 index 0000000..fbd3e23 --- /dev/null +++ b/ApplicationLibrary/Views/Profile/ProfileEditorEnvironment.swift @@ -0,0 +1,14 @@ +#if os(iOS) || os(macOS) + import SwiftUI + + public struct ProfileEditorKey: EnvironmentKey { + public static let defaultValue: ((Binding, Bool) -> AnyView)? = nil + } + + public extension EnvironmentValues { + var profileEditor: ((Binding, Bool) -> AnyView)? { + get { self[ProfileEditorKey.self] } + set { self[ProfileEditorKey.self] = newValue } + } + } +#endif diff --git a/ApplicationLibrary/Views/Profile/ProfileEditorTheme.swift b/ApplicationLibrary/Views/Profile/ProfileEditorTheme.swift deleted file mode 100644 index a8ee9d9..0000000 --- a/ApplicationLibrary/Views/Profile/ProfileEditorTheme.swift +++ /dev/null @@ -1,73 +0,0 @@ -#if os(iOS) - import Runestone - import UIKit - - final class ProfileEditorTheme: Theme { - let font: UIFont = .monospacedSystemFont(ofSize: 14, weight: .regular) - let textColor: UIColor = .label - - let gutterBackgroundColor: UIColor = .secondarySystemBackground - let gutterHairlineColor: UIColor = .separator - - let lineNumberColor: UIColor = .secondaryLabel - let lineNumberFont: UIFont = .monospacedSystemFont(ofSize: 14, weight: .regular) - - let selectedLineBackgroundColor: UIColor = .systemFill - let selectedLinesLineNumberColor: UIColor = .label - let selectedLinesGutterBackgroundColor: UIColor = .secondarySystemBackground - - let invisibleCharactersColor: UIColor = .tertiaryLabel - - let pageGuideHairlineColor: UIColor = .separator - let pageGuideBackgroundColor: UIColor = .secondarySystemBackground - - let markedTextBackgroundColor: UIColor = .systemFill - let markedTextBackgroundCornerRadius: CGFloat = 4 - - func textColor(for rawHighlightName: String) -> UIColor? { - guard let highlightName = HighlightName(rawHighlightName) else { - return nil - } - switch highlightName { - case .comment: - return .secondaryLabel - case .property: - return .systemCyan - case .string: - return .systemGreen - case .number: - return .systemOrange - case .constantBuiltin: - return .systemPurple - case .error: - return .systemRed - } - } - - func fontTraits(for _: String) -> FontTraits { - [] - } - } - - private enum HighlightName: String { - case comment - case property - case string - case number - case constantBuiltin = "constant.builtin" - case error - - init?(_ rawHighlightName: String) { - var components = rawHighlightName.split(separator: ".") - while !components.isEmpty { - let candidateRawHighlightName = components.joined(separator: ".") - if let highlightName = Self(rawValue: candidateRawHighlightName) { - self = highlightName - return - } - components.removeLast() - } - return nil - } - } -#endif diff --git a/ApplicationLibrary/Views/Profile/ProfileSheetHelpers.swift b/ApplicationLibrary/Views/Profile/ProfileSheetHelpers.swift index a9fe4a7..8ffb7b9 100644 --- a/ApplicationLibrary/Views/Profile/ProfileSheetHelpers.swift +++ b/ApplicationLibrary/Views/Profile/ProfileSheetHelpers.swift @@ -18,14 +18,14 @@ public enum SheetSize { @MainActor public struct NavigationSheet: View { - private let title: String + private let title: String? private let size: SheetSize private let showDoneButton: Bool private let onDismiss: (() -> Void)? private let content: () -> Content public init( - title: String, + title: String? = nil, size: SheetSize = .large, showDoneButton: Bool = false, onDismiss: (() -> Void)? = nil, @@ -39,13 +39,25 @@ public struct NavigationSheet: View { } public var body: some View { - NavigationStackCompat { - content() - .navigationTitle(title) - #if os(iOS) - .navigationBarTitleDisplayMode(.inline) - #endif - #if os(macOS) + #if os(macOS) + macOSBody + #else + iOSBody + #endif + } + + #if os(macOS) + private var macOSBody: some View { + VStack(alignment: .leading, spacing: 0) { + if let title { + Text(title) + .font(.headline) + .padding(.horizontal, 20) + .padding(.top, 20) + .padding(.bottom, 12) + } + content() + } .toolbar { if showDoneButton { ToolbarItem(placement: .confirmationAction) { @@ -55,12 +67,19 @@ public struct NavigationSheet: View { } } } - #endif } - #if os(iOS) || os(tvOS) - .sheetDetent(size) - #endif - } + #else + private var iOSBody: some View { + NavigationStackCompat { + content() + .navigationTitle(title ?? "") + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + #endif + } + .sheetDetent(size) + } + #endif } #if os(iOS) || os(tvOS) diff --git a/ApplicationLibrary/Views/Profile/RunestoneTextView.swift b/ApplicationLibrary/Views/Profile/RunestoneTextView.swift deleted file mode 100644 index 1e87cfd..0000000 --- a/ApplicationLibrary/Views/Profile/RunestoneTextView.swift +++ /dev/null @@ -1,85 +0,0 @@ -#if os(iOS) - import Runestone - import SwiftUI - import TreeSitterJSON5Runestone - - struct RunestoneTextView: UIViewRepresentable { - @Binding var text: String - let isEditable: Bool - - func makeUIView(context: Context) -> TextView { - let textView = TextView() - - textView.showLineNumbers = true - textView.isLineWrappingEnabled = false - textView.showTabs = false - textView.showSpaces = false - textView.showLineBreaks = false - textView.showSoftLineBreaks = false - textView.showNonBreakingSpaces = false - - textView.autocorrectionType = .no - textView.autocapitalizationType = .none - textView.smartDashesType = .no - textView.smartQuotesType = .no - textView.smartInsertDeleteType = .no - - textView.backgroundColor = .secondarySystemGroupedBackground - textView.contentInsetAdjustmentBehavior = .always - textView.alwaysBounceVertical = true - - textView.kern = 0.3 - textView.lineHeightMultiplier = 1.3 - - textView.characterPairs = [ - BasicCharacterPair(leading: "{", trailing: "}"), - BasicCharacterPair(leading: "[", trailing: "]"), - BasicCharacterPair(leading: "\"", trailing: "\""), - ] - - let theme = ProfileEditorTheme() - let state = TextViewState(text: text, theme: theme, language: .json5) - textView.setState(state) - - textView.isEditable = isEditable - textView.editorDelegate = context.coordinator - - return textView - } - - func updateUIView(_ textView: TextView, context _: Context) { - if textView.text != text { - textView.text = text - } - if textView.isEditable != isEditable { - textView.isEditable = isEditable - } - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - final class Coordinator: TextViewDelegate { - var parent: RunestoneTextView - - init(_ parent: RunestoneTextView) { - self.parent = parent - } - - func textViewDidChange(_ textView: TextView) { - parent.text = textView.text - } - } - } - - final class BasicCharacterPair: CharacterPair { - let leading: String - let trailing: String - - init(leading: String, trailing: String) { - self.leading = leading - self.trailing = trailing - } - } -#endif diff --git a/Frameworks/Runestone b/Frameworks/Runestone new file mode 160000 index 0000000..b3ef61c --- /dev/null +++ b/Frameworks/Runestone @@ -0,0 +1 @@ +Subproject commit b3ef61c54b152a560c746a0552f0b449dda2b9de diff --git a/Frameworks/TreeSitterJSON5/Package.swift b/Frameworks/TreeSitterJSON5/Package.swift index c94fd82..cec9b7d 100644 --- a/Frameworks/TreeSitterJSON5/Package.swift +++ b/Frameworks/TreeSitterJSON5/Package.swift @@ -13,7 +13,7 @@ let package = Package( .library(name: "TreeSitterJSON5Runestone", targets: ["TreeSitterJSON5Runestone"]), ], dependencies: [ - .package(url: "https://github.com/simonbs/Runestone", from: "0.3.0"), + .package(path: "../Runestone"), ], targets: [ .target(name: "TreeSitterJSON5", cSettings: [.headerSearchPath("src")]), diff --git a/Localizable.xcstrings b/Localizable.xcstrings index 25718e4..592d03f 100644 --- a/Localizable.xcstrings +++ b/Localizable.xcstrings @@ -195,6 +195,9 @@ } } } + }, + "Cancel" : { + }, "Chain" : { "localizations" : { @@ -507,6 +510,9 @@ } } } + }, + "Do you want to save the changes you made?" : { + }, "Documentation" : { "localizations" : { @@ -527,6 +533,9 @@ } } } + }, + "Don't Save" : { + }, "Done" : { "localizations" : { @@ -1017,6 +1026,9 @@ }, "Machine: " : { "shouldTranslate" : false + }, + "Manage profiles" : { + }, "Match Rule" : { "shouldTranslate" : false @@ -1295,6 +1307,7 @@ } }, "Profiles" : { + "extractionState" : "stale", "localizations" : { "zh-Hans" : { "stringUnit" : { @@ -1746,6 +1759,9 @@ } } } + }, + "Unsaved Changes" : { + }, "Update" : { "localizations" : { diff --git a/MacLibrary/CodeEditTextView.swift b/MacLibrary/CodeEditTextView.swift new file mode 100644 index 0000000..80d6851 --- /dev/null +++ b/MacLibrary/CodeEditTextView.swift @@ -0,0 +1,127 @@ +import AppKit +import CodeEditLanguages +import CodeEditSourceEditor +import CodeEditTextView +import SwiftUI + +private extension NSColor { + var forEditor: NSColor { + usingColorSpace(.sRGB) ?? self + } +} + +private func makeTheme() -> EditorTheme { + EditorTheme( + text: .init(color: NSColor.labelColor.forEditor), + insertionPoint: NSColor.labelColor.forEditor, + invisibles: .init(color: NSColor.tertiaryLabelColor.forEditor), + background: NSColor.textBackgroundColor.forEditor, + lineHighlight: NSColor.quaternaryLabelColor.forEditor, + selection: NSColor.selectedTextBackgroundColor.forEditor, + keywords: .init(color: NSColor.systemPurple.forEditor), + commands: .init(color: NSColor.systemCyan.forEditor), + types: .init(color: NSColor.systemCyan.forEditor), + attributes: .init(color: NSColor.systemCyan.forEditor), + variables: .init(color: NSColor.labelColor.forEditor), + values: .init(color: NSColor.systemOrange.forEditor), + numbers: .init(color: NSColor.systemOrange.forEditor), + strings: .init(color: NSColor.systemGreen.forEditor), + characters: .init(color: NSColor.systemGreen.forEditor), + comments: .init(color: NSColor.secondaryLabelColor.forEditor) + ) +} + +private func makeConfiguration(isEditable: Bool) -> SourceEditorConfiguration { + SourceEditorConfiguration( + appearance: .init( + theme: makeTheme(), + font: .monospacedSystemFont(ofSize: 14, weight: .regular), + lineHeightMultiple: 1.3, + wrapLines: false + ), + behavior: .init( + isEditable: isEditable, + isSelectable: true + ), + peripherals: .init( + showMinimap: false, + showFoldingRibbon: false + ) + ) +} + +struct CodeEditTextView: NSViewRepresentable { + @Binding var text: String + let isEditable: Bool + + func makeNSView(context: Context) -> NSView { + let controller = TextViewController( + string: text, + language: .json, + configuration: makeConfiguration(isEditable: isEditable), + cursorPositions: [] + ) + controller.loadView() + + let containerView = NSView() + containerView.translatesAutoresizingMaskIntoConstraints = false + + let controllerView = controller.view + controllerView.translatesAutoresizingMaskIntoConstraints = false + containerView.addSubview(controllerView) + + NSLayoutConstraint.activate([ + controllerView.topAnchor.constraint(equalTo: containerView.topAnchor), + controllerView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + controllerView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + controllerView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + ]) + + context.coordinator.controller = controller + context.coordinator.setupObservation() + return containerView + } + + func updateNSView(_: NSView, context: Context) { + guard let controller = context.coordinator.controller else { return } + if controller.text != text { + controller.text = text + } + if controller.configuration.behavior.isEditable != isEditable { + controller.configuration = makeConfiguration(isEditable: isEditable) + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(text: $text) + } + + class Coordinator: NSObject { + var controller: TextViewController? + @Binding var text: String + private var observation: NSObjectProtocol? + + init(text: Binding) { + _text = text + super.init() + } + + func setupObservation() { + guard let controller else { return } + observation = NotificationCenter.default.addObserver( + forName: TextView.textDidChangeNotification, + object: controller.textView, + queue: .main + ) { [weak self] _ in + guard let self, let controller = self.controller else { return } + self.text = controller.text + } + } + + deinit { + if let observation { + NotificationCenter.default.removeObserver(observation) + } + } + } +} diff --git a/MacLibrary/EditProfileContentWindow.swift b/MacLibrary/EditProfileContentWindow.swift new file mode 100644 index 0000000..1f2a696 --- /dev/null +++ b/MacLibrary/EditProfileContentWindow.swift @@ -0,0 +1,204 @@ +import ApplicationLibrary +import Library +import SwiftUI + +struct EditProfileContentWindow: View { + let context: EditProfileContentView.Context? + + @StateObject private var viewModel: EditProfileContentViewModel + @State private var showDiscardAlert = false + @State private var windowState = WindowState() + + private let readOnly: Bool + + init(context: EditProfileContentView.Context?) { + self.context = context + readOnly = context?.readOnly == true + _viewModel = StateObject(wrappedValue: EditProfileContentViewModel(profileID: context?.profileID)) + } + + @Environment(\.profileEditor) private var profileEditor + + var body: some View { + Group { + if viewModel.isLoading { + ProgressView() + .frame(maxWidth: .infinity, maxHeight: .infinity) + .task { + await viewModel.loadContent() + } + } else { + editorView + .onChangeCompat(of: viewModel.profileContent) { + viewModel.markAsChanged() + } + } + } + .frame(minWidth: 600, minHeight: 400) + .background(WindowAccessor { window in + guard let window else { return } + if windowState.window == nil { + windowState.window = window + windowState.onClose = { [weak viewModel] in + viewModel?.reset() + } + let delegate = WindowCloseDelegate( + windowState: windowState, + hasUnsavedChanges: { [weak viewModel] in + viewModel?.isChanged == true + }, + showAlert: { + showDiscardAlert = true + } + ) + windowState.delegate = delegate + window.delegate = delegate + } + }) + .onExitCommand { + handleClose() + } + .alert("Unsaved Changes", isPresented: $showDiscardAlert) { + Button("Don't Save", role: .destructive) { + windowState.forceClose() + } + Button("Cancel", role: .cancel) {} + if !readOnly { + Button("Save") { + Task { + await viewModel.saveContent() + if viewModel.alert == nil { + windowState.forceClose() + } + } + } + } + } message: { + Text("Do you want to save the changes you made?") + } + .alertBinding($viewModel.alert) + .navigationTitle(navigationTitle) + .toolbar { + ToolbarItemGroup(placement: .navigation) { + if !readOnly { + Button { + Task { + await viewModel.saveContent() + } + } label: { + Label("Save", image: "save") + } + .keyboardShortcut("s", modifiers: .command) + .disabled(!viewModel.isChanged) + } else { + Button { + NSPasteboard.general.setString(viewModel.profileContent, forType: .string) + } label: { + Label("Copy", systemImage: "doc.on.clipboard") + } + } + } + } + } + + private var navigationTitle: String { + if readOnly { + return String(localized: "View Content") + } else { + return String(localized: "Edit Content") + } + } + + @ViewBuilder + private var editorView: some View { + if let profileEditor { + profileEditor( + readOnly ? .constant(viewModel.profileContent) : $viewModel.profileContent, + !readOnly + ) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } else { + defaultEditorView + } + } + + @ViewBuilder + private var defaultEditorView: some View { + Group { + if readOnly { + TextEditor(text: .constant(viewModel.profileContent)) + } else { + TextEditor(text: $viewModel.profileContent) + } + } + .font(Font.system(.caption2, design: .monospaced)) + .autocorrectionDisabled(true) + .textContentType(.init(rawValue: "")) + .padding() + } + + private func handleClose() { + if viewModel.isChanged, !readOnly { + showDiscardAlert = true + } else { + windowState.forceClose() + } + } +} + +private class WindowState { + weak var window: NSWindow? + var delegate: WindowCloseDelegate? + var onClose: (() -> Void)? + + func forceClose() { + delegate?.allowClose = true + window?.close() + } +} + +private struct WindowAccessor: NSViewRepresentable { + let callback: (NSWindow?) -> Void + + func makeNSView(context _: Context) -> NSView { + let view = NSView() + DispatchQueue.main.async { + callback(view.window) + } + return view + } + + func updateNSView(_: NSView, context _: Context) {} +} + +private class WindowCloseDelegate: NSObject, NSWindowDelegate { + var allowClose = false + private let windowState: WindowState + private let hasUnsavedChanges: () -> Bool + private let showAlert: () -> Void + + init(windowState: WindowState, hasUnsavedChanges: @escaping () -> Bool, showAlert: @escaping () -> Void) { + self.windowState = windowState + self.hasUnsavedChanges = hasUnsavedChanges + self.showAlert = showAlert + super.init() + } + + func windowShouldClose(_: NSWindow) -> Bool { + if allowClose { + return true + } + if hasUnsavedChanges() { + DispatchQueue.main.async { + self.showAlert() + } + return false + } + return true + } + + func windowWillClose(_: Notification) { + windowState.onClose?() + allowClose = false + } +} diff --git a/MacLibrary/MacApplication.swift b/MacLibrary/MacApplication.swift index dff9fae..43421cf 100644 --- a/MacLibrary/MacApplication.swift +++ b/MacLibrary/MacApplication.swift @@ -7,6 +7,10 @@ public struct MacApplication: Scene { @State private var isMenuPresented = false @StateObject private var environments = ExtensionEnvironments() + private let profileEditor: (Binding, Bool) -> AnyView = { text, isEditable in + AnyView(CodeEditTextView(text: text, isEditable: isEditable)) + } + public init() {} public var body: some Scene { Window("sing-box", id: "main", content: { @@ -52,6 +56,13 @@ public struct MacApplication: Scene { } .menuBarExtraStyle(.window) .menuBarExtraAccess(isPresented: $isMenuPresented) + + WindowGroup(for: EditProfileContentView.Context.self) { $context in + EditProfileContentWindow(context: context) + .environment(\.profileEditor, profileEditor) + } + .windowResizability(.contentMinSize) + .defaultSize(width: 700, height: 500) } private func initialize() async { diff --git a/MacLibrary/MainView.swift b/MacLibrary/MainView.swift index 67c827f..0bb206f 100644 --- a/MacLibrary/MainView.swift +++ b/MacLibrary/MainView.swift @@ -10,6 +10,10 @@ public struct MainView: View { @State private var showCardManagement = false @State private var cardConfigurationVersion = 0 + private let profileEditor: (Binding, Bool) -> AnyView = { text, isEditable in + AnyView(CodeEditTextView(text: text, isEditable: isEditable)) + } + public init() {} public var body: some View { @@ -59,6 +63,7 @@ public struct MainView: View { .environment(\.selection, $viewModel.selection) .environment(\.importProfile, $viewModel.importProfile) .environment(\.importRemoteProfile, $viewModel.importRemoteProfile) + .environment(\.profileEditor, profileEditor) .handlesExternalEvents(preferring: [], allowing: ["*"]) .onOpenURL(perform: viewModel.openURL) .sheet(isPresented: $showCardManagement) { diff --git a/SFI/MainView.swift b/SFI/MainView.swift index e69e5e0..f8b20b4 100644 --- a/SFI/MainView.swift +++ b/SFI/MainView.swift @@ -16,6 +16,10 @@ struct MainView: View { @State private var showConnections = false @State private var buttonState = ButtonVisibilityState() + private let profileEditor: (Binding, Bool) -> AnyView = { text, isEditable in + AnyView(RunestoneTextView(text: text, isEditable: isEditable)) + } + private var shouldShowBottomAccessory: Bool { guard !environments.extensionProfileLoading else { return false @@ -124,6 +128,7 @@ struct MainView: View { .environment(\.selection, $selection) .environment(\.importProfile, $importProfile) .environment(\.importRemoteProfile, $importRemoteProfile) + .environment(\.profileEditor, profileEditor) .handlesExternalEvents(preferring: [], allowing: ["*"]) .onOpenURL(perform: openURL) .sheet(isPresented: $showGroups) { @@ -160,6 +165,7 @@ struct MainView: View { .environment(\.selection, $selection) .environment(\.importProfile, $importProfile) .environment(\.importRemoteProfile, $importRemoteProfile) + .environment(\.profileEditor, profileEditor) .handlesExternalEvents(preferring: [], allowing: ["*"]) .onOpenURL(perform: openURL) } diff --git a/SFI/ProfileEditorTheme.swift b/SFI/ProfileEditorTheme.swift new file mode 100644 index 0000000..ace5457 --- /dev/null +++ b/SFI/ProfileEditorTheme.swift @@ -0,0 +1,71 @@ +import Runestone +import UIKit + +final class ProfileEditorTheme: Theme { + let font: UIFont = .monospacedSystemFont(ofSize: 14, weight: .regular) + let textColor: UIColor = .label + + let gutterBackgroundColor: UIColor = .secondarySystemBackground + let gutterHairlineColor: UIColor = .separator + + let lineNumberColor: UIColor = .secondaryLabel + let lineNumberFont: UIFont = .monospacedSystemFont(ofSize: 14, weight: .regular) + + let selectedLineBackgroundColor: UIColor = .systemFill + let selectedLinesLineNumberColor: UIColor = .label + let selectedLinesGutterBackgroundColor: UIColor = .secondarySystemBackground + + let invisibleCharactersColor: UIColor = .tertiaryLabel + + let pageGuideHairlineColor: UIColor = .separator + let pageGuideBackgroundColor: UIColor = .secondarySystemBackground + + let markedTextBackgroundColor: UIColor = .systemFill + let markedTextBackgroundCornerRadius: CGFloat = 4 + + func textColor(for rawHighlightName: String) -> UIColor? { + guard let highlightName = HighlightName(rawHighlightName) else { + return nil + } + switch highlightName { + case .comment: + return .secondaryLabel + case .property: + return .systemCyan + case .string: + return .systemGreen + case .number: + return .systemOrange + case .constantBuiltin: + return .systemPurple + case .error: + return .systemRed + } + } + + func fontTraits(for _: String) -> FontTraits { + [] + } +} + +private enum HighlightName: String { + case comment + case property + case string + case number + case constantBuiltin = "constant.builtin" + case error + + init?(_ rawHighlightName: String) { + var components = rawHighlightName.split(separator: ".") + while !components.isEmpty { + let candidateRawHighlightName = components.joined(separator: ".") + if let highlightName = Self(rawValue: candidateRawHighlightName) { + self = highlightName + return + } + components.removeLast() + } + return nil + } +} diff --git a/SFI/RunestoneTextView.swift b/SFI/RunestoneTextView.swift new file mode 100644 index 0000000..623242f --- /dev/null +++ b/SFI/RunestoneTextView.swift @@ -0,0 +1,83 @@ +import Runestone +import SwiftUI +import TreeSitterJSON5Runestone + +struct RunestoneTextView: UIViewRepresentable { + @Binding var text: String + let isEditable: Bool + + func makeUIView(context: Context) -> TextView { + let textView = TextView() + + textView.showLineNumbers = true + textView.isLineWrappingEnabled = false + textView.showTabs = false + textView.showSpaces = false + textView.showLineBreaks = false + textView.showSoftLineBreaks = false + textView.showNonBreakingSpaces = false + + textView.autocorrectionType = .no + textView.autocapitalizationType = .none + textView.smartDashesType = .no + textView.smartQuotesType = .no + textView.smartInsertDeleteType = .no + + textView.backgroundColor = .secondarySystemGroupedBackground + textView.contentInsetAdjustmentBehavior = .always + textView.alwaysBounceVertical = true + + textView.kern = 0.3 + textView.lineHeightMultiplier = 1.3 + + textView.characterPairs = [ + BasicCharacterPair(leading: "{", trailing: "}"), + BasicCharacterPair(leading: "[", trailing: "]"), + BasicCharacterPair(leading: "\"", trailing: "\""), + ] + + let theme = ProfileEditorTheme() + let state = TextViewState(text: text, theme: theme, language: .json5) + textView.setState(state) + + textView.isEditable = isEditable + textView.editorDelegate = context.coordinator + + return textView + } + + func updateUIView(_ textView: TextView, context _: Context) { + if textView.text != text { + textView.text = text + } + if textView.isEditable != isEditable { + textView.isEditable = isEditable + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + final class Coordinator: TextViewDelegate { + var parent: RunestoneTextView + + init(_ parent: RunestoneTextView) { + self.parent = parent + } + + func textViewDidChange(_ textView: TextView) { + parent.text = textView.text + } + } +} + +final class BasicCharacterPair: CharacterPair { + let leading: String + let trailing: String + + init(leading: String, trailing: String) { + self.leading = leading + self.trailing = trailing + } +} diff --git a/sing-box.xcodeproj/project.pbxproj b/sing-box.xcodeproj/project.pbxproj index cb8a326..aa14a41 100644 --- a/sing-box.xcodeproj/project.pbxproj +++ b/sing-box.xcodeproj/project.pbxproj @@ -29,6 +29,7 @@ 3A9759202A4EB69C00E4404B /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; 3A9759212A4EB69C00E4404B /* Library.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3AC194492A50013F00BD8CB9 /* IntentsExtension.appex in Embed ExtensionKit Extensions */ = {isa = PBXBuildFile; fileRef = 3A77016D2A4E6B34008F031F /* IntentsExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 3ACE5E032EE1A91300644196 /* CodeEditSourceEditor in Frameworks */ = {isa = PBXBuildFile; productRef = 3ACE5E022EE1A91200644196 /* CodeEditSourceEditor */; }; 3AE1719A2A8128DD00393060 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AF342B12A4AA520002B34AC /* NetworkExtension.framework */; }; 3AE171A62A81294400393060 /* TVExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3AE171992A8128DD00393060 /* TVExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 3AE171A92A81297300393060 /* Library.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3AEC211D2A459B4700A63465 /* Library.framework */; }; @@ -536,8 +537,6 @@ buildActionMask = 2147483647; files = ( 3A4EAD1B2A4FEB02005435B3 /* Library.framework in Frameworks */, - 3A2E87FB2ED5ABDA00644195 /* TreeSitterJSON5Runestone in Frameworks */, - 3A2E87F22ED5A91100644195 /* Runestone in Frameworks */, 3A4A020D2B53E3DC004EFB87 /* QRCode in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -585,6 +584,8 @@ files = ( 3A9759202A4EB69C00E4404B /* Library.framework in Frameworks */, 3A4EAD372A4FEC20005435B3 /* ApplicationLibrary.framework in Frameworks */, + 3A2E87F22ED5A91100644195 /* Runestone in Frameworks */, + 3A2E87FB2ED5ABDA00644195 /* TreeSitterJSON5Runestone in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -633,6 +634,7 @@ files = ( 3AEECC452A6DFE61006A0E0C /* ApplicationLibrary.framework in Frameworks */, 3AEECC4E2A6DFF13006A0E0C /* MacControlCenterUI in Frameworks */, + 3ACE5E032EE1A91300644196 /* CodeEditSourceEditor in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -758,8 +760,6 @@ name = ApplicationLibrary; packageProductDependencies = ( 3A4A020C2B53E3DC004EFB87 /* QRCode */, - 3A2E87F12ED5A91100644195 /* Runestone */, - 3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */, ); productName = ApplicationLibrary; productReference = 3A4EAD102A4FEAE6005435B3 /* ApplicationLibrary.framework */; @@ -872,6 +872,8 @@ ); name = SFI; packageProductDependencies = ( + 3A2E87F12ED5A91100644195 /* Runestone */, + 3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */, ); productName = SFI; productReference = 3AEC20F32A459AB400A63465 /* sing-box.app */; @@ -992,6 +994,7 @@ name = MacLibrary; packageProductDependencies = ( 3AEECC4D2A6DFF13006A0E0C /* MacControlCenterUI */, + 3ACE5E022EE1A91200644196 /* CodeEditSourceEditor */, ); productName = MacLibrary; productReference = 3AEECC2F2A6DFDAD006A0E0C /* MacLibrary.framework */; @@ -1064,8 +1067,9 @@ 3A017F902A4AB2E4009149FA /* XCRemoteSwiftPackageReference "GRDB" */, 3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */, 3A4A020B2B53E3DC004EFB87 /* XCRemoteSwiftPackageReference "qrcode" */, - 3A2E87F02ED5A91100644195 /* XCRemoteSwiftPackageReference "Runestone" */, + 3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */, 3A2E87F92ED5ABCF00644195 /* XCLocalSwiftPackageReference "Frameworks/TreeSitterJSON5" */, + 3ACE5E012EE1A91100644196 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */, ); productRefGroup = 3AEC20C72A45991900A63465 /* Products */; projectDirPath = ""; @@ -2562,6 +2566,10 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ + 3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Frameworks/Runestone; + }; 3A2E87F92ED5ABCF00644195 /* XCLocalSwiftPackageReference "Frameworks/TreeSitterJSON5" */ = { isa = XCLocalSwiftPackageReference; relativePath = Frameworks/TreeSitterJSON5; @@ -2577,14 +2585,6 @@ minimumVersion = 6.15.1; }; }; - 3A2E87F02ED5A91100644195 /* XCRemoteSwiftPackageReference "Runestone" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/simonbs/Runestone"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 0.5.1; - }; - }; 3A4A020B2B53E3DC004EFB87 /* XCRemoteSwiftPackageReference "qrcode" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/dagronf/qrcode.git"; @@ -2609,6 +2609,14 @@ minimumVersion = 2.0.0; }; }; + 3ACE5E012EE1A91100644196 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/CodeEditApp/CodeEditSourceEditor.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.15.2; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2619,7 +2627,7 @@ }; 3A2E87F12ED5A91100644195 /* Runestone */ = { isa = XCSwiftPackageProductDependency; - package = 3A2E87F02ED5A91100644195 /* XCRemoteSwiftPackageReference "Runestone" */; + package = 3A2E87F02ED5A91100644195 /* XCLocalSwiftPackageReference "Frameworks/Runestone" */; productName = Runestone; }; 3A2E87FA2ED5ABDA00644195 /* TreeSitterJSON5Runestone */ = { @@ -2637,6 +2645,11 @@ package = 3A7E90362A46778E00D53052 /* XCRemoteSwiftPackageReference "BinaryCodable" */; productName = BinaryCodable; }; + 3ACE5E022EE1A91200644196 /* CodeEditSourceEditor */ = { + isa = XCSwiftPackageProductDependency; + package = 3ACE5E012EE1A91100644196 /* XCRemoteSwiftPackageReference "CodeEditSourceEditor" */; + productName = CodeEditSourceEditor; + }; 3AEECC4D2A6DFF13006A0E0C /* MacControlCenterUI */ = { isa = XCSwiftPackageProductDependency; package = 3A57DF3A2A4D705000690BC5 /* XCRemoteSwiftPackageReference "MacControlCenterUI" */; diff --git a/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 47799b5..73872e7 100644 --- a/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/sing-box.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "48cbd083120736a1a1ce4b717dcbfdb7900aa5e2aa21991865021fb98adc2093", + "originHash" : "3ae592e476adf467b19c2cde1d4ba44683fb6f05a27fc1959105b8eb14cce1d2", "pins" : [ { "identity" : "binarycodable", @@ -10,6 +10,42 @@ "version" : "2.0.3" } }, + { + "identity" : "codeeditlanguages", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditLanguages.git", + "state" : { + "revision" : "331d5dbc5fc8513be5848fce8a2a312908f36a11", + "version" : "0.1.20" + } + }, + { + "identity" : "codeeditsourceeditor", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSourceEditor.git", + "state" : { + "revision" : "424453d2232c9912933a3b5a1f3d3df669404ed0", + "version" : "0.15.2" + } + }, + { + "identity" : "codeeditsymbols", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditSymbols.git", + "state" : { + "revision" : "ae69712b08571c4469c2ed5cd38ad9f19439793e", + "version" : "0.2.3" + } + }, + { + "identity" : "codeedittextview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CodeEditApp/CodeEditTextView.git", + "state" : { + "revision" : "d7ac3f11f22ec2e820187acce8f3a3fb7aa8ddec", + "version" : "0.12.1" + } + }, { "identity" : "grdb.swift", "kind" : "remoteSourceControl", @@ -47,12 +83,21 @@ } }, { - "identity" : "runestone", + "identity" : "rearrange", "kind" : "remoteSourceControl", - "location" : "https://github.com/simonbs/Runestone", + "location" : "https://github.com/ChimeHQ/Rearrange", "state" : { - "revision" : "1fad339aab99cf2136ce6bf8c32da3265b2e85e5", - "version" : "0.5.1" + "revision" : "f1d74e1642956f0300756ad8d1d64e9034857bc3", + "version" : "2.0.0" + } + }, + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections.git", + "state" : { + "revision" : "7b847a3b7008b2dc2f47ca3110d8c782fb2e5c7e", + "version" : "1.3.0" } }, { @@ -73,13 +118,49 @@ "version" : "1.6.1" } }, + { + "identity" : "swiftlintplugin", + "kind" : "remoteSourceControl", + "location" : "https://github.com/lukepistrol/SwiftLintPlugin", + "state" : { + "revision" : "9bbc46a4cf8275ceb39334f6276b6b215d67d5d5", + "version" : "0.62.2" + } + }, + { + "identity" : "swifttreesitter", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/SwiftTreeSitter.git", + "state" : { + "revision" : "08ef81eb8620617b55b08868126707ad72bf754f", + "version" : "0.25.0" + } + }, + { + "identity" : "textformation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextFormation", + "state" : { + "revision" : "b1ce9a14bd86042bba4de62236028dc4ce9db6a1", + "version" : "0.9.0" + } + }, + { + "identity" : "textstory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ChimeHQ/TextStory", + "state" : { + "revision" : "91df6fc9bd817f9712331a4a3e826f7bdc823e1d", + "version" : "0.9.1" + } + }, { "identity" : "tree-sitter", "kind" : "remoteSourceControl", "location" : "https://github.com/tree-sitter/tree-sitter", "state" : { - "revision" : "98be227227af10cc7a269cb3ffb23686c0610b17", - "version" : "0.20.9" + "revision" : "da6fe9beb4f7f67beb75914ca8e0d48ae48d6406", + "version" : "0.25.10" } } ],