Files
SideStore/AltStore/Views/App Detail/AppDetailView.swift
bogotesr 3be0a4a89c Add es-419 and finish adding support for the translations
Added Latin American Spanish (probably not the best translation)

Made everything reference the swiftgen stuff rather than having strings
2023-05-20 19:22:47 +02:00

209 lines
6.7 KiB
Swift

//
// AppDetailView.swift
// SideStore
//
// Created by Fabian Thies on 18.11.22.
// Copyright © 2022 Fabian Thies. All rights reserved.
//
import SwiftUI
import AsyncImage
import ExpandableText
import SFSafeSymbols
import AltStoreCore
struct AppDetailView: View {
let storeApp: StoreApp
var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .medium
dateFormatter.timeStyle = .none
return dateFormatter
}()
var byteCountFormatter: ByteCountFormatter = {
let formatter = ByteCountFormatter()
return formatter
}()
@State var scrollOffset: CGFloat = .zero
let maxContentCornerRadius: CGFloat = 24
let headerViewHeight: CGFloat = 140
let permissionColumns = 4
var headerBlurRadius: CGFloat {
min(20, max(0, 20 - (scrollOffset / -150) * 20))
}
var isHeaderViewVisible: Bool {
scrollOffset < headerViewHeight + 12
}
var contentCornerRadius: CGFloat {
max(CGFloat.zero, min(maxContentCornerRadius, maxContentCornerRadius * (1 - self.scrollOffset / self.headerViewHeight)))
}
var body: some View {
ObservableScrollView(scrollOffset: $scrollOffset) { proxy in
LazyVStack {
headerView
.frame(height: headerViewHeight)
contentView
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
AppPillButton(app: storeApp)
.disabled(isHeaderViewVisible)
.offset(y: isHeaderViewVisible ? 12 : 0)
.opacity(isHeaderViewVisible ? 0 : 1)
.animation(.easeInOut(duration: 0.2), value: isHeaderViewVisible)
}
ToolbarItemGroup(placement: .principal) {
HStack {
Spacer()
AppIconView(iconUrl: storeApp.iconURL, size: 24)
Text(storeApp.name)
.bold()
Spacer()
}
.offset(y: isHeaderViewVisible ? 12 : 0)
.opacity(isHeaderViewVisible ? 0 : 1)
.animation(.easeInOut(duration: 0.2), value: isHeaderViewVisible)
}
}
}
var headerView: some View {
ZStack(alignment: .center) {
GeometryReader { proxy in
AppIconView(iconUrl: storeApp.iconURL, size: proxy.frame(in: .global).width)
.blur(radius: headerBlurRadius)
.offset(y: min(0, scrollOffset))
}
.padding()
AppRowView(app: storeApp)
.padding(.horizontal)
}
}
var contentView: some View {
VStack(alignment: .leading, spacing: 32) {
if storeApp.sourceIdentifier == Source.altStoreIdentifier {
officialAppBadge
}
if let subtitle = storeApp.subtitle {
Text(subtitle)
.multilineTextAlignment(.center)
.frame(maxWidth: .infinity)
.padding(.horizontal)
}
if !storeApp.screenshotURLs.isEmpty {
// Equatable: Only reload the view if the screenshots change.
// This prevents unnecessary redraws on scroll.
AppScreenshotsScrollView(urls: storeApp.screenshotURLs)
.equatable()
}
ExpandableText(text: storeApp.localizedDescription)
.lineLimit(6)
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
.padding(.horizontal)
currentVersionView
.padding(.horizontal)
permissionsView
.padding(.horizontal)
// TODO: Add review view
// Only let users rate the app if it is currently installed!
}
.padding(.vertical)
.background(
RoundedRectangle(cornerRadius: contentCornerRadius)
.foregroundColor(Color(UIColor.systemBackground))
.shadow(radius: isHeaderViewVisible ? 12 : 0)
)
}
var officialAppBadge: some View {
HStack {
Spacer()
Image(systemSymbol: .checkmarkSealFill)
Text("Official App")
Spacer()
}
.foregroundColor(.accentColor)
}
var trustedAppBadge: some View {
HStack {
Spacer()
Image(systemSymbol: .shieldLefthalfFill)
Text("From Trusted Source")
Spacer()
}
.foregroundColor(.accentColor)
}
var currentVersionView: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(alignment: .bottom) {
VStack(alignment: .leading) {
Text(L10n.AppDetailView.whatsNew)
.bold()
.font(.title3)
Text("\(L10n.AppDetailView.version) \(storeApp.version)")
.font(.callout)
.foregroundColor(.secondary)
}
Spacer()
VStack(alignment: .trailing) {
Text(dateFormatter.string(from: storeApp.versionDate))
Text(byteCountFormatter.string(fromByteCount: Int64(storeApp.size)))
}
.font(.callout)
.foregroundColor(.secondary)
}
if let versionDescription = storeApp.versionDescription {
ExpandableText(text: versionDescription)
.lineLimit(5)
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
} else {
Text(L10n.AppDetailView.noVersionInformation)
.foregroundColor(.secondary)
}
}
}
var permissionsView: some View {
VStack(alignment: .leading, spacing: 8) {
Text(L10n.AppDetailView.permissions)
.bold()
.font(.title3)
if storeApp.permissions.isEmpty {
Text(L10n.AppDetailView.noPermissions)
.font(.callout)
.foregroundColor(.secondary)
} else {
AppPermissionsGrid(permissions: storeApp.permissions)
}
Spacer()
}
}
}