mirror of
https://github.com/MioMioOS/mio-plugin-music.git
synced 2026-06-11 03:44:31 +00:00
v2.0.1: compact UI + AppleScript timeouts
UI polish (ExpandedView rewrite): - Horizontal hero row: 128×128 album art on the left, title/artist/ album + source badge on the right. Half the vertical footprint of v2.0.0 at the same info density. - Dropped the "NOW PLAYING" eyebrow (redundant with the source badge). - Tightened outer padding 28 → 20, inter-section spacing 22-28 → 16. - Play button 56 → 48, prev/next 44 → 36; still 44pt tap targets via the invisible hover frame. AppleScript timeout fix (the real bug, unrelated to UI): - Every fetch() script now wraps the `tell application` block in `with timeout of N seconds` (2s for Spotify/Music, 3s for Chrome). - Music.app hanging was stalling the entire source router for 120s (default AppleEvent timeout), freezing the UI on stale Spotify data. - runAppleScript() suppresses error -1712 (errAETimeout) alongside existing -600 / -1728 — expected, not noisy. Info.plist: CFBundleShortVersionString 2.0.0 → 2.0.1, CFBundleVersion 2 → 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
64daaa3371
commit
c67ddd0024
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@ build/
|
|||||||
*.swiftmodule
|
*.swiftmodule
|
||||||
*.dSYM
|
*.dSYM
|
||||||
.build/
|
.build/
|
||||||
|
icon/
|
||||||
|
|||||||
@ -15,9 +15,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.0.0</string>
|
<string>2.0.1</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2</string>
|
<string>3</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>MusicPlugin.MusicPlugin</string>
|
<string>MusicPlugin.MusicPlugin</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@ -35,7 +35,7 @@ final class MusicPlugin: NSObject, MioPlugin {
|
|||||||
var id: String { "music-player" }
|
var id: String { "music-player" }
|
||||||
var name: String { "Music Player" }
|
var name: String { "Music Player" }
|
||||||
var icon: String { "music.note" }
|
var icon: String { "music.note" }
|
||||||
var version: String { "2.0.0" }
|
var version: String { "2.0.1" }
|
||||||
|
|
||||||
func activate() {
|
func activate() {
|
||||||
NSLog("[mio-plugin-music] activate")
|
NSLog("[mio-plugin-music] activate")
|
||||||
|
|||||||
@ -432,7 +432,11 @@ func runAppleScript(_ source: String, tag: String) async -> String? {
|
|||||||
let result = script.executeAndReturnError(&errorDict)
|
let result = script.executeAndReturnError(&errorDict)
|
||||||
if let errorDict {
|
if let errorDict {
|
||||||
let num = errorDict[NSAppleScript.errorNumber] as? Int ?? 0
|
let num = errorDict[NSAppleScript.errorNumber] as? Int ?? 0
|
||||||
if num != -600 && num != -1728 {
|
// Silence known-expected error codes:
|
||||||
|
// -600 = application not running
|
||||||
|
// -1712 = errAETimeout (our `with timeout of N seconds` firing)
|
||||||
|
// -1728 = AEError, generic Apple Event descriptor issue
|
||||||
|
if num != -600 && num != -1712 && num != -1728 {
|
||||||
let msg = errorDict[NSAppleScript.errorMessage] as? String ?? "<no message>"
|
let msg = errorDict[NSAppleScript.errorMessage] as? String ?? "<no message>"
|
||||||
NSLog("[mio-plugin-music] AppleScript error [\(tag)] \(num): \(msg)")
|
NSLog("[mio-plugin-music] AppleScript error [\(tag)] \(num): \(msg)")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,10 +18,17 @@ enum AppleMusicAppleScript {
|
|||||||
// MARK: - Fetch
|
// MARK: - Fetch
|
||||||
|
|
||||||
static func fetch() async -> AppleScriptTrackInfo? {
|
static func fetch() async -> AppleScriptTrackInfo? {
|
||||||
|
// Music.app occasionally stalls its AppleEvent handler (observed in
|
||||||
|
// macOS 15.x when the app is mid-sync). Without an explicit timeout
|
||||||
|
// each fetch inherits the 120-second default, which freezes the whole
|
||||||
|
// source router for 2 minutes. `with timeout of 2 seconds` raises
|
||||||
|
// errAETimeout (-1712) if Music doesn't respond quickly, and our
|
||||||
|
// Swift layer turns that into nil so the router can move on.
|
||||||
let script = """
|
let script = """
|
||||||
tell application "System Events"
|
tell application "System Events"
|
||||||
if not (exists process "Music") then return "NOT_RUNNING"
|
if not (exists process "Music") then return "NOT_RUNNING"
|
||||||
end tell
|
end tell
|
||||||
|
with timeout of 2 seconds
|
||||||
tell application "Music"
|
tell application "Music"
|
||||||
if player state is playing or player state is paused then
|
if player state is playing or player state is paused then
|
||||||
set trackName to name of current track
|
set trackName to name of current track
|
||||||
@ -36,6 +43,7 @@ enum AppleMusicAppleScript {
|
|||||||
return "NOT_PLAYING"
|
return "NOT_PLAYING"
|
||||||
end if
|
end if
|
||||||
end tell
|
end tell
|
||||||
|
end timeout
|
||||||
"""
|
"""
|
||||||
|
|
||||||
guard let raw = await runAppleScript(script, tag: "music") else { return nil }
|
guard let raw = await runAppleScript(script, tag: "music") else { return nil }
|
||||||
|
|||||||
@ -34,10 +34,13 @@ enum ChromeWebSource {
|
|||||||
// MARK: - Fetch
|
// MARK: - Fetch
|
||||||
|
|
||||||
static func fetch() async -> ChromeTrackInfo? {
|
static func fetch() async -> ChromeTrackInfo? {
|
||||||
|
// Iterating N tabs × JS injection is O(N) and can be slow with many
|
||||||
|
// tabs; cap at 3 seconds so the router doesn't stall the whole cycle.
|
||||||
let script = """
|
let script = """
|
||||||
tell application "System Events"
|
tell application "System Events"
|
||||||
if not (exists process "Google Chrome") then return "NOT_RUNNING"
|
if not (exists process "Google Chrome") then return "NOT_RUNNING"
|
||||||
end tell
|
end tell
|
||||||
|
with timeout of 3 seconds
|
||||||
tell application "Google Chrome"
|
tell application "Google Chrome"
|
||||||
set playingTitle to ""
|
set playingTitle to ""
|
||||||
set playingURL to ""
|
set playingURL to ""
|
||||||
@ -70,6 +73,7 @@ enum ChromeWebSource {
|
|||||||
if playingURL is not "" then return "PLAYING_TAB||" & playingTitle & "||" & playingURL & "||" & playingInfo
|
if playingURL is not "" then return "PLAYING_TAB||" & playingTitle & "||" & playingURL & "||" & playingInfo
|
||||||
return "NOT_FOUND"
|
return "NOT_FOUND"
|
||||||
end tell
|
end tell
|
||||||
|
end timeout
|
||||||
"""
|
"""
|
||||||
|
|
||||||
guard let raw = await runAppleScript(script, tag: "chrome") else { return nil }
|
guard let raw = await runAppleScript(script, tag: "chrome") else { return nil }
|
||||||
|
|||||||
@ -34,10 +34,15 @@ enum SpotifyAppleScript {
|
|||||||
// MARK: - Fetch
|
// MARK: - Fetch
|
||||||
|
|
||||||
static func fetch() async -> AppleScriptTrackInfo? {
|
static func fetch() async -> AppleScriptTrackInfo? {
|
||||||
|
// `with timeout of 2 seconds` bounds the tell block; if Spotify hangs,
|
||||||
|
// AppleScript raises errAETimeout (-1712) and our Swift layer returns
|
||||||
|
// nil so the source router can move on instead of the serial queue
|
||||||
|
// stalling for the default 120-second AppleEvent timeout.
|
||||||
let script = """
|
let script = """
|
||||||
tell application "System Events"
|
tell application "System Events"
|
||||||
if not (exists process "Spotify") then return "NOT_RUNNING"
|
if not (exists process "Spotify") then return "NOT_RUNNING"
|
||||||
end tell
|
end tell
|
||||||
|
with timeout of 2 seconds
|
||||||
tell application "Spotify"
|
tell application "Spotify"
|
||||||
if player state is playing or player state is paused then
|
if player state is playing or player state is paused then
|
||||||
set trackName to name of current track
|
set trackName to name of current track
|
||||||
@ -56,6 +61,7 @@ enum SpotifyAppleScript {
|
|||||||
return "NOT_PLAYING"
|
return "NOT_PLAYING"
|
||||||
end if
|
end if
|
||||||
end tell
|
end tell
|
||||||
|
end timeout
|
||||||
"""
|
"""
|
||||||
|
|
||||||
guard let raw = await runAppleScript(script, tag: "spotify") else { return nil }
|
guard let raw = await runAppleScript(script, tag: "spotify") else { return nil }
|
||||||
@ -87,6 +93,7 @@ enum SpotifyAppleScript {
|
|||||||
tell application "System Events"
|
tell application "System Events"
|
||||||
if not (exists process "Spotify") then return ""
|
if not (exists process "Spotify") then return ""
|
||||||
end tell
|
end tell
|
||||||
|
with timeout of 2 seconds
|
||||||
tell application "Spotify"
|
tell application "Spotify"
|
||||||
try
|
try
|
||||||
return artwork url of current track
|
return artwork url of current track
|
||||||
@ -94,6 +101,7 @@ enum SpotifyAppleScript {
|
|||||||
return ""
|
return ""
|
||||||
end try
|
end try
|
||||||
end tell
|
end tell
|
||||||
|
end timeout
|
||||||
"""
|
"""
|
||||||
guard let urlString = await runAppleScript(script, tag: "spotify-art"),
|
guard let urlString = await runAppleScript(script, tag: "spotify-art"),
|
||||||
!urlString.isEmpty,
|
!urlString.isEmpty,
|
||||||
|
|||||||
@ -9,13 +9,18 @@
|
|||||||
// 3. Nothing playing (title.isEmpty)
|
// 3. Nothing playing (title.isEmpty)
|
||||||
// 4. Now playing (default)
|
// 4. Now playing (default)
|
||||||
//
|
//
|
||||||
// Background uses an extracted tint from the album art (fades to
|
// v2.0.1 layout: compact horizontal hero inspired by SuperIsland's
|
||||||
// near-black). Control surface, text and spacing follow the
|
// NowPlaying. Medium album art on the left, metadata + source badge
|
||||||
// MioIsland aesthetic:
|
// on the right, progress + times inline below, transport controls
|
||||||
// - #0A0A0A near-black base
|
// at bottom. Half the vertical footprint of v2.0.0 for the same
|
||||||
// - white text with opacity tiers (1.0 / 0.7 / 0.5 / 0.3)
|
// information density.
|
||||||
// - lime #CAFF00 as the single accent color
|
//
|
||||||
// - 16pt corner on the big card, 8pt on small chips
|
// Background uses a tint extracted from the album art (fades into
|
||||||
|
// a near-black base). Palette:
|
||||||
|
// #0A0A0A near-black base
|
||||||
|
// white text tiers 1.0 / 0.75 / 0.45 / 0.3
|
||||||
|
// lime #CAFF00 as the single accent color
|
||||||
|
// 16pt corner on the big art, 8pt on small chips
|
||||||
//
|
//
|
||||||
|
|
||||||
import AppKit
|
import AppKit
|
||||||
@ -45,7 +50,7 @@ struct ExpandedView: View {
|
|||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
|
|
||||||
content
|
content
|
||||||
.padding(28)
|
.padding(20)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
@ -99,50 +104,45 @@ struct ExpandedView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Playing card
|
// MARK: - Playing card — compact horizontal layout
|
||||||
|
|
||||||
private var playingCard: some View {
|
private var playingCard: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 16) {
|
||||||
// Header row: small eyebrow + source badge.
|
// Hero row: album art left, metadata + source badge right
|
||||||
HStack(alignment: .firstTextBaseline) {
|
HStack(alignment: .top, spacing: 14) {
|
||||||
Text(L10n.nowPlayingHeading.uppercased())
|
albumArt
|
||||||
.font(.system(size: 10, weight: .bold))
|
|
||||||
.tracking(2)
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
.foregroundColor(Self.ink.opacity(0.5))
|
// Source badge, flush right with the artwork top
|
||||||
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
sourceBadge
|
sourceBadge
|
||||||
}
|
}
|
||||||
.padding(.bottom, 22)
|
|
||||||
|
|
||||||
// Album art (big, centered)
|
|
||||||
albumArt
|
|
||||||
.padding(.bottom, 24)
|
|
||||||
|
|
||||||
// Title + artist + album
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Text(state.title.isEmpty ? L10n.unknownTitle : state.title)
|
Text(state.title.isEmpty ? L10n.unknownTitle : state.title)
|
||||||
.font(.system(size: 22, weight: .semibold))
|
.font(.system(size: 18, weight: .semibold))
|
||||||
.foregroundColor(Self.ink)
|
.foregroundColor(Self.ink)
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
Text(state.artist.isEmpty ? L10n.unknownArtist : state.artist)
|
Text(state.artist.isEmpty ? L10n.unknownArtist : state.artist)
|
||||||
.font(.system(size: 14, weight: .regular))
|
.font(.system(size: 13, weight: .regular))
|
||||||
.foregroundColor(Self.ink.opacity(0.75))
|
.foregroundColor(Self.ink.opacity(0.75))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
if !state.album.isEmpty {
|
if !state.album.isEmpty {
|
||||||
Text(state.album)
|
Text(state.album)
|
||||||
.font(.system(size: 12, weight: .regular))
|
.font(.system(size: 11, weight: .regular))
|
||||||
.foregroundColor(Self.ink.opacity(0.45))
|
.foregroundColor(Self.ink.opacity(0.45))
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.padding(.horizontal, 8)
|
|
||||||
.padding(.bottom, 28)
|
|
||||||
|
|
||||||
// Seek bar + time labels
|
Spacer(minLength: 0)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress + times inline on one row
|
||||||
VStack(spacing: 6) {
|
VStack(spacing: 6) {
|
||||||
SeekBar(
|
SeekBar(
|
||||||
progress: state.progress,
|
progress: state.progress,
|
||||||
@ -161,12 +161,12 @@ struct ExpandedView: View {
|
|||||||
.foregroundColor(Self.ink.opacity(0.5))
|
.foregroundColor(Self.ink.opacity(0.5))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, 24)
|
|
||||||
|
|
||||||
// Transport controls
|
// Transport controls
|
||||||
transportControls
|
transportControls
|
||||||
|
.padding(.top, 2)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: 520)
|
.frame(maxWidth: 460)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var albumArt: some View {
|
private var albumArt: some View {
|
||||||
@ -175,33 +175,33 @@ struct ExpandedView: View {
|
|||||||
Image(nsImage: art)
|
Image(nsImage: art)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fill)
|
.aspectRatio(contentMode: .fill)
|
||||||
.frame(width: 260, height: 260)
|
.frame(width: 128, height: 128)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||||
} else {
|
} else {
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||||
.fill(Color.white.opacity(0.08))
|
.fill(Color.white.opacity(0.08))
|
||||||
.frame(width: 260, height: 260)
|
.frame(width: 128, height: 128)
|
||||||
.overlay(
|
.overlay(
|
||||||
Image(systemName: "music.note")
|
Image(systemName: "music.note")
|
||||||
.font(.system(size: 64, weight: .light))
|
.font(.system(size: 34, weight: .light))
|
||||||
.foregroundColor(Self.ink.opacity(0.35))
|
.foregroundColor(Self.ink.opacity(0.35))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.shadow(color: .black.opacity(0.4), radius: 20, x: 0, y: 10)
|
.shadow(color: .black.opacity(0.35), radius: 14, x: 0, y: 6)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sourceBadge: some View {
|
private var sourceBadge: some View {
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 5) {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(state.isPlaying ? Self.lime : Self.ink.opacity(0.4))
|
.fill(state.isPlaying ? Self.lime : Self.ink.opacity(0.4))
|
||||||
.frame(width: 6, height: 6)
|
.frame(width: 5, height: 5)
|
||||||
Text(displaySourceName)
|
Text(displaySourceName)
|
||||||
.font(.system(size: 10, weight: .medium))
|
.font(.system(size: 10, weight: .medium))
|
||||||
.foregroundColor(Self.ink.opacity(0.7))
|
.foregroundColor(Self.ink.opacity(0.7))
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 10)
|
.padding(.horizontal, 8)
|
||||||
.padding(.vertical, 5)
|
.padding(.vertical, 3)
|
||||||
.background(
|
.background(
|
||||||
Capsule().fill(Color.white.opacity(0.08))
|
Capsule().fill(Color.white.opacity(0.08))
|
||||||
)
|
)
|
||||||
@ -212,25 +212,25 @@ struct ExpandedView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var transportControls: some View {
|
private var transportControls: some View {
|
||||||
HStack(spacing: 40) {
|
HStack(spacing: 28) {
|
||||||
transportButton(
|
transportButton(
|
||||||
symbol: "backward.fill",
|
symbol: "backward.fill",
|
||||||
size: 20,
|
size: 16,
|
||||||
tooltip: L10n.previousTooltip
|
tooltip: L10n.previousTooltip
|
||||||
) {
|
) {
|
||||||
state.previousTrack()
|
state.previousTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play / pause. Larger, accent button.
|
// Play / pause — accent button, slightly smaller than v2.0.0 (48 vs 56)
|
||||||
Button(action: { state.togglePlayPause() }) {
|
Button(action: { state.togglePlayPause() }) {
|
||||||
ZStack {
|
ZStack {
|
||||||
Circle()
|
Circle()
|
||||||
.fill(Self.lime)
|
.fill(Self.lime)
|
||||||
.frame(width: 56, height: 56)
|
.frame(width: 48, height: 48)
|
||||||
Image(systemName: state.isPlaying ? "pause.fill" : "play.fill")
|
Image(systemName: state.isPlaying ? "pause.fill" : "play.fill")
|
||||||
.font(.system(size: 22, weight: .bold))
|
.font(.system(size: 18, weight: .bold))
|
||||||
.foregroundColor(.black)
|
.foregroundColor(.black)
|
||||||
.offset(x: state.isPlaying ? 0 : 2) // optical nudge for play
|
.offset(x: state.isPlaying ? 0 : 2) // optical nudge for play glyph
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
@ -239,7 +239,7 @@ struct ExpandedView: View {
|
|||||||
|
|
||||||
transportButton(
|
transportButton(
|
||||||
symbol: "forward.fill",
|
symbol: "forward.fill",
|
||||||
size: 20,
|
size: 16,
|
||||||
tooltip: L10n.nextTooltip
|
tooltip: L10n.nextTooltip
|
||||||
) {
|
) {
|
||||||
state.nextTrack()
|
state.nextTrack()
|
||||||
@ -264,23 +264,23 @@ struct ExpandedView: View {
|
|||||||
// MARK: - Empty card (nothing playing)
|
// MARK: - Empty card (nothing playing)
|
||||||
|
|
||||||
private var emptyCard: some View {
|
private var emptyCard: some View {
|
||||||
VStack(spacing: 14) {
|
VStack(spacing: 12) {
|
||||||
Image(systemName: "music.note")
|
Image(systemName: "music.note")
|
||||||
.font(.system(size: 44, weight: .light))
|
.font(.system(size: 40, weight: .light))
|
||||||
.foregroundColor(Self.ink.opacity(0.3))
|
.foregroundColor(Self.ink.opacity(0.3))
|
||||||
|
|
||||||
Text(L10n.nothingPlaying)
|
Text(L10n.nothingPlaying)
|
||||||
.font(.system(size: 18, weight: .semibold))
|
.font(.system(size: 16, weight: .semibold))
|
||||||
.foregroundColor(Self.ink.opacity(0.7))
|
.foregroundColor(Self.ink.opacity(0.7))
|
||||||
|
|
||||||
Text(L10n.nothingPlayingHint)
|
Text(L10n.nothingPlayingHint)
|
||||||
.font(.system(size: 12, weight: .regular))
|
.font(.system(size: 11, weight: .regular))
|
||||||
.foregroundColor(Self.ink.opacity(0.4))
|
.foregroundColor(Self.ink.opacity(0.4))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
}
|
}
|
||||||
.padding(32)
|
.padding(24)
|
||||||
.frame(maxWidth: 360)
|
.frame(maxWidth: 320)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Warning cards (host outdated / chinese app detected)
|
// MARK: - Warning cards (host outdated / chinese app detected)
|
||||||
@ -291,31 +291,31 @@ struct ExpandedView: View {
|
|||||||
hint: String,
|
hint: String,
|
||||||
tint: Color
|
tint: Color
|
||||||
) -> some View {
|
) -> some View {
|
||||||
VStack(spacing: 14) {
|
VStack(spacing: 12) {
|
||||||
Image(systemName: symbol)
|
Image(systemName: symbol)
|
||||||
.font(.system(size: 40, weight: .regular))
|
.font(.system(size: 36, weight: .regular))
|
||||||
.foregroundColor(tint)
|
.foregroundColor(tint)
|
||||||
|
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.system(size: 16, weight: .semibold))
|
.font(.system(size: 15, weight: .semibold))
|
||||||
.foregroundColor(Self.ink.opacity(0.9))
|
.foregroundColor(Self.ink.opacity(0.9))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
|
|
||||||
Text(hint)
|
Text(hint)
|
||||||
.font(.system(size: 12, weight: .regular))
|
.font(.system(size: 11, weight: .regular))
|
||||||
.foregroundColor(Self.ink.opacity(0.55))
|
.foregroundColor(Self.ink.opacity(0.55))
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
.lineSpacing(2)
|
.lineSpacing(2)
|
||||||
}
|
}
|
||||||
.padding(28)
|
.padding(22)
|
||||||
.frame(maxWidth: 380)
|
.frame(maxWidth: 340)
|
||||||
.background(
|
.background(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||||
.fill(Color.white.opacity(0.04))
|
.fill(Color.white.opacity(0.04))
|
||||||
)
|
)
|
||||||
.overlay(
|
.overlay(
|
||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||||
.stroke(Color.white.opacity(0.08), lineWidth: 0.5)
|
.stroke(Color.white.opacity(0.08), lineWidth: 0.5)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -359,7 +359,7 @@ private struct TransportIconButton: View {
|
|||||||
Image(systemName: symbol)
|
Image(systemName: symbol)
|
||||||
.font(.system(size: size, weight: .semibold))
|
.font(.system(size: size, weight: .semibold))
|
||||||
.foregroundColor(isHovered ? Self.lime : Color.white.opacity(0.75))
|
.foregroundColor(isHovered ? Self.lime : Color.white.opacity(0.75))
|
||||||
.frame(width: 44, height: 44)
|
.frame(width: 36, height: 36)
|
||||||
.background(
|
.background(
|
||||||
Circle()
|
Circle()
|
||||||
.fill(Color.white.opacity(isHovered ? 0.10 : 0.0))
|
.fill(Color.white.opacity(isHovered ? 0.10 : 0.0))
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user