From f2816bbdc0240285c6fb69a971f1f89c661d9caa Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Fri, 10 Jul 2026 21:19:47 -0700 Subject: [PATCH 1/2] add ios about page --- ComputerSolitaire/Views/AboutView.swift | 172 ++++++++++++++------- ComputerSolitaire/Views/SettingsView.swift | 24 +++ 2 files changed, 136 insertions(+), 60 deletions(-) diff --git a/ComputerSolitaire/Views/AboutView.swift b/ComputerSolitaire/Views/AboutView.swift index 0a94246..9e9a7b0 100644 --- a/ComputerSolitaire/Views/AboutView.swift +++ b/ComputerSolitaire/Views/AboutView.swift @@ -1,83 +1,135 @@ -#if os(macOS) +import Foundation import SwiftUI -struct AboutView: View { - @Environment(\.openURL) private var openURL +enum AppInfo { + static let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "—" + static let copyrightYear = String(Calendar.current.component(.year, from: Date())) + static let githubURL = URL(string: "https://github.com/austin-smith/ComputerSolitaire")! +} - private var appVersion: String { - Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String - } +private enum AboutViewMetrics { +#if os(iOS) + static let iconSize: CGFloat = 100 + static let iconCornerRadius: CGFloat = 22 + static let contentSpacing: CGFloat = 16 + static let titleFont = Font.system(.title2, design: .monospaced, weight: .bold) + static let taglineFont = Font.subheadline.weight(.medium) + static let descriptionFont = Font.subheadline + static let versionFont = Font.subheadline.weight(.medium) + static let copyrightFont = Font.caption +#else + static let iconSize: CGFloat = 128 + static let iconCornerRadius: CGFloat = 22 + static let contentSpacing: CGFloat = 12 + static let titleFont = Font.system(size: 22, weight: .bold, design: .monospaced) + static let taglineFont = Font.system(size: 13, weight: .medium) + static let descriptionFont = Font.system(size: 11) + static let versionFont = Font.system(size: 11, weight: .medium) + static let copyrightFont = Font.system(size: 10) +#endif +} - private var copyrightYear: String { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy" - return formatter.string(from: Date()) +struct AboutView: View { + var body: some View { +#if os(iOS) + ScrollView { + aboutContent + .frame(maxWidth: .infinity) + .padding(.horizontal, 24) + .padding(.top, 32) + .padding(.bottom, 40) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .navigationTitle("About") + .navigationBarTitleDisplayMode(.inline) +#else + aboutContent + .padding(.horizontal, 28) + .padding(.vertical, 24) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(.regularMaterial) +#endif } - var body: some View { - VStack(spacing: 12) { - VStack(spacing: 12) { - Image(nsImage: NSApp.applicationIconImage) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 128, height: 128) - .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) - .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 6) + private var aboutContent: some View { + VStack(spacing: AboutViewMetrics.contentSpacing) { + Image("AppIconPreviewDefault") + .resizable() + .aspectRatio(contentMode: .fit) + .frame( + width: AboutViewMetrics.iconSize, + height: AboutViewMetrics.iconSize + ) + .clipShape( + .rect( + cornerRadius: AboutViewMetrics.iconCornerRadius, + style: .continuous + ) + ) + .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 6) + .accessibilityHidden(true) - VStack(spacing: 4) { - Text("Computer Solitaire") - .font(.system(size: 22, weight: .bold, design: .monospaced)) - .foregroundStyle(.primary) - } - } + VStack(spacing: 4) { + Text("Computer Solitaire") + .font(AboutViewMetrics.titleFont) + .foregroundStyle(.primary) - VStack(spacing: 16) { - Text("Fully native solitaire game for your computer.") - .font(.system(size: 11)) + Text(tagline) + .font(AboutViewMetrics.taglineFont) .foregroundStyle(.secondary) - .multilineTextAlignment(.center) - .lineLimit(nil) - .fixedSize(horizontal: false, vertical: true) + } + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) - VStack(spacing: 2) { - HStack(spacing: 8) { - Text("Version") - .font(.system(size: 11, weight: .medium)) - .foregroundStyle(.secondary) - Text(appVersion) - .font(.system(size: 11, weight: .medium, design: .monospaced)) - .foregroundStyle(.primary) - } + Text("Computer Solitaire is a low-frills, ad-free solitaire game for your computer. Includes Klondike, FreeCell, and other things you enjoy.") + .font(AboutViewMetrics.descriptionFont) + .foregroundStyle(.primary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) +#if os(iOS) + .padding(.horizontal, 8) +#endif - Text("© \(copyrightYear) Austin Smith") - .font(.system(size: 10)) - .foregroundStyle(.tertiary) - .padding(.top, 12) + VStack(spacing: 2) { + HStack(spacing: 8) { + Text("Version") + .foregroundStyle(.secondary) + Text(AppInfo.version) + .fontDesign(.monospaced) + .foregroundStyle(.primary) } + .font(AboutViewMetrics.versionFont) + + Text("© \(AppInfo.copyrightYear) Austin Smith") + .font(AboutViewMetrics.copyrightFont) + .foregroundStyle(.tertiary) + .padding(.top, 12) } + .accessibilityElement(children: .combine) VStack(spacing: 8) { - Button { - guard let url = URL(string: "https://github.com/austin-smith/ComputerSolitaire") else { return } - openURL(url) - } label: { - Text("GitHub") - } - .buttonStyle(.bordered) - .controlSize(.regular) - } + Divider() + .padding(.horizontal, 24) + Link("GitHub", destination: AppInfo.githubURL) + .buttonStyle(.bordered) + .controlSize(.regular) + } } - .padding(.horizontal, 28) - .padding(.vertical, 24) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 0)) + } + + private var tagline: String { + "Fully native solitaire game for your computer." } } -#Preview("About Us") { +#Preview("About") { +#if os(iOS) + NavigationStack { + AboutView() + } +#else AboutView() .frame(width: 320, height: 380) -} - #endif +} diff --git a/ComputerSolitaire/Views/SettingsView.swift b/ComputerSolitaire/Views/SettingsView.swift index dbfa8be..17a5f6d 100644 --- a/ComputerSolitaire/Views/SettingsView.swift +++ b/ComputerSolitaire/Views/SettingsView.swift @@ -88,6 +88,10 @@ struct SettingsView: View { #endif helpSection + +#if os(iOS) + aboutSection +#endif } .navigationTitle("Settings") #if os(iOS) @@ -287,6 +291,26 @@ struct SettingsView: View { } } +#if os(iOS) + private var aboutSection: some View { + Section { + NavigationLink { + AboutView() + } label: { + HStack { + Text("Computer Solitaire") + Spacer() + Text(AppInfo.version) + .fontDesign(.monospaced) + .foregroundStyle(.secondary) + } + } + } header: { + Text("About") + } + } +#endif + // MARK: - Custom controls private func variantCard(_ variant: GameVariant) -> some View { From c6dc64598932aaeb3911e808a5c4cd6aabc9fbad Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Fri, 10 Jul 2026 21:33:17 -0700 Subject: [PATCH 2/2] refine about page styling --- ComputerSolitaire/Views/AboutView.swift | 202 ++++++++++++++---------- 1 file changed, 115 insertions(+), 87 deletions(-) diff --git a/ComputerSolitaire/Views/AboutView.swift b/ComputerSolitaire/Views/AboutView.swift index 9e9a7b0..d6601df 100644 --- a/ComputerSolitaire/Views/AboutView.swift +++ b/ComputerSolitaire/Views/AboutView.swift @@ -1,135 +1,163 @@ import Foundation import SwiftUI +#if os(iOS) enum AppInfo { static let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "—" static let copyrightYear = String(Calendar.current.component(.year, from: Date())) static let githubURL = URL(string: "https://github.com/austin-smith/ComputerSolitaire")! } -private enum AboutViewMetrics { -#if os(iOS) - static let iconSize: CGFloat = 100 - static let iconCornerRadius: CGFloat = 22 - static let contentSpacing: CGFloat = 16 - static let titleFont = Font.system(.title2, design: .monospaced, weight: .bold) - static let taglineFont = Font.subheadline.weight(.medium) - static let descriptionFont = Font.subheadline - static let versionFont = Font.subheadline.weight(.medium) - static let copyrightFont = Font.caption -#else - static let iconSize: CGFloat = 128 - static let iconCornerRadius: CGFloat = 22 - static let contentSpacing: CGFloat = 12 - static let titleFont = Font.system(size: 22, weight: .bold, design: .monospaced) - static let taglineFont = Font.system(size: 13, weight: .medium) - static let descriptionFont = Font.system(size: 11) - static let versionFont = Font.system(size: 11, weight: .medium) - static let copyrightFont = Font.system(size: 10) -#endif -} - struct AboutView: View { var body: some View { -#if os(iOS) ScrollView { - aboutContent - .frame(maxWidth: .infinity) - .padding(.horizontal, 24) - .padding(.top, 32) - .padding(.bottom, 40) + VStack(spacing: 16) { + Image("AppIconPreviewDefault") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + .clipShape(.rect(cornerRadius: 22, style: .continuous)) + .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 6) + .accessibilityHidden(true) + + VStack(spacing: 4) { + Text("Computer Solitaire") + .font(.system(size: 24, weight: .bold, design: .monospaced)) + .foregroundStyle(.primary) + + Text("Solitaire game for your computer") + .font(.system(size: 15, weight: .medium)) + .foregroundStyle(.secondary) + } + + Text("Computer Solitaire is a low-frills, ad-free solitaire game for your computer. Includes Klondike, FreeCell, and other things you enjoy.") + .font(.system(size: 14)) + .foregroundStyle(.primary) + .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal, 24) + + VStack(spacing: 2) { + HStack(spacing: 8) { + Text("Version") + .font(.system(size: 14, weight: .medium)) + .foregroundStyle(.secondary) + Text(AppInfo.version) + .font(.system(size: 14, weight: .medium, design: .monospaced)) + .foregroundStyle(.primary) + } + + Text("© \(AppInfo.copyrightYear) Austin Smith") + .font(.system(size: 12)) + .foregroundStyle(.tertiary) + .padding(.top, 12) + } + .accessibilityElement(children: .combine) + + VStack(spacing: 8) { + Divider() + .padding(.horizontal, 24) + + Link("GitHub", destination: AppInfo.githubURL) + .buttonStyle(.bordered) + .controlSize(.regular) + } + } + .frame(maxWidth: .infinity) + .padding(.top, 32) + .padding(.bottom, 40) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .navigationTitle("About") .navigationBarTitleDisplayMode(.inline) + } +} + +#Preview("About") { + NavigationStack { + AboutView() + } +} #else - aboutContent - .padding(.horizontal, 28) - .padding(.vertical, 24) - .frame(maxWidth: .infinity, maxHeight: .infinity) - .background(.regularMaterial) -#endif +struct AboutView: View { + @Environment(\.openURL) private var openURL + + private var appVersion: String { + Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String } - private var aboutContent: some View { - VStack(spacing: AboutViewMetrics.contentSpacing) { - Image("AppIconPreviewDefault") - .resizable() - .aspectRatio(contentMode: .fit) - .frame( - width: AboutViewMetrics.iconSize, - height: AboutViewMetrics.iconSize - ) - .clipShape( - .rect( - cornerRadius: AboutViewMetrics.iconCornerRadius, - style: .continuous - ) - ) - .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 6) - .accessibilityHidden(true) - - VStack(spacing: 4) { - Text("Computer Solitaire") - .font(AboutViewMetrics.titleFont) - .foregroundStyle(.primary) + private var copyrightYear: String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy" + return formatter.string(from: Date()) + } - Text(tagline) - .font(AboutViewMetrics.taglineFont) - .foregroundStyle(.secondary) + var body: some View { + VStack(spacing: 12) { + VStack(spacing: 8) { + Image(nsImage: NSApp.applicationIconImage) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + .clipShape(RoundedRectangle(cornerRadius: 22, style: .continuous)) + .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 6) + + VStack(spacing: 3) { + Text("Computer Solitaire") + .font(.system(size: 22, weight: .bold, design: .monospaced)) + .foregroundStyle(.primary) + + Text("Solitaire game for your computer") + .font(.system(size: 13, weight: .medium)) + .foregroundStyle(.secondary) + } } - .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) Text("Computer Solitaire is a low-frills, ad-free solitaire game for your computer. Includes Klondike, FreeCell, and other things you enjoy.") - .font(AboutViewMetrics.descriptionFont) + .font(.system(size: 11)) .foregroundStyle(.primary) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) -#if os(iOS) - .padding(.horizontal, 8) -#endif VStack(spacing: 2) { HStack(spacing: 8) { Text("Version") + .font(.system(size: 11, weight: .medium)) .foregroundStyle(.secondary) - Text(AppInfo.version) - .fontDesign(.monospaced) + Text(appVersion) + .font(.system(size: 11, weight: .medium, design: .monospaced)) .foregroundStyle(.primary) } - .font(AboutViewMetrics.versionFont) - Text("© \(AppInfo.copyrightYear) Austin Smith") - .font(AboutViewMetrics.copyrightFont) + Text("© \(copyrightYear) Austin Smith") + .font(.system(size: 10)) .foregroundStyle(.tertiary) - .padding(.top, 12) + .padding(.top, 8) } - .accessibilityElement(children: .combine) - VStack(spacing: 8) { + VStack(spacing: 6) { Divider() .padding(.horizontal, 24) - Link("GitHub", destination: AppInfo.githubURL) - .buttonStyle(.bordered) - .controlSize(.regular) + Button { + guard let url = URL(string: "https://github.com/austin-smith/ComputerSolitaire") else { return } + openURL(url) + } label: { + Text("GitHub") + } + .buttonStyle(.bordered) + .controlSize(.regular) } } - } - - private var tagline: String { - "Fully native solitaire game for your computer." + .padding(.horizontal, 28) + .padding(.vertical, 16) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 0)) } } -#Preview("About") { -#if os(iOS) - NavigationStack { - AboutView() - } -#else +#Preview("About Us") { AboutView() .frame(width: 320, height: 380) -#endif } +#endif