Skip to content

Commit 68b48c6

Browse files
authored
Polish installer and About screen for release (#239)
* ui: polish installer and About screen * fix: preserve DMG background scale
1 parent 99bb21f commit 68b48c6

15 files changed

Lines changed: 386 additions & 87 deletions

Sources/VocaMac/App/BrandAssets.swift

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,6 @@ enum BrandAssets {
2727

2828
/// Brand green `#0F6B57`.
2929
static let brandGreen = NSColor(red: 0.059, green: 0.420, blue: 0.341, alpha: 1.0)
30-
31-
/// Settings / Talk-to-us teal `#0F6B57`. Tint template marks and About
32-
/// links with this. Do not bake the hex into official SVG files.
33-
static let settingsTeal = Color(
34-
red: 15.0 / 255.0,
35-
green: 107.0 / 255.0,
36-
blue: 87.0 / 255.0
37-
)
3830
}
3931

4032
/// Which artwork the menu bar should show for a given app status.
@@ -78,3 +70,23 @@ struct BrandLogoView: View {
7870
}
7971
}
8072
}
73+
74+
/// Renders the official standalone Voca mark as a monochrome template.
75+
struct BrandMarkView: View {
76+
let size: CGFloat
77+
78+
var body: some View {
79+
if let mark = BrandAssets.mark {
80+
Image(nsImage: mark)
81+
.renderingMode(.template)
82+
.resizable()
83+
.scaledToFit()
84+
.frame(width: size, height: size)
85+
.accessibilityHidden(true)
86+
} else {
87+
Image(systemName: "waveform")
88+
.font(.system(size: size))
89+
.accessibilityHidden(true)
90+
}
91+
}
92+
}
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
// AboutPlatformMark.swift
2+
// VocaMac
3+
//
4+
// Official platform marks used by the VocaHQ family links.
5+
6+
import AppKit
7+
import Foundation
8+
import SwiftUI
9+
10+
/// Official platform artwork from VocaHQ/.github.
11+
enum AboutPlatformMark: String, CaseIterable, Identifiable {
12+
case apple
13+
case android
14+
case windows
15+
case linux
16+
17+
var id: String { rawValue }
18+
19+
/// SHA-256 of the bundled canonical VocaHQ platform SVG.
20+
var expectedSHA256: String {
21+
switch self {
22+
case .apple: return "aab854e12a2a1a639c90662705452313b06b22c670bcfeeb49975a065d792afc"
23+
case .android: return "1862ad78b96e213c0ebd7366bdc4164c7d96b77ccd144fa90297bfe659165cea"
24+
case .windows: return "a2e1f084391d7acdf89e7d6c0c9a87247aaa1d60bee30b35a30a5283a14df357"
25+
case .linux: return "7107327b049ef9192577392d15810fead852a664f48d7f7034fe46e9a09fa8ef"
26+
}
27+
}
28+
29+
func svgData() -> Data? {
30+
guard let url = svgURL() else { return nil }
31+
return try? Data(contentsOf: url)
32+
}
33+
34+
/// Official path data from the bundled 24-point SVG.
35+
func officialPathData() -> String? {
36+
guard let text = String(data: svgData() ?? Data(), encoding: .utf8),
37+
let regex = try? NSRegularExpression(pattern: #"\bd="([^"]+)""#),
38+
let match = regex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)),
39+
let range = Range(match.range(at: 1), in: text) else {
40+
return nil
41+
}
42+
return String(text[range])
43+
}
44+
45+
func templateImage() -> NSImage? {
46+
Self.cacheLock.lock()
47+
defer { Self.cacheLock.unlock() }
48+
if let cached = Self.imageCache[rawValue] {
49+
return cached
50+
}
51+
guard let pathData = officialPathData(),
52+
let path = SVGPath.makeCGPath(from: pathData) else { return nil }
53+
let size = NSSize(width: 24, height: 24)
54+
let image = NSImage(size: size, flipped: false) { rect in
55+
guard let context = NSGraphicsContext.current?.cgContext else { return false }
56+
context.saveGState()
57+
context.translateBy(x: 0, y: rect.height)
58+
context.scaleBy(x: rect.width / 24, y: -rect.height / 24)
59+
context.setFillColor(NSColor.black.cgColor)
60+
context.addPath(path)
61+
context.fillPath(using: .winding)
62+
context.restoreGState()
63+
return true
64+
}
65+
image.isTemplate = true
66+
Self.imageCache[rawValue] = image
67+
return image
68+
}
69+
70+
private func svgURL() -> URL? {
71+
let bundle = Bundle.module
72+
return bundle.url(forResource: rawValue, withExtension: "svg", subdirectory: "Resources/platform")
73+
?? bundle.url(forResource: rawValue, withExtension: "svg", subdirectory: "platform")
74+
?? bundle.url(forResource: rawValue, withExtension: "svg")
75+
}
76+
77+
private static let cacheLock = NSLock()
78+
private static var imageCache: [String: NSImage] = [:]
79+
}
80+
81+
/// A VocaHQ product represented by its platform rather than a raw URL.
82+
struct AboutFamilyProduct: Identifiable, Equatable {
83+
let title: String
84+
let platform: String
85+
let url: URL
86+
let marks: [AboutPlatformMark]
87+
let systemImage: String?
88+
89+
var id: String { title }
90+
91+
static let all: [AboutFamilyProduct] = [
92+
AboutFamilyProduct(
93+
title: "VocaLinux",
94+
platform: "Linux",
95+
url: URL(string: "https://vocalinux.com")!,
96+
marks: [.linux],
97+
systemImage: nil
98+
),
99+
AboutFamilyProduct(
100+
title: "VocaMac",
101+
platform: "macOS",
102+
url: URL(string: "https://vocamac.com")!,
103+
marks: [.apple],
104+
systemImage: nil
105+
),
106+
AboutFamilyProduct(
107+
title: "VocaWin",
108+
platform: "Windows",
109+
url: URL(string: "https://vocawin.com")!,
110+
marks: [.windows],
111+
systemImage: nil
112+
),
113+
AboutFamilyProduct(
114+
title: "VocaPhone",
115+
platform: "iOS & Android",
116+
url: URL(string: "https://vocaphone.vocahq.com")!,
117+
marks: [.apple, .android],
118+
systemImage: nil
119+
),
120+
AboutFamilyProduct(
121+
title: "VocaGateway",
122+
platform: "Self-hosted",
123+
url: URL(string: "https://vocagateway.vocahq.com")!,
124+
marks: [],
125+
systemImage: "server.rack"
126+
),
127+
]
128+
}
129+
130+
/// Canonical organization and project links shown on the About screen.
131+
enum AboutLinks {
132+
static let headquarters = URL(string: "https://vocahq.com")!
133+
static let contributors = URL(
134+
string: "https://github.com/VocaHQ/vocamac#:~:text=about%20GitHub%20Sponsors-,Contributors,-9"
135+
)!
136+
}
137+
138+
/// A platform mark that follows the current macOS foreground style.
139+
struct AboutPlatformMarkImage: View {
140+
let mark: AboutPlatformMark
141+
var size: CGFloat = 24
142+
143+
var body: some View {
144+
if let image = mark.templateImage() {
145+
Image(nsImage: image)
146+
.renderingMode(.template)
147+
.resizable()
148+
.scaledToFit()
149+
.frame(width: size, height: size)
150+
.accessibilityHidden(true)
151+
}
152+
}
153+
}

