11import SwiftUI
22
3- /// About preferences pane displaying app information
3+ /// About preferences pane displaying app information with vibrant animations
44struct AboutPreferencesPane : View {
55
66 // MARK: - Properties
@@ -12,47 +12,70 @@ struct AboutPreferencesPane: View {
1212 return " Version \( version) (Build \( build) ) "
1313 } ( )
1414
15+ @State private var isHovering = false
16+ @State private var rotationDegrees : Double = 0
17+
1518 // MARK: - Views
1619
1720 var body : some View {
18- ScrollView {
19- VStack ( spacing: 24 ) {
20- appInfoSection
21+ ZStack {
22+ animatedGradientBackground
23+
24+ ScrollView {
25+ VStack ( spacing: 24 ) {
26+ appInfoSection
2127
22- Spacer ( )
28+ Spacer ( )
29+ }
30+ . padding ( )
2331 }
24- . padding ( )
2532 }
2633 }
34+ }
2735
28- // MARK: - Subviews
29-
30- private var appInfoSection : some View {
31- VStack ( spacing: 12 ) {
32- // App Icon
33- if let appIcon = NSImage ( named: " AppIcon " ) {
34- Image ( nsImage: appIcon)
35- . resizable ( )
36- . frame ( width: 128 , height: 128 )
37- . cornerRadius ( 24 )
38- . shadow ( color: . black. opacity ( 0.2 ) , radius: 10 , x: 0 , y: 5 )
39- } else {
40- // Fallback icon
41- Image ( systemName: " flag.circle.fill " )
42- . resizable ( )
43- . frame ( width: 128 , height: 128 )
44- . foregroundColor ( . accentColor)
36+ // MARK: - Animated Gradient Background
37+ private extension AboutPreferencesPane {
38+
39+ var animatedGradientBackground : some View {
40+ TimelineView ( . animation( minimumInterval: 1.0 / 30.0 ) ) { timeline in
41+ let elapsed = timeline. date. timeIntervalSinceReferenceDate
42+ let angle = Angle . degrees ( elapsed. truncatingRemainder ( dividingBy: 360 ) * 8 )
43+
44+ AngularGradient (
45+ colors: [
46+ Color . purple. opacity ( 0.3 ) ,
47+ Color . blue. opacity ( 0.25 ) ,
48+ Color . teal. opacity ( 0.3 ) ,
49+ Color . pink. opacity ( 0.25 ) ,
50+ Color . purple. opacity ( 0.3 )
51+ ] ,
52+ center: . center,
53+ angle: angle
54+ )
55+ . blur ( radius: 60 )
56+ . ignoresSafeArea ( )
57+ }
58+ }
59+ }
60+
61+ // MARK: - App Info Section
62+ private extension AboutPreferencesPane {
63+
64+ var appInfoSection : some View {
65+ VStack ( spacing: 16 ) {
66+ // App Icon with orbiting flags
67+ ZStack {
68+ orbitingFlagsView
69+
70+ interactiveAppIcon
4571 }
72+ . frame ( width: 200 , height: 200 )
4673
4774 // App Name
4875 Text ( appName)
49- . font ( . system( size: 32 , weight: . bold) )
76+ . font ( . system( size: 32 , weight: . bold, design : . rounded ) )
5077 . foregroundColor ( . primary)
51-
52- // Version
53- Text ( appVersion)
54- . font ( . callout)
55- . foregroundColor ( . secondary)
78+ . frame ( maxWidth: . infinity, alignment: . center)
5679
5780 // Tagline
5881 Text ( " Beautiful keyboard layout switching for macOS " )
@@ -61,40 +84,127 @@ struct AboutPreferencesPane: View {
6184 . multilineTextAlignment ( . center)
6285 . padding ( . horizontal, 40 )
6386
87+ // Version — keycap style
88+ Text ( appVersion)
89+ . font ( . system( size: 11 , weight: . medium, design: . rounded) )
90+ . foregroundColor ( . secondary)
91+
6492 // Links
65- HStack ( spacing: 20 ) {
66- Button {
67- if let url = URL ( string: " https://bohdan-ios.github.io/languageflag-website/ " ) {
68- NSWorkspace . shared. open ( url)
69- }
70- } label: {
71- HStack ( spacing: 4 ) {
72- Image ( systemName: " link.circle " )
73- Text ( " Website " )
74- }
93+ linksSection
94+ }
95+ . padding ( . top, 20 )
96+ }
97+ }
98+
99+ // MARK: - Orbiting Flags
100+ private extension AboutPreferencesPane {
101+
102+ /// Flags that orbit around the app icon in a slow circle
103+ var orbitingFlagsView : some View {
104+ let flags = [ " 🇺🇸 " , " 🇫🇷 " , " 🇩🇪 " , " 🇯🇵 " , " 🇰🇷 " , " 🇺🇦 " , " 🇪🇸 " , " 🇧🇷 " ]
105+ let radius : CGFloat = 82
106+
107+ return TimelineView ( . animation( minimumInterval: 1.0 / 30.0 ) ) { timeline in
108+ let elapsed = timeline. date. timeIntervalSinceReferenceDate
109+ let baseAngle = elapsed. truncatingRemainder ( dividingBy: 360 ) * 12 // degrees per second
110+
111+ Canvas { context, size in
112+ let center = CGPoint ( x: size. width / 2 , y: size. height / 2 )
113+
114+ for (index, flag) in flags. enumerated ( ) {
115+ let offsetAngle = baseAngle + Double( index) * ( 360.0 / Double( flags. count) )
116+ let radians = offsetAngle * . pi / 180
117+
118+ let x = center. x + radius * cos( radians)
119+ let y = center. y + radius * sin( radians)
120+
121+ let text = Text ( flag) . font ( . system( size: 20 ) )
122+ context. opacity = 0.7
123+ context. draw (
124+ context. resolve ( text) ,
125+ at: CGPoint ( x: x, y: y) ,
126+ anchor: . center
127+ )
75128 }
76- . buttonStyle ( . link)
77-
78- Button {
79- if let url = URL ( string: " https://github.com/bohdan-ios/LanguageFlag " ) {
80- NSWorkspace . shared. open ( url)
81- }
82- } label: {
83- HStack ( spacing: 4 ) {
84- Image ( systemName: " chevron.left.forwardslash.chevron.right " )
85- Text ( " Source " )
86- }
129+ }
130+ }
131+ }
132+ }
133+
134+ // MARK: - Interactive App Icon
135+ private extension AboutPreferencesPane {
136+
137+ var interactiveAppIcon : some View {
138+ ZStack {
139+ // Glow ring behind icon
140+ Circle ( )
141+ . fill ( Color . accentColor. opacity ( isHovering ? 0.4 : 0.15 ) )
142+ . frame ( width: 140 , height: 140 )
143+ . blur ( radius: isHovering ? 20 : 12 )
144+
145+ Group {
146+ if let appIcon = NSImage ( named: " AppIcon " ) {
147+ Image ( nsImage: appIcon)
148+ . resizable ( )
149+ . frame ( width: 120 , height: 120 )
150+ . cornerRadius ( 24 )
151+ . shadow ( color: . black. opacity ( 0.25 ) , radius: 12 , x: 0 , y: 6 )
152+ } else {
153+ Image ( systemName: " flag.circle.fill " )
154+ . resizable ( )
155+ . frame ( width: 120 , height: 120 )
156+ . foregroundColor ( . accentColor)
87157 }
88- . buttonStyle ( . link)
89158 }
90- . font ( . caption)
159+ . rotation3DEffect ( . degrees( rotationDegrees) , axis: ( x: 0 , y: 1 , z: 0 ) )
160+ . scaleEffect ( isHovering ? 1.08 : 1.0 )
161+ . animation ( . spring( response: 0.4 , dampingFraction: 0.6 ) , value: isHovering)
162+ }
163+ . onHover { hovering in
164+ isHovering = hovering
165+ }
166+ . onTapGesture {
167+ withAnimation ( . spring( response: 0.6 , dampingFraction: 0.5 ) ) {
168+ rotationDegrees += 360
169+ }
91170 }
92- . padding ( . top, 20 )
93171 }
94172}
95173
96- // MARK: - Preview
174+ // MARK: - Links
175+ private extension AboutPreferencesPane {
97176
177+ var linksSection : some View {
178+ HStack ( spacing: 20 ) {
179+ Button {
180+ if let url = URL ( string: " https://bohdan-ios.github.io/languageflag-website/ " ) {
181+ NSWorkspace . shared. open ( url)
182+ }
183+ } label: {
184+ HStack ( spacing: 4 ) {
185+ Image ( systemName: " link.circle " )
186+ Text ( " Website " )
187+ }
188+ }
189+ . buttonStyle ( . link)
190+
191+ Button {
192+ if let url = URL ( string: " https://github.com/bohdan-ios/LanguageFlag " ) {
193+ NSWorkspace . shared. open ( url)
194+ }
195+ } label: {
196+ HStack ( spacing: 4 ) {
197+ Image ( systemName: " chevron.left.forwardslash.chevron.right " )
198+ Text ( " Source " )
199+ }
200+ }
201+ . buttonStyle ( . link)
202+ }
203+ . font ( . caption)
204+ }
205+ }
206+
207+ // MARK: - Preview
98208struct AboutPreferencesPane_Previews : PreviewProvider {
99209
100210 static var previews : some View {
0 commit comments