mirror of
https://github.com/h3pdesign/Neon-Vision-Editor
synced 2026-04-21 13:27:16 +00:00
release: prepare v0.4.15
This commit is contained in:
parent
43180529de
commit
763eea0e5e
8 changed files with 149 additions and 41 deletions
|
|
@ -4,6 +4,11 @@ All notable changes to **Neon Vision Editor** are documented in this file.
|
|||
|
||||
The format follows *Keep a Changelog*. Versions use semantic versioning with prerelease tags.
|
||||
|
||||
## [v0.4.15] - 2026-02-14
|
||||
|
||||
### Fixed
|
||||
- Fixed the editor `Highlight Current Line` behavior on macOS so previous line background highlights are cleared and only the active line remains highlighted.
|
||||
|
||||
## [v0.4.14] - 2026-02-14
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -358,7 +358,7 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 228;
|
||||
CURRENT_PROJECT_VERSION = 229;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
|
|
@ -401,7 +401,7 @@
|
|||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 0.4.14;
|
||||
MARKETING_VERSION = 0.4.15;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
@ -438,7 +438,7 @@
|
|||
CODE_SIGNING_ALLOWED = YES;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 228;
|
||||
CURRENT_PROJECT_VERSION = 229;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = CS727NF72U;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
|
|
@ -481,7 +481,7 @@
|
|||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.0;
|
||||
MARKETING_VERSION = 0.4.14;
|
||||
MARKETING_VERSION = 0.4.15;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "h3p.Neon-Vision-Editor";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
|
|
|
|||
|
|
@ -109,6 +109,9 @@ final class AppUpdateManager: ObservableObject {
|
|||
@Published private(set) var automaticPromptToken: Int = 0
|
||||
@Published private(set) var isInstalling: Bool = false
|
||||
@Published private(set) var installMessage: String?
|
||||
@Published private(set) var installProgress: Double = 0
|
||||
@Published private(set) var installPhase: String = ""
|
||||
@Published private(set) var awaitingInstallCompletionAction: Bool = false
|
||||
@Published private(set) var lastCheckResultSummary: String = "Never checked"
|
||||
|
||||
private let owner: String
|
||||
|
|
@ -250,7 +253,7 @@ final class AppUpdateManager: ObservableObject {
|
|||
automaticPromptToken &+= 1
|
||||
installMessage = "Automatic install is disabled while running from Xcode/DerivedData."
|
||||
} else {
|
||||
await attemptAutoInstall()
|
||||
await attemptAutoInstall(interactive: false)
|
||||
}
|
||||
} else {
|
||||
pendingAutomaticPrompt = true
|
||||
|
|
@ -321,6 +324,9 @@ final class AppUpdateManager: ObservableObject {
|
|||
|
||||
func clearInstallMessage() {
|
||||
installMessage = nil
|
||||
installProgress = 0
|
||||
installPhase = ""
|
||||
awaitingInstallCompletionAction = false
|
||||
}
|
||||
|
||||
func installUpdateNow() async {
|
||||
|
|
@ -332,7 +338,24 @@ final class AppUpdateManager: ObservableObject {
|
|||
installMessage = "Install now is disabled while running from Xcode/DerivedData. Use Download Update instead."
|
||||
return
|
||||
}
|
||||
await attemptAutoInstall()
|
||||
await attemptAutoInstall(interactive: true)
|
||||
}
|
||||
|
||||
func completeInstalledUpdate(restart: Bool) {
|
||||
#if os(macOS)
|
||||
guard awaitingInstallCompletionAction else { return }
|
||||
let currentApp = Bundle.main.bundleURL.standardizedFileURL
|
||||
if restart {
|
||||
installMessage = "Restarting app…"
|
||||
NSWorkspace.shared.openApplication(at: currentApp, configuration: NSWorkspace.OpenConfiguration(), completionHandler: nil)
|
||||
} else {
|
||||
installMessage = "Closing app to finish update…"
|
||||
}
|
||||
awaitingInstallCompletionAction = false
|
||||
NSApp.terminate(nil)
|
||||
#else
|
||||
installMessage = "Automatic install is supported on macOS only."
|
||||
#endif
|
||||
}
|
||||
|
||||
private func shouldRunInitialCheckNow() -> Bool {
|
||||
|
|
@ -486,7 +509,7 @@ final class AppUpdateManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func attemptAutoInstall() async {
|
||||
private func attemptAutoInstall(interactive: Bool) async {
|
||||
#if os(macOS)
|
||||
guard !isInstalling else { return }
|
||||
guard let release = latestRelease else { return }
|
||||
|
|
@ -500,6 +523,9 @@ final class AppUpdateManager: ObservableObject {
|
|||
}
|
||||
|
||||
isInstalling = true
|
||||
installProgress = 0.01
|
||||
installPhase = "Preparing installer…"
|
||||
awaitingInstallCompletionAction = false
|
||||
defer { isInstalling = false }
|
||||
|
||||
do {
|
||||
|
|
@ -507,11 +533,15 @@ final class AppUpdateManager: ObservableObject {
|
|||
// 1) verify artifact checksum from release metadata
|
||||
// 2) verify code signature validity + signing identity
|
||||
let expectedHash = try Self.extractSHA256(from: release.notes, preferredAssetName: assetName)
|
||||
installProgress = 0.12
|
||||
installPhase = "Downloading release asset…"
|
||||
let (tmpURL, response) = try await session.download(from: downloadURL)
|
||||
guard let http = response as? HTTPURLResponse, 200..<300 ~= http.statusCode else {
|
||||
throw URLError(.badServerResponse)
|
||||
}
|
||||
|
||||
installProgress = 0.40
|
||||
installPhase = "Verifying checksum…"
|
||||
let actualHash = try Self.sha256Hex(of: tmpURL)
|
||||
guard actualHash.caseInsensitiveCompare(expectedHash) == .orderedSame else {
|
||||
throw UpdateError.checksumMismatch
|
||||
|
|
@ -525,6 +555,8 @@ final class AppUpdateManager: ObservableObject {
|
|||
let downloadedZip = workDir.appendingPathComponent(assetName)
|
||||
try fileManager.moveItem(at: tmpURL, to: downloadedZip)
|
||||
|
||||
installProgress = 0.56
|
||||
installPhase = "Unpacking update…"
|
||||
let unzipStatus = try Self.unzip(zipURL: downloadedZip, to: unzipDir)
|
||||
guard unzipStatus == 0 else {
|
||||
throw UpdateError.installUnsupported("Failed to unpack update archive.")
|
||||
|
|
@ -533,6 +565,8 @@ final class AppUpdateManager: ObservableObject {
|
|||
guard let appBundle = Self.findFirstAppBundle(in: unzipDir) else {
|
||||
throw UpdateError.installUnsupported("No .app bundle found in downloaded update.")
|
||||
}
|
||||
installProgress = 0.70
|
||||
installPhase = "Verifying app signature…"
|
||||
guard try Self.verifyCodeSignatureStrictCLI(of: appBundle) else {
|
||||
throw UpdateError.invalidCodeSignature
|
||||
}
|
||||
|
|
@ -556,6 +590,8 @@ final class AppUpdateManager: ObservableObject {
|
|||
let backupApp = targetDir.appendingPathComponent("\(currentApp.deletingPathExtension().lastPathComponent)-backup-\(Int(Date().timeIntervalSince1970)).app")
|
||||
|
||||
do {
|
||||
installProgress = 0.86
|
||||
installPhase = "Installing app update…"
|
||||
try fileManager.moveItem(at: currentApp, to: backupApp)
|
||||
try fileManager.moveItem(at: appBundle, to: currentApp)
|
||||
} catch {
|
||||
|
|
@ -565,10 +601,19 @@ final class AppUpdateManager: ObservableObject {
|
|||
throw UpdateError.installUnsupported("Automatic install failed due to file permissions. Use Download Update for manual install.")
|
||||
}
|
||||
|
||||
installMessage = "Update installed. Relaunching…"
|
||||
NSWorkspace.shared.openApplication(at: currentApp, configuration: NSWorkspace.OpenConfiguration(), completionHandler: nil)
|
||||
NSApp.terminate(nil)
|
||||
installProgress = 1.0
|
||||
installPhase = "Install complete."
|
||||
if interactive {
|
||||
awaitingInstallCompletionAction = true
|
||||
installMessage = "Update installed. Choose “Restart App” or “Install & Close App”."
|
||||
} else {
|
||||
installMessage = "Update installed. Relaunching…"
|
||||
NSWorkspace.shared.openApplication(at: currentApp, configuration: NSWorkspace.OpenConfiguration(), completionHandler: nil)
|
||||
NSApp.terminate(nil)
|
||||
}
|
||||
} catch {
|
||||
installProgress = 0
|
||||
installPhase = ""
|
||||
installMessage = error.localizedDescription
|
||||
if let release = latestRelease {
|
||||
openURL(release.downloadURL ?? release.releaseURL)
|
||||
|
|
|
|||
|
|
@ -119,8 +119,15 @@ struct AppUpdaterDialog: View {
|
|||
.foregroundStyle(.secondary)
|
||||
|
||||
if appUpdateManager.isInstalling {
|
||||
ProgressView("Installing update…")
|
||||
.font(.caption)
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
ProgressView(value: appUpdateManager.installProgress, total: 1.0) {
|
||||
Text(appUpdateManager.installPhase.isEmpty ? "Installing update…" : appUpdateManager.installPhase)
|
||||
.font(.caption)
|
||||
}
|
||||
Text("\(Int((appUpdateManager.installProgress * 100).rounded()))%")
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if let installMessage = appUpdateManager.installMessage {
|
||||
|
|
@ -140,31 +147,43 @@ struct AppUpdaterDialog: View {
|
|||
HStack {
|
||||
switch appUpdateManager.status {
|
||||
case .updateAvailable:
|
||||
Button("Skip This Version") {
|
||||
appUpdateManager.skipCurrentVersion()
|
||||
isPresented = false
|
||||
}
|
||||
if appUpdateManager.awaitingInstallCompletionAction {
|
||||
Spacer()
|
||||
|
||||
Button("Remind Me Tomorrow") {
|
||||
appUpdateManager.remindMeTomorrow()
|
||||
isPresented = false
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Download Update") {
|
||||
appUpdateManager.openDownloadPage()
|
||||
isPresented = false
|
||||
}
|
||||
|
||||
Button("Install Now") {
|
||||
Task {
|
||||
await appUpdateManager.installUpdateNow()
|
||||
Button("Install & Close App") {
|
||||
appUpdateManager.completeInstalledUpdate(restart: false)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(appUpdateManager.isInstalling)
|
||||
|
||||
Button("Restart App") {
|
||||
appUpdateManager.completeInstalledUpdate(restart: true)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
} else {
|
||||
Button("Skip This Version") {
|
||||
appUpdateManager.skipCurrentVersion()
|
||||
isPresented = false
|
||||
}
|
||||
|
||||
Button("Remind Me Tomorrow") {
|
||||
appUpdateManager.remindMeTomorrow()
|
||||
isPresented = false
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Download Update") {
|
||||
appUpdateManager.openDownloadPage()
|
||||
isPresented = false
|
||||
}
|
||||
|
||||
Button("Install Now") {
|
||||
Task {
|
||||
await appUpdateManager.installUpdateNow()
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(appUpdateManager.isInstalling)
|
||||
}
|
||||
case .failed:
|
||||
Button("Close") {
|
||||
isPresented = false
|
||||
|
|
|
|||
|
|
@ -1838,9 +1838,9 @@ struct ContentView: View {
|
|||
highlightRefreshToken: highlightRefreshToken
|
||||
)
|
||||
.id(currentLanguage)
|
||||
.frame(maxWidth: viewModel.isBrainDumpMode ? 800 : .infinity)
|
||||
.frame(maxWidth: viewModel.isBrainDumpMode ? 920 : .infinity)
|
||||
.frame(maxHeight: .infinity)
|
||||
.padding(.horizontal, viewModel.isBrainDumpMode ? 100 : 0)
|
||||
.padding(.horizontal, viewModel.isBrainDumpMode ? 24 : 0)
|
||||
.padding(.vertical, viewModel.isBrainDumpMode ? 40 : 0)
|
||||
.background(
|
||||
Group {
|
||||
|
|
@ -1856,6 +1856,11 @@ struct ContentView: View {
|
|||
wordCountView
|
||||
}
|
||||
}
|
||||
.frame(
|
||||
maxWidth: .infinity,
|
||||
maxHeight: .infinity,
|
||||
alignment: viewModel.isBrainDumpMode ? .top : .topLeading
|
||||
)
|
||||
|
||||
if showProjectStructureSidebar && !viewModel.isBrainDumpMode {
|
||||
Divider()
|
||||
|
|
@ -1872,6 +1877,15 @@ struct ContentView: View {
|
|||
.frame(minWidth: 220, idealWidth: 260, maxWidth: 340)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
Group {
|
||||
if viewModel.isBrainDumpMode && enableTranslucentWindow {
|
||||
Color.clear.background(.ultraThinMaterial)
|
||||
} else {
|
||||
Color.clear
|
||||
}
|
||||
}
|
||||
)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
|
||||
let withEvents = withTypingEvents(
|
||||
|
|
|
|||
|
|
@ -1845,6 +1845,9 @@ struct CustomTextEditor: NSViewRepresentable {
|
|||
tv.textStorage?.beginEditing()
|
||||
// Clear previous coloring and apply base color
|
||||
tv.textStorage?.removeAttribute(.foregroundColor, range: fullRange)
|
||||
// Clear previous background/underline artifacts so caret-line highlight doesn't accumulate.
|
||||
tv.textStorage?.removeAttribute(.backgroundColor, range: fullRange)
|
||||
tv.textStorage?.removeAttribute(.underlineStyle, range: fullRange)
|
||||
tv.textStorage?.addAttribute(.foregroundColor, value: baseColor, range: fullRange)
|
||||
// Apply colored ranges
|
||||
for (range, color) in coloredRanges {
|
||||
|
|
|
|||
|
|
@ -155,7 +155,8 @@ struct NeonSettingsView: View {
|
|||
.background(
|
||||
SettingsWindowConfigurator(
|
||||
minSize: NSSize(width: 900, height: 820),
|
||||
idealSize: NSSize(width: 980, height: 880)
|
||||
idealSize: NSSize(width: 980, height: 880),
|
||||
translucentEnabled: supportsTranslucency && translucentWindow
|
||||
)
|
||||
)
|
||||
#endif
|
||||
|
|
@ -1056,6 +1057,7 @@ final class FontPickerController: NSObject, NSFontChanging {
|
|||
struct SettingsWindowConfigurator: NSViewRepresentable {
|
||||
let minSize: NSSize
|
||||
let idealSize: NSSize
|
||||
let translucentEnabled: Bool
|
||||
|
||||
func makeNSView(context: Context) -> NSView {
|
||||
let view = NSView(frame: .zero)
|
||||
|
|
@ -1077,11 +1079,27 @@ struct SettingsWindowConfigurator: NSViewRepresentable {
|
|||
width: max(window.minSize.width, minSize.width),
|
||||
height: max(window.minSize.height, minSize.height)
|
||||
)
|
||||
// Match native macOS Settings layout: centered preference tabs and hidden title text.
|
||||
window.toolbarStyle = .preference
|
||||
window.titleVisibility = .hidden
|
||||
let targetWidth = max(window.frame.size.width, idealSize.width)
|
||||
let targetHeight = max(window.frame.size.height, idealSize.height)
|
||||
if targetWidth != window.frame.size.width || targetHeight != window.frame.size.height {
|
||||
window.setContentSize(NSSize(width: targetWidth, height: targetHeight))
|
||||
}
|
||||
|
||||
// Keep settings-window translucency in sync without relying on editor view events.
|
||||
window.isOpaque = !translucentEnabled
|
||||
window.backgroundColor = translucentEnabled ? .clear : NSColor.windowBackgroundColor
|
||||
window.titlebarAppearsTransparent = translucentEnabled
|
||||
if translucentEnabled {
|
||||
window.styleMask.insert(.fullSizeContentView)
|
||||
} else {
|
||||
window.styleMask.remove(.fullSizeContentView)
|
||||
}
|
||||
if #available(macOS 13.0, *) {
|
||||
window.titlebarSeparatorStyle = translucentEnabled ? .none : .automatic
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
12
README.md
12
README.md
|
|
@ -17,7 +17,7 @@
|
|||
</p>
|
||||
|
||||
> Status: **active release**
|
||||
> Latest release: **v0.4.14**
|
||||
> Latest release: **v0.4.15**
|
||||
> Platform target: **macOS 26 (Tahoe)** compatible with **macOS Sequoia**
|
||||
> Apple Silicon: tested / Intel: not tested
|
||||
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
Prebuilt binaries are available on [GitHub Releases](https://github.com/h3pdesign/Neon-Vision-Editor/releases).
|
||||
|
||||
- Latest release: **v0.4.14**
|
||||
- Latest release: **v0.4.15**
|
||||
- TestFlight beta: [Join here](https://testflight.apple.com/join/YWB2fGAP)
|
||||
- Architecture: Apple Silicon (Intel not tested)
|
||||
- Notarization: *is finally there*
|
||||
|
|
@ -122,6 +122,10 @@ If macOS blocks first launch:
|
|||
|
||||
## Changelog
|
||||
|
||||
### v0.4.15 (summary)
|
||||
|
||||
- Fixed the editor `Highlight Current Line` behavior on macOS so previous line background highlights are cleared and only the active line remains highlighted.
|
||||
|
||||
### v0.4.14 (summary)
|
||||
|
||||
- Added centralized theme canonicalization with an explicit `Custom` option in settings so legacy/case-variant values resolve consistently across launches.
|
||||
|
|
@ -164,12 +168,12 @@ Full release history: [`CHANGELOG.md`](CHANGELOG.md)
|
|||
|
||||
## Release Integrity
|
||||
|
||||
- Tag: `v0.4.14`
|
||||
- Tag: `v0.4.15`
|
||||
- Tagged commit: `TBD`
|
||||
- Verify local tag target:
|
||||
|
||||
```bash
|
||||
git rev-parse --verify v0.4.14
|
||||
git rev-parse --verify v0.4.15
|
||||
```
|
||||
|
||||
- Verify downloaded artifact checksum locally:
|
||||
|
|
|
|||
Loading…
Reference in a new issue