mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[CHANGE] Overhaul of the AppDetailView with version history, reviews & ratings, and app information
This commit is contained in:
committed by
Joe Mattiello
parent
f3e58e1485
commit
3d0f385af7
@@ -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 */ = {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
49
AltStore/Views/App Detail/AppVersionHistoryView.swift
Normal file
49
AltStore/Views/App Detail/AppVersionHistoryView.swift
Normal 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: )
|
||||
// }
|
||||
//}
|
||||
@@ -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 ?? "")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user