diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index ea44164a..3b2166e3 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -27,12 +27,12 @@ 1F07F56B2955F11500F7BE95 /* AppScreenshotsPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F07F56A2955F11500F7BE95 /* AppScreenshotsPreview.swift */; }; 1F07F56F2955FB2000F7BE95 /* AppIDsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F07F56E2955FB2000F7BE95 /* AppIDsView.swift */; }; 1F0DD81C2932D2FF007608A4 /* AppScreenshotsScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD81B2932D2FF007608A4 /* AppScreenshotsScrollView.swift */; }; - 1F0DD81F2932D84C007608A4 /* ExpandableText in Frameworks */ = {isa = PBXBuildFile; productRef = 1F0DD81E2932D84C007608A4 /* ExpandableText */; }; 1F0DD8212933B749007608A4 /* AppPermissionsGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8202933B749007608A4 /* AppPermissionsGrid.swift */; }; 1F0DD83F29367F6C007608A4 /* ConnectAppleIDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD83E29367F6C007608A4 /* ConnectAppleIDView.swift */; }; 1F0DD84129368056007608A4 /* EnvironmentValues.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD84029368056007608A4 /* EnvironmentValues.swift */; }; 1F0DD8432936B0F9007608A4 /* RoundedTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8422936B0F9007608A4 /* RoundedTextField.swift */; }; 1F0DD8452936B3FE007608A4 /* FilledButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F0DD8442936B3FE007608A4 /* FilledButtonStyle.swift */; }; + 1F1295812989B51F0048FCB9 /* ExpandableText in Frameworks */ = {isa = PBXBuildFile; productRef = 1F1295802989B51F0048FCB9 /* ExpandableText */; }; 1F2EF787297C4D40002FD839 /* LicensesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F2EF786297C4D40002FD839 /* LicensesView.swift */; }; 1F44634529744E570070E514 /* HintView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F44634429744E570070E514 /* HintView.swift */; }; 1F5DF9D82974426300DDAA47 /* AppScreenshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F5DF9D72974426300DDAA47 /* AppScreenshot.swift */; }; @@ -70,6 +70,7 @@ 1FB96FCF292BBBCA007E68D1 /* SiriShortcutSetupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */; }; 1FB96FEC292C171D007E68D1 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FEB292C171D007E68D1 /* NotificationManager.swift */; }; 1FB96FF3292D0539007E68D1 /* PillButtonProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */; }; + 1FFEF104298552DB0098374C /* AppVersionHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */; }; 4879A95F2861046500FC1BBD /* AltSign in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A95E2861046500FC1BBD /* AltSign */; }; 4879A9622861049C00FC1BBD /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = 4879A9612861049C00FC1BBD /* OpenSSL */; }; 99C4EF4D2979132100CB538D /* SemanticVersion in Frameworks */ = {isa = PBXBuildFile; productRef = 99C4EF4C2979132100CB538D /* SemanticVersion */; }; @@ -612,6 +613,7 @@ 1FB96FCE292BBBC9007E68D1 /* SiriShortcutSetupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiriShortcutSetupView.swift; sourceTree = ""; }; 1FB96FEB292C171D007E68D1 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = ""; }; 1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonProgressViewStyle.swift; sourceTree = ""; }; + 1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionHistoryView.swift; sourceTree = ""; }; B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Roxas.xcodeproj; path = Dependencies/Roxas/Roxas.xcodeproj; sourceTree = ""; }; B33FFBA9295F8F78002259E6 /* preboard.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = preboard.c; path = src/preboard.c; sourceTree = ""; }; B33FFBAB295F8F98002259E6 /* companion_proxy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = companion_proxy.c; path = src/companion_proxy.c; sourceTree = ""; }; @@ -1020,8 +1022,8 @@ 191E6087290C7B50001A3B7C /* libminimuxer.a in Frameworks */, 191E5FB4290A5DA0001A3B7C /* libminimuxer.a in Frameworks */, 19104DBC2909C4E500C49C7B /* libEmotionalDamage.a in Frameworks */, + 1F1295812989B51F0048FCB9 /* ExpandableText in Frameworks */, 19104D952909BAEA00C49C7B /* libimobiledevice.a in Frameworks */, - 1F0DD81F2932D84C007608A4 /* ExpandableText in Frameworks */, B3146ED2284F581E00BBC3FD /* Roxas.framework in Frameworks */, D533E8B72727841800A9B5DD /* libAppleArchive.tbd in Frameworks */, B3C395F9284F362400DA9E2F /* AppCenterCrashes in Frameworks */, @@ -1157,6 +1159,7 @@ 1F0DD81B2932D2FF007608A4 /* AppScreenshotsScrollView.swift */, 1F0DD8202933B749007608A4 /* AppPermissionsGrid.swift */, 1F07F56A2955F11500F7BE95 /* AppScreenshotsPreview.swift */, + 1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */, ); path = "App Detail"; sourceTree = ""; @@ -2275,9 +2278,9 @@ B3C395F6284F362400DA9E2F /* AppCenterAnalytics */, B3C395F8284F362400DA9E2F /* AppCenterCrashes */, 4879A9612861049C00FC1BBD /* OpenSSL */, - 1F0DD81E2932D84C007608A4 /* ExpandableText */, 1F74FF1D295263510047C051 /* AsyncImage */, 1F07F5662955D16A00F7BE95 /* SFSafeSymbols */, + 1F1295802989B51F0048FCB9 /* ExpandableText */, ); productName = AltStore; productReference = BFD2476A2284B9A500981D42 /* SideStore.app */; @@ -2350,10 +2353,9 @@ 4879A95D2861046500FC1BBD /* XCRemoteSwiftPackageReference "AltSign" */, 4879A9602861049C00FC1BBD /* XCRemoteSwiftPackageReference "OpenSSL" */, 99C4EF472978D52400CB538D /* XCRemoteSwiftPackageReference "SemanticVersion" */, - 1FB96FB629297C11007E68D1 /* XCRemoteSwiftPackageReference "GridStack" */, - 1F0DD81D2932D84C007608A4 /* XCRemoteSwiftPackageReference "ExpandableText" */, 1F74FF1C295263510047C051 /* XCRemoteSwiftPackageReference "AsyncImage" */, 1F07F5652955D16A00F7BE95 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, + 1F12957F2989B51F0048FCB9 /* XCRemoteSwiftPackageReference "ExpandableText" */, ); productRefGroup = BFD2476B2284B9A500981D42 /* Products */; projectDirPath = ""; @@ -2756,6 +2758,7 @@ 1F943C712927F90400ABE095 /* MyAppsView.swift in Sources */, 1FA1C8CA294906890083119D /* MyAppsViewModel.swift in Sources */, BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */, + 1FFEF104298552DB0098374C /* AppVersionHistoryView.swift in Sources */, BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */, BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */, BFCCB51A245E3401001853EA /* VerifyAppOperation.swift in Sources */, @@ -3696,9 +3699,9 @@ minimumVersion = 4.0.0; }; }; - 1F0DD81D2932D84C007608A4 /* XCRemoteSwiftPackageReference "ExpandableText" */ = { + 1F12957F2989B51F0048FCB9 /* XCRemoteSwiftPackageReference "ExpandableText" */ = { isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/NuPlay/ExpandableText"; + repositoryURL = "https://github.com/fabianthdev/ExpandableText"; requirement = { branch = main; kind = branch; @@ -3807,9 +3810,9 @@ package = 1F07F5652955D16A00F7BE95 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; productName = SFSafeSymbols; }; - 1F0DD81E2932D84C007608A4 /* ExpandableText */ = { + 1F1295802989B51F0048FCB9 /* ExpandableText */ = { isa = XCSwiftPackageProductDependency; - package = 1F0DD81D2932D84C007608A4 /* XCRemoteSwiftPackageReference "ExpandableText" */; + package = 1F12957F2989B51F0048FCB9 /* XCRemoteSwiftPackageReference "ExpandableText" */; productName = ExpandableText; }; 1F74FF1D295263510047C051 /* AsyncImage */ = { diff --git a/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index c43ae304..183f1543 100644 --- a/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AltStore.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -30,10 +30,10 @@ { "identity" : "expandabletext", "kind" : "remoteSourceControl", - "location" : "https://github.com/NuPlay/ExpandableText", + "location" : "https://github.com/fabianthdev/ExpandableText", "state" : { "branch" : "main", - "revision" : "d140b404c6683bb169bb01ef4eeecdb6d9be8fb8" + "revision" : "a375f5b8c73f0af69aa7add890378fdf404a29bc" } }, { diff --git a/AltStore/Generated/Localizations.swift b/AltStore/Generated/Localizations.swift index 51fadbf8..dfd97399 100644 --- a/AltStore/Generated/Localizations.swift +++ b/AltStore/Generated/Localizations.swift @@ -55,16 +55,24 @@ internal enum L10n { internal static let restoreBackup = L10n.tr("Localizable", "AppAction.restoreBackup", fallback: "Restore backup") } internal enum AppDetailView { + /// + internal static let information = L10n.tr("Localizable", "AppDetailView.information", fallback: "") /// More... internal static let more = L10n.tr("Localizable", "AppDetailView.more", fallback: "More...") /// The app requires no permissions. internal static let noPermissions = L10n.tr("Localizable", "AppDetailView.noPermissions", fallback: "The app requires no permissions.") + /// No screenshots available for this app. + internal static let noScreenshots = L10n.tr("Localizable", "AppDetailView.noScreenshots", fallback: "No screenshots available for this app.") /// No version information internal static let noVersionInformation = L10n.tr("Localizable", "AppDetailView.noVersionInformation", fallback: "No version information") /// Permissions internal static let permissions = L10n.tr("Localizable", "AppDetailView.permissions", fallback: "Permissions") - /// Version - internal static let version = L10n.tr("Localizable", "AppDetailView.version", fallback: "Version") + /// Ratings & Reviews + internal static let reviews = L10n.tr("Localizable", "AppDetailView.reviews", fallback: "Ratings & Reviews") + /// Version %@ + internal static func version(_ p1: Any) -> String { + return L10n.tr("Localizable", "AppDetailView.version", String(describing: p1), fallback: "Version %@") + } /// What's New internal static let whatsNew = L10n.tr("Localizable", "AppDetailView.whatsNew", fallback: "What's New") internal enum Badge { @@ -73,6 +81,46 @@ internal enum L10n { /// From Trusted Source internal static let trusted = L10n.tr("Localizable", "AppDetailView.Badge.trusted", fallback: "From Trusted Source") } + internal enum Information { + /// Compatibility + internal static let compatibility = L10n.tr("Localizable", "AppDetailView.Information.compatibility", fallback: "Compatibility") + /// Requires iOS %@ or higher + internal static func compatibilityAtLeast(_ p1: Any) -> String { + return L10n.tr("Localizable", "AppDetailView.Information.compatibilityAtLeast", String(describing: p1), fallback: "Requires iOS %@ or higher") + } + /// Requires iOS %@ or lower + internal static func compatibilityOrLower(_ p1: Any) -> String { + return L10n.tr("Localizable", "AppDetailView.Information.compatibilityOrLower", String(describing: p1), fallback: "Requires iOS %@ or lower") + } + /// Unknown + internal static let compatibilityUnknown = L10n.tr("Localizable", "AppDetailView.Information.compatibilityUnknown", fallback: "Unknown") + /// Developer + internal static let developer = L10n.tr("Localizable", "AppDetailView.Information.developer", fallback: "Developer") + /// Latest Version + internal static let latestVersion = L10n.tr("Localizable", "AppDetailView.Information.latestVersion", fallback: "Latest Version") + /// Size + internal static let size = L10n.tr("Localizable", "AppDetailView.Information.size", fallback: "Size") + /// Source + internal static let source = L10n.tr("Localizable", "AppDetailView.Information.source", fallback: "Source") + } + internal enum Reviews { + /// out of %d + internal static func outOf(_ p1: Int) -> String { + return L10n.tr("Localizable", "AppDetailView.Reviews.outOf", p1, fallback: "out of %d") + } + /// %d Ratings + internal static func ratings(_ p1: Int) -> String { + return L10n.tr("Localizable", "AppDetailView.Reviews.ratings", p1, fallback: "%d Ratings") + } + /// See All + internal static let seeAll = L10n.tr("Localizable", "AppDetailView.Reviews.seeAll", fallback: "See All") + } + internal enum WhatsNew { + /// Show project on GitHub + internal static let showOnGithub = L10n.tr("Localizable", "AppDetailView.WhatsNew.showOnGithub", fallback: "Show project on GitHub") + /// Version History + internal static let versionHistory = L10n.tr("Localizable", "AppDetailView.WhatsNew.versionHistory", fallback: "Version History") + } } internal enum AppIDsView { /// Each app and app extension installed with SideStore must register an App ID with Apple. diff --git a/AltStore/Helper/DateFormatterHelper.swift b/AltStore/Helper/DateFormatterHelper.swift index 106229be..8478bea9 100644 --- a/AltStore/Helper/DateFormatterHelper.swift +++ b/AltStore/Helper/DateFormatterHelper.swift @@ -16,6 +16,20 @@ struct DateFormatterHelper { dateComponentsFormatter.collapsesLargestUnit = false return dateComponentsFormatter }() + + private static let relativeDateFormatter: RelativeDateTimeFormatter = { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .abbreviated + return formatter + }() + + private static let mediumDateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .medium + dateFormatter.timeStyle = .none + return dateFormatter + }() + static func string(forExpirationDate date: Date) -> String { @@ -32,10 +46,11 @@ struct DateFormatterHelper { self.appExpirationDateFormatter.unitsStyle = .full self.appExpirationDateFormatter.allowedUnits = [.day] } - - return self.appExpirationDateFormatter.string(from: startDate, to: date) ?? "" - } return self.appExpirationDateFormatter.string(from: startDate, to: date) ?? "" } + + static func string(forRelativeDate date: Date, to referenceDate: Date = Date()) -> String { + self.relativeDateFormatter.localizedString(for: date, relativeTo: referenceDate) + } } diff --git a/AltStore/Resources/en.lproj/Localizable.strings b/AltStore/Resources/en.lproj/Localizable.strings index 8cf23227..c7e35fe3 100644 --- a/AltStore/Resources/en.lproj/Localizable.strings +++ b/AltStore/Resources/en.lproj/Localizable.strings @@ -119,12 +119,28 @@ /* AppDetailView*/ "AppDetailView.Badge.official" = "Official App"; "AppDetailView.Badge.trusted" = "From Trusted Source"; +"AppDetailView.noScreenshots" = "No screenshots available for this app."; "AppDetailView.more" = "More..."; "AppDetailView.whatsNew" = "What's New"; -"AppDetailView.version" = "Version"; +"AppDetailView.WhatsNew.versionHistory" = "Version History"; +"AppDetailView.WhatsNew.showOnGithub" = "Show project on GitHub"; +"AppDetailView.reviews" = "Ratings & Reviews"; +"AppDetailView.Reviews.outOf" = "out of %d"; +"AppDetailView.Reviews.ratings" = "%d Ratings"; +"AppDetailView.Reviews.seeAll" = "See All"; +"AppDetailView.version" = "Version %@"; "AppDetailView.noVersionInformation" = "No version information"; "AppDetailView.noPermissions" = "The app requires no permissions."; "AppDetailView.permissions" = "Permissions"; +"AppDetailView.information" = "Information"; +"AppDetailView.Information.source" = "Source"; +"AppDetailView.Information.developer" = "Developer"; +"AppDetailView.Information.size" = "Size"; +"AppDetailView.Information.latestVersion" = "Latest Version"; +"AppDetailView.Information.compatibility" = "Compatibility"; +"AppDetailView.Information.compatibilityUnknown" = "Unknown"; +"AppDetailView.Information.compatibilityAtLeast" = "Requires iOS %@ or higher"; +"AppDetailView.Information.compatibilityOrLower" = "Requires iOS %@ or lower"; /* AppPermissionGrid */ "AppPermissionGrid.usageDescription" = "Usage Description"; diff --git a/AltStore/Views/App Detail/AppDetailView.swift b/AltStore/Views/App Detail/AppDetailView.swift index 37afb9c7..89e47374 100644 --- a/AltStore/Views/App Detail/AppDetailView.swift +++ b/AltStore/Views/App Detail/AppDetailView.swift @@ -16,14 +16,7 @@ struct AppDetailView: View { let storeApp: StoreApp - var dateFormatter: DateFormatter = { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = .medium - dateFormatter.timeStyle = .none - return dateFormatter - }() - - var byteCountFormatter: ByteCountFormatter = { + let byteCountFormatter: ByteCountFormatter = { let formatter = ByteCountFormatter() return formatter }() @@ -93,38 +86,81 @@ struct AppDetailView: View { } var contentView: some View { - VStack(alignment: .leading, spacing: 32) { - if storeApp.sourceIdentifier == Source.altStoreIdentifier { - officialAppBadge - } - - if let subtitle = storeApp.subtitle { - Text(subtitle) - .multilineTextAlignment(.center) + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 32) { + if storeApp.sourceIdentifier == Source.altStoreIdentifier { + officialAppBadge + } + + if let subtitle = storeApp.subtitle { + VStack { + if #available(iOS 15.0, *) { + Image(systemSymbol: .quoteOpening) + .foregroundColor(.secondary.opacity(0.5)) + .imageScale(.large) + .transformEffect(CGAffineTransform(a: 1, b: 0, c: -0.3, d: 1, tx: 0, ty: 0)) + .frame(maxWidth: .infinity, alignment: .leading) + .offset(x: 30) + } + + Text(subtitle) + .bold() + .italic() + .multilineTextAlignment(.center) + .frame(maxWidth: .infinity) + + if #available(iOS 15.0, *) { + Image(systemSymbol: .quoteClosing) + .foregroundColor(.secondary.opacity(0.5)) + .imageScale(.large) + .transformEffect(CGAffineTransform(a: 1, b: 0, c: -0.3, d: 1, tx: 0, ty: 0)) + .frame(maxWidth: .infinity, alignment: .trailing) + .offset(x: -30) + } + } + .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() + } else { + VStack() { + Text(L10n.AppDetailView.noScreenshots) + .italic() + .foregroundColor(.secondary) + } .frame(maxWidth: .infinity) .padding(.horizontal) + } + + ExpandableText(text: storeApp.localizedDescription) + .lineLimit(6) + .expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor)) + .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() + + + VStack(spacing: 16) { + Divider() + + currentVersionView + + Divider() + + ratingsView + + Divider() + + permissionsView + + Divider() + + informationView } - - 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(.horizontal) } .padding(.vertical) .background( @@ -156,25 +192,26 @@ struct AppDetailView: View { var currentVersionView: some View { VStack(alignment: .leading, spacing: 8) { - HStack(alignment: .bottom) { - VStack(alignment: .leading) { + VStack { + HStack(alignment: .firstTextBaseline) { Text(L10n.AppDetailView.whatsNew) .bold() .font(.title3) - - if let version = storeApp.latestVersion?.version { - Text("\(L10n.AppDetailView.version) \(version)") - .font(.callout) - .foregroundColor(.secondary) + + Spacer() + + NavigationLink { + AppVersionHistoryView(storeApp: self.storeApp) + } label: { + Text(L10n.AppDetailView.WhatsNew.versionHistory) } } - - Spacer() - if let versionDate = storeApp.versionDate, let versionSize = storeApp.size { - VStack(alignment: .trailing) { - Text(dateFormatter.string(from: versionDate)) - Text(byteCountFormatter.string(fromByteCount: Int64(versionSize))) + if let latestVersion = storeApp.latestVersion { + HStack { + Text(L10n.AppDetailView.version(latestVersion.version)) + Spacer() + Text(DateFormatterHelper.string(forRelativeDate: latestVersion.date)) } .font(.callout) .foregroundColor(.secondary) @@ -189,6 +226,109 @@ struct AppDetailView: View { Text(L10n.AppDetailView.noVersionInformation) .foregroundColor(.secondary) } + + + if true { + SwiftUI.Button { + UIApplication.shared.open(URL(string: "https://github.com/SideStore/SideStore")!) { _ in } + } label: { + HStack { + Text(L10n.AppDetailView.WhatsNew.showOnGithub) + Image(systemSymbol: .arrowUpForwardSquare) + } + } + } + } + } + + var ratingsView: some View { + VStack(alignment: .leading, spacing: 8) { + HStack(alignment: .firstTextBaseline) { + Text(L10n.AppDetailView.whatsNew) + .bold() + .font(.title3) + + Spacer() + + NavigationLink { + AppVersionHistoryView(storeApp: self.storeApp) + } label: { + Text(L10n.AppDetailView.Reviews.seeAll) + } + } + + HStack(spacing: 40) { + VStack { + Text("3.0") + .font(.system(size: 48, weight: .bold, design: .rounded)) + .opacity(0.8) + Text(L10n.AppDetailView.Reviews.outOf(5)) + .bold() + .font(.callout) + .foregroundColor(.secondary) + } + + VStack(alignment: .trailing) { + LazyVGrid(columns: [GridItem(.fixed(48), alignment: .trailing), GridItem(.flexible())], spacing: 2) { + ForEach(Array(1...5).reversed(), id: \.self) { rating in + HStack(spacing: 2) { + ForEach(0..