vibetunnel/VibeTunnel/Presentation/Views/AboutView.swift
Peter Steinberger 520cf9641d Add Help menu to menu bar with website and issue links
- Add Help submenu with links to website and GitHub issues
- Include Check for Updates functionality (using Sparkle stub)
- Display app version (0.1.3) in Help menu
- Move About option from main menu to Help submenu
- Update About description to "Turn any browser into your Mac's terminal."
2025-06-16 05:23:56 +02:00

181 lines
5.4 KiB
Swift

import AppKit
import SwiftUI
/// About view displaying application information, version details, and credits.
///
/// This view provides information about VibeTunnel including version numbers,
/// build details, developer credits, and links to external resources like
/// GitHub repository and support channels.
struct AboutView: View {
var appName: String {
Bundle.main.infoDictionary?["CFBundleDisplayName"] as? String ??
Bundle.main.infoDictionary?["CFBundleName"] as? String ?? "VibeTunnel"
}
var appVersion: String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown"
return "\(version) (\(build))"
}
var body: some View {
ScrollView {
VStack(spacing: 24) {
appInfoSection
descriptionSection
linksSection
Spacer(minLength: 40)
copyrightSection
}
.frame(maxWidth: .infinity)
.standardPadding()
}
.scrollContentBackground(.hidden)
}
private var appInfoSection: some View {
VStack(spacing: 16) {
InteractiveAppIcon()
Text(appName)
.font(.largeTitle)
.fontWeight(.medium)
Text("Version \(appVersion)")
.font(.footnote)
.foregroundStyle(.secondary)
}
.padding(.top, 20)
}
private var descriptionSection: some View {
Text("Turn any browser into your Mac's terminal.")
.font(.body)
.foregroundStyle(.secondary)
}
private var linksSection: some View {
VStack(spacing: 12) {
HoverableLink(url: "https://github.com/amantus-ai/vibetunnel", title: "View on GitHub", icon: "link")
HoverableLink(
url: "https://github.com/amantus-ai/vibetunnel/issues",
title: "Report an Issue",
icon: "exclamationmark.bubble"
)
HoverableLink(url: "https://x.com/VibeTunnel", title: "Follow @VibeTunnel", icon: "bird")
}
}
private var copyrightSection: some View {
Text("© 2025 VibeTunnel Team • MIT Licensed")
.font(.footnote)
.foregroundStyle(.secondary)
.padding(.bottom, 32)
}
}
/// Hoverable link component with underline animation.
///
/// This component displays a link with an icon that shows an underline on hover
/// and changes the cursor to a pointing hand for better user experience.
struct HoverableLink: View {
let url: String
let title: String
let icon: String
@State private var isHovering = false
private var destinationURL: URL {
URL(string: url) ?? URL(fileURLWithPath: "/")
}
var body: some View {
Link(destination: destinationURL) {
Label(title, systemImage: icon)
.underline(isHovering, color: .accentColor)
}
.buttonStyle(.link)
.pointingHandCursor()
.onHover { hovering in
withAnimation(.easeInOut(duration: 0.2)) {
isHovering = hovering
}
}
}
}
/// Interactive app icon component with shadow effects and website link.
///
/// This component displays the VibeTunnel app icon with dynamic shadow effects that respond
/// to user interaction. It includes hover effects for visual feedback and opens the
/// VibeTunnel website when clicked.
struct InteractiveAppIcon: View {
@State private var isHovering = false
@State private var isPressed = false
@Environment(\.colorScheme)
private var colorScheme
var body: some View {
ZStack {
Image(nsImage: NSApp.applicationIconImage)
.resizable()
.frame(width: 128, height: 128)
.clipShape(RoundedRectangle(cornerRadius: 22))
.scaleEffect(isPressed ? 0.95 : (isHovering ? 1.05 : 1.0))
.shadow(
color: shadowColor,
radius: shadowRadius,
x: 0,
y: shadowOffset
)
.animation(.easeInOut(duration: 0.2), value: isHovering)
.animation(.easeInOut(duration: 0.1), value: isPressed)
// Invisible button overlay for click handling
Button(action: openWebsite) {
Rectangle()
.fill(Color.clear)
.frame(width: 128, height: 128)
}
.buttonStyle(PlainButtonStyle())
}
.pointingHandCursor()
.onHover { hovering in
isHovering = hovering
}
.pressEvents(
onPress: { isPressed = true },
onRelease: { isPressed = false }
)
}
private var shadowColor: Color {
if colorScheme == .dark {
.black.opacity(isHovering ? 0.6 : 0.4)
} else {
.black.opacity(isHovering ? 0.3 : 0.2)
}
}
private var shadowRadius: CGFloat {
isHovering ? 20 : 12
}
private var shadowOffset: CGFloat {
isHovering ? 8 : 4
}
private func openWebsite() {
guard let url = URL(string: "https://vibetunnel.ai") else { return }
NSWorkspace.shared.open(url)
}
}
// MARK: - Preview
#Preview("About View") {
AboutView()
.frame(width: 570, height: 600)
}