[CHANGE] Overhaul of the AppDetailView with version history, reviews & ratings, and app information

This commit is contained in:
Fabian Thies
2023-01-31 22:38:42 +01:00
committed by Joe Mattiello
parent f3e58e1485
commit 3d0f385af7
10 changed files with 395 additions and 69 deletions

View File

@@ -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 = "<group>"; };
1FB96FEB292C171D007E68D1 /* NotificationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationManager.swift; sourceTree = "<group>"; };
1FB96FF2292D0539007E68D1 /* PillButtonProgressViewStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PillButtonProgressViewStyle.swift; sourceTree = "<group>"; };
1FFEF103298552DB0098374C /* AppVersionHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionHistoryView.swift; sourceTree = "<group>"; };
B3146EC6284F580500BBC3FD /* Roxas.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Roxas.xcodeproj; path = Dependencies/Roxas/Roxas.xcodeproj; sourceTree = "<group>"; };
B33FFBA9295F8F78002259E6 /* preboard.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = preboard.c; path = src/preboard.c; sourceTree = "<group>"; };
B33FFBAB295F8F98002259E6 /* companion_proxy.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = companion_proxy.c; path = src/companion_proxy.c; sourceTree = "<group>"; };
@@ -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 = "<group>";
@@ -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 */ = {

View File

@@ -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"
}
},
{

View File

@@ -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.

View File

@@ -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)
}
}

View File

@@ -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";

View File

@@ -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..<rating) { _ in
Image(systemSymbol: .starFill)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(height: 8)
}
}
ProgressView(value: 0.5)
.frame(maxWidth: .infinity)
.progressViewStyle(LinearProgressViewStyle(tint: .secondary))
}
}
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
Text(L10n.AppDetailView.Reviews.ratings(5))
.font(.callout)
.foregroundColor(.secondary)
}
}
TabView {
ForEach(0..<5) { i in
HintView(backgroundColor: Color(UIColor.secondarySystemBackground)) {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 4) {
HStack {
Text("Review \(i + 1)")
.bold()
.lineLimit(1)
Spacer()
Text(DateFormatterHelper.string(forRelativeDate: Date().addingTimeInterval(-60*60)))
.foregroundColor(.secondary)
}
RatingStars(rating: i + 1)
.frame(height: 12)
.foregroundColor(.yellow)
}
ExpandableText(text: "Long review text content here.\nMultiple lines.\nAt least three are shown.\nBut are there more?")
.lineLimit(3)
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
}
.frame(maxWidth: .infinity)
}
.tag(i)
.padding(.horizontal, 16)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(height: 150)
.padding(.horizontal, -16)
}
}
@@ -209,4 +349,53 @@ struct AppDetailView: View {
Spacer()
}
}
var informationData: [(title: String, content: String)] {
var data: [(title: String, content: String)] = [
(L10n.AppDetailView.Information.source, self.storeApp.source?.name ?? ""),
(L10n.AppDetailView.Information.developer, self.storeApp.developerName),
// ("Category", self.storeApp.category),
]
if let latestVersion = self.storeApp.latestVersion {
data += [
(L10n.AppDetailView.Information.size, self.byteCountFormatter.string(fromByteCount: latestVersion.size)),
(L10n.AppDetailView.Information.latestVersion, self.storeApp.latestVersion?.version ?? ""),
]
var compatibility: String = L10n.AppDetailView.Information.compatibilityUnknown
let iOSVersion = ProcessInfo.processInfo.operatingSystemVersion
if let minOSVersion = latestVersion.minOSVersion, ProcessInfo.processInfo.isOperatingSystemAtLeast(minOSVersion) == false {
compatibility = L10n.AppDetailView.Information.compatibilityAtLeast(minOSVersion.stringValue)
}
if let maxOSVersion = latestVersion.maxOSVersion,
(!ProcessInfo.processInfo.isOperatingSystemAtLeast(maxOSVersion) || maxOSVersion.stringValue.compare(iOSVersion.stringValue, options: .numeric) == .orderedSame) {
compatibility = L10n.AppDetailView.Information.compatibilityOrLower(maxOSVersion.stringValue)
}
data.append((L10n.AppDetailView.Information.compatibility, compatibility))
}
return data
}
var informationView: some View {
VStack(alignment: .leading) {
Text(L10n.AppDetailView.information)
.bold()
.font(.title3)
LazyVGrid(columns: [GridItem(.flexible(), alignment: .leading), GridItem(.flexible(), alignment: .trailing)], spacing: 8) {
ForEach(informationData, id: \.title) { title, content in
Text(title)
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
Text(content)
.multilineTextAlignment(.trailing)
}
}
}
}
}

View File

@@ -43,6 +43,8 @@ struct AppScreenshotsPreview: View {
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.navigationTitle("\(index + 1) of \(self.urls.count)")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
SwiftUI.Button {

View File

@@ -0,0 +1,49 @@
//
// AppVersionHistoryView.swift
// SideStore
//
// Created by Fabian Thies on 28.01.23.
// Copyright © 2023 SideStore. All rights reserved.
//
import SwiftUI
import AltStoreCore
import ExpandableText
struct AppVersionHistoryView: View {
let storeApp: StoreApp
var body: some View {
List {
ForEach(storeApp.versions.sorted(by: { $0.date < $1.date }), id: \.version) { version in
VStack(spacing: 8) {
HStack {
Text(version.version).bold()
Spacer()
Text(DateFormatterHelper.string(forRelativeDate: version.date))
.foregroundColor(.secondary)
}
if let versionDescription = version.localizedDescription {
ExpandableText(text: versionDescription)
.lineLimit(3)
.expandButton(TextSet(text: L10n.AppDetailView.more, font: .callout, color: .accentColor))
.buttonStyle(.plain)
} else {
Text("No version desciption available")
.italic()
.foregroundColor(.secondary)
}
}
}
}
.listStyle(PlainListStyle())
.navigationTitle("Version History")
}
}
//struct AppVersionHistoryView_Previews: PreviewProvider {
// static var previews: some View {
// AppVersionHistoryView(storeApp: )
// }
//}

View File

@@ -50,13 +50,15 @@ struct NewsItemView: View {
.bold()
.foregroundColor(.white)
VStack(alignment: .leading) {
HStack(spacing: 0) {
if let sourceName = newsItem.source?.name {
Text(sourceName)
.italic()
}
if let externalURL = newsItem.externalURL {
Text(" • ")
HStack(spacing: 0) {
Image(systemSymbol: .link)
Text(externalURL.host ?? "")

View File

@@ -61,8 +61,10 @@ struct RootView: View {
}
}
.padding()
.background(Color(UIColor.altPrimary))
.foregroundColor(.white)
.background(Color.accentColor)
.clipShape(RoundedRectangle(cornerRadius: 16))
.shadow(radius: 15)
}
Spacer()
@@ -83,7 +85,7 @@ extension RootView {
switch self {
case .news: return .newspaper
case .browse: return .booksVertical
case .myApps: return .appBadge
case .myApps: return .squareStack
case .settings: return .gearshape
}
}