Sources/VocaMac/Models/AboutSocialMark.swift

Lines changed: 19 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import SwiftUI
1515
/// Files live in `Resources/social/` and are the exact SVG bytes from
1616
/// VocaHQ/.github `brand/vocahq/social`. On disk, `fill` stays
1717
/// `currentColor`. AppKit does not honor that, so drawing substitutes
18-
/// black in memory, rasters a template image, and tints it Settings teal.
18+
/// black in memory, rasters a template image, and lets macOS tint it.
1919
enum AboutSocialMark: String, CaseIterable, Identifiable {
2020
case github
2121
case discord
@@ -81,30 +81,17 @@ enum AboutSocialMark: String, CaseIterable, Identifiable {
8181
return String(text[range])
8282
}
8383

84-
/// Template image so Settings teal can tint the official mark.
84+
/// Template image so the current macOS control style can tint the mark.
8585
///
86-
/// Loads the official bytes, swaps `currentColor` to `#000000` in
87-
/// memory only, rasters that, and caches the result. If AppKit still
88-
/// returns an empty image, rasters the official path `d` instead.
86+
/// Rasters the official path directly and caches the result. This avoids
87+
/// CoreSVG's inconsistent `currentColor` handling at small control sizes.
88+
/// The full SVG remains a fallback if the path parser cannot load a mark.
8989
func templateImage() -> NSImage? {
9090
Self.cacheLock.lock()
9191
defer { Self.cacheLock.unlock() }
9292
if let cached = Self.imageCache[rawValue] {
9393
return cached
9494
}
95-
guard let official = svgData(),
96-
let svg = String(data: official, encoding: .utf8),
97-
let drawable = svg.replacingOccurrences(of: "currentColor", with: "#000000").data(using: .utf8)
98-
else {
99-
return nil
100-
}
101-
102-
if let image = Self.rasterizedTemplate(fromSVG: drawable), Self.hasVisiblePixels(image) {
103-
image.isTemplate = true
104-
Self.imageCache[rawValue] = image
105-
return image
106-
}
107-
10895
if let d = officialPathData(),
10996
let image = Self.rasterizedTemplate(fromPath: d),
11097
Self.hasVisiblePixels(image) {
@@ -113,7 +100,17 @@ enum AboutSocialMark: String, CaseIterable, Identifiable {
113100
return image
114101
}
115102

116-
return nil
103+
guard let official = svgData(),
104+
let svg = String(data: official, encoding: .utf8),
105+
let drawable = svg.replacingOccurrences(of: "currentColor", with: "#000000").data(using: .utf8),
106+
let image = Self.rasterizedTemplate(fromSVG: drawable),
107+
Self.hasVisiblePixels(image)
108+
else {
109+
return nil
110+
}
111+
image.isTemplate = true
112+
Self.imageCache[rawValue] = image
113+
return image
117114
}
118115

119116
private static let cacheLock = NSLock()
@@ -202,25 +199,13 @@ enum AboutSocialMark: String, CaseIterable, Identifiable {
202199
}
203200
}
204201

205-
/// Official VocaDesign mark, drawn at 16pt in a text button.
206-
///
207-
/// Talk-to-us icons tint with Settings teal `#0F6B57` through template
208-
/// rendering so they match About links. Pass `tint: nil` on a filled
209-
/// control so the button label color wins. Do not bake `#0F6B57` into
210-
/// the SVG files.
202+
/// Official VocaDesign mark, drawn as a macOS template image.
211203
struct AboutSocialMarkImage: View {
212204
let mark: AboutSocialMark
213205
var size: CGFloat = 16
214-
var tint: Color? = BrandAssets.settingsTeal
215206

216207
var body: some View {
217-
Group {
218-
if let tint {
219-
markImage.foregroundStyle(tint)
220-
} else {
221-
markImage
222-
}
223-
}
208+
markImage
224209
}
225210

226211
@ViewBuilder
@@ -234,7 +219,7 @@ struct AboutSocialMarkImage: View {
234219
.accessibilityHidden(true)
235220
} else if let d = mark.officialPathData() {
236221
OfficialSVGPathShape(d: d)
237-
.fill(tint ?? Color.primary)
222+
.fill(Color.primary)
238223
.frame(width: size, height: size)
239224
.accessibilityHidden(true)
240225
} else {

Sources/VocaMac/Models/SettingsSearchIndex.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,8 +219,8 @@ enum SettingsSearchIndex {
219219
id: "family",
220220
page: .about,
221221
title: "Part of VocaHQ",
222-
subtitle: "VocaLinux, VocaMac, VocaPhone, VocaGateway",
223-
keywords: ["family", "vocahq", "vocalinux", "vocamac", "vocaphone", "vocagateway"]
222+
subtitle: "VocaLinux, VocaMac, VocaWin, VocaPhone, VocaGateway",
223+
keywords: ["family", "vocahq", "vocalinux", "vocamac", "vocawin", "windows", "vocaphone", "vocagateway"]
224224
),
225225
SettingsSearchEntry(
226226
id: "github-issues",
-3.31 KB
Loading
-7.59 KB
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading

0 commit comments

Comments
 (0)