AppTileView.swift 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. import SwiftUI
  2. import AppKit
  3. struct AppTileView: View {
  4. let app: LauncherApp
  5. let onTap: () -> Void
  6. @State private var isHovering = false
  7. @State private var isPressing = false
  8. var body: some View {
  9. Button(action: onTap) {
  10. VStack(spacing: 11) {
  11. AppIconView(app: app, size: 64)
  12. .shadow(
  13. color: .black.opacity(isHovering ? 0.34 : 0.2),
  14. radius: isHovering ? 14 : 8,
  15. x: 0,
  16. y: isHovering ? 10 : 5
  17. )
  18. .scaleEffect(isPressing ? 0.96 : 1.0)
  19. .offset(y: isHovering ? -1 : 0)
  20. Text(app.name)
  21. .font(.system(size: 13, weight: .medium))
  22. .foregroundStyle(.white.opacity(isHovering ? 1.0 : 0.95))
  23. .lineLimit(1)
  24. .truncationMode(.tail)
  25. .frame(maxWidth: .infinity)
  26. .offset(y: isHovering ? -0.5 : 0)
  27. }
  28. .padding(.horizontal, 7)
  29. .padding(.vertical, 10)
  30. .background(
  31. RoundedRectangle(cornerRadius: 14, style: .continuous)
  32. .fill(
  33. LinearGradient(
  34. colors: isHovering
  35. ? [Color.white.opacity(0.14), Color.white.opacity(0.06)]
  36. : [Color.clear, Color.clear],
  37. startPoint: .topLeading,
  38. endPoint: .bottomTrailing
  39. )
  40. )
  41. )
  42. .overlay(
  43. RoundedRectangle(cornerRadius: 14, style: .continuous)
  44. .stroke(
  45. isHovering ? Color.white.opacity(0.24) : Color.clear,
  46. lineWidth: isHovering ? 1.2 : 1
  47. )
  48. )
  49. .scaleEffect(isPressing ? 0.98 : (isHovering ? 1.025 : 1.0))
  50. .offset(y: isPressing ? 0 : (isHovering ? -2 : 0))
  51. .shadow(
  52. color: .black.opacity(isHovering ? 0.22 : 0),
  53. radius: isHovering ? 16 : 0,
  54. x: 0,
  55. y: isHovering ? 10 : 0
  56. )
  57. .contentShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
  58. .animation(.spring(response: 0.24, dampingFraction: 0.88), value: isHovering)
  59. .animation(.easeOut(duration: 0.12), value: isPressing)
  60. }
  61. .buttonStyle(.plain)
  62. .simultaneousGesture(
  63. DragGesture(minimumDistance: 0)
  64. .onChanged { _ in
  65. isPressing = true
  66. }
  67. .onEnded { _ in
  68. isPressing = false
  69. }
  70. )
  71. .onHover { hovering in
  72. isHovering = hovering
  73. if !hovering {
  74. isPressing = false
  75. }
  76. }
  77. }
  78. }
  79. private struct AppIconView: View {
  80. let app: LauncherApp
  81. let size: CGFloat
  82. var body: some View {
  83. ZStack {
  84. if app.isCreateNew {
  85. RoundedRectangle(cornerRadius: 18, style: .continuous)
  86. .stroke(Color.white.opacity(0.35), lineWidth: 2)
  87. .background(
  88. RoundedRectangle(cornerRadius: 18, style: .continuous)
  89. .fill(Color.white.opacity(0.03))
  90. )
  91. Image(systemName: "plus")
  92. .font(.system(size: size * 0.34, weight: .regular))
  93. .foregroundStyle(.white.opacity(0.9))
  94. } else {
  95. RoundedRectangle(cornerRadius: 18, style: .continuous)
  96. .fill(LinearGradient(colors: gradientColors, startPoint: .topLeading, endPoint: .bottomTrailing))
  97. RoundedRectangle(cornerRadius: 18, style: .continuous)
  98. .stroke(Color.white.opacity(0.08), lineWidth: 1)
  99. if let iconImage = NSImage(named: app.assetIconName) {
  100. Image(nsImage: iconImage)
  101. .resizable()
  102. .scaledToFit()
  103. .padding(size * 0.12)
  104. } else {
  105. Image(systemName: app.fallbackSymbolName)
  106. .font(.system(size: size * 0.32, weight: .semibold))
  107. .foregroundStyle(.white)
  108. }
  109. }
  110. }
  111. .frame(width: size, height: size)
  112. }
  113. private var gradientColors: [Color] {
  114. switch app.name {
  115. case let name where name.contains("Gmail"):
  116. return [Color.red.opacity(0.85), Color.orange.opacity(0.8)]
  117. case let name where name.contains("Docs"):
  118. return [Color.blue.opacity(0.9), Color.indigo.opacity(0.85)]
  119. case let name where name.contains("Drive"):
  120. return [Color.green.opacity(0.9), Color.blue.opacity(0.75)]
  121. case let name where name.contains("YouTube"):
  122. return [Color.red.opacity(0.95), Color.pink.opacity(0.75)]
  123. case let name where name.contains("Maps"):
  124. return [Color.green.opacity(0.85), Color.teal.opacity(0.75)]
  125. default:
  126. return [Color.blue.opacity(0.85), Color.cyan.opacity(0.7)]
  127. }
  128. }
  129. }
  130. struct AppDetailView: View {
  131. let app: LauncherApp
  132. @Environment(\.dismiss) private var dismiss
  133. var body: some View {
  134. ZStack {
  135. Color(red: 0.09, green: 0.09, blue: 0.11)
  136. .ignoresSafeArea()
  137. VStack(spacing: 14) {
  138. AppIconView(app: app, size: 76)
  139. Text(app.name)
  140. .font(.system(size: 22, weight: .semibold))
  141. .foregroundStyle(.white)
  142. Text(app.description)
  143. .font(.system(size: 14))
  144. .foregroundStyle(.white.opacity(0.82))
  145. Button("Close") {
  146. dismiss()
  147. }
  148. .buttonStyle(.borderedProminent)
  149. }
  150. .padding(24)
  151. }
  152. }
  153. }