diff --git a/HabitRPG/Generated/Assets-Images.swift b/HabitRPG/Generated/Assets-Images.swift
index 4ed9b2e31..98c7908e6 100644
--- a/HabitRPG/Generated/Assets-Images.swift
+++ b/HabitRPG/Generated/Assets-Images.swift
@@ -167,6 +167,8 @@ internal enum Asset {
internal static let crown = ImageAsset(name: "crown")
internal static let downIcon = ImageAsset(name: "down_icon")
internal enum Empty {
+ internal static let backgrounds = ImageAsset(name: "empty/Backgrounds")
+ internal static let customizations = ImageAsset(name: "empty/Customizations")
internal static let eggs = ImageAsset(name: "empty/Eggs")
internal static let food = ImageAsset(name: "empty/Food")
internal static let hatchingPotions = ImageAsset(name: "empty/HatchingPotions")
diff --git a/HabitRPG/Generated/Strings.swift b/HabitRPG/Generated/Strings.swift
index 019d32820..c3e3ba273 100644
--- a/HabitRPG/Generated/Strings.swift
+++ b/HabitRPG/Generated/Strings.swift
@@ -216,6 +216,8 @@ public enum L10n {
public static var createdTaskTitle: String { return L10n.tr("Mainstrings", "createdTaskTitle") }
/// Currency
public static var currency: String { return L10n.tr("Mainstrings", "currency") }
+ /// Customization Shop
+ public static var customizationShop: String { return L10n.tr("Mainstrings", "customization_shop") }
/// Daily
public static var daily: String { return L10n.tr("Mainstrings", "daily") }
/// Want to try something new? Join a Challenge to expand your task list and win some Gems!
@@ -1590,6 +1592,14 @@ public enum L10n {
}
public enum Empty {
+ /// You don’t own any of these items
+ public static var noItems: String { return L10n.tr("Mainstrings", "empty.no_items") }
+ /// Head over to the Customization Shop to browse the many ways you can customize your avatar!
+ public static var noItemsDescription: String { return L10n.tr("Mainstrings", "empty.no_items_description") }
+ /// Looking for more?
+ public static var wantMoreItems: String { return L10n.tr("Mainstrings", "empty.want_more_items") }
+ /// Check out the Customization Shop for even more ways to customize your avatar!
+ public static var wantMoreItemsDescription: String { return L10n.tr("Mainstrings", "empty.want_more_items_description") }
public enum Dailies {
/// Dailies are tasks that repeat on a regular basis. Choose the schedule that works for you!
diff --git a/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Contents.json b/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Contents.json
new file mode 100644
index 000000000..5dbd244b1
--- /dev/null
+++ b/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "Group 476.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "Group 476@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "Group 476@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Group 476.png b/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Group 476.png
new file mode 100644
index 000000000..5e7dcae61
Binary files /dev/null and b/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Group 476.png differ
diff --git a/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Group 476@2x.png b/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Group 476@2x.png
new file mode 100644
index 000000000..83c6baf54
Binary files /dev/null and b/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Group 476@2x.png differ
diff --git a/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Group 476@3x.png b/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Group 476@3x.png
new file mode 100644
index 000000000..b9999cf4d
Binary files /dev/null and b/HabitRPG/Images.xcassets/empty/Backgrounds.imageset/Group 476@3x.png differ
diff --git a/HabitRPG/Images.xcassets/empty/Customizations.imageset/Contents.json b/HabitRPG/Images.xcassets/empty/Customizations.imageset/Contents.json
new file mode 100644
index 000000000..8a3cd50af
--- /dev/null
+++ b/HabitRPG/Images.xcassets/empty/Customizations.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "filename" : "Group 480.png",
+ "idiom" : "universal",
+ "scale" : "1x"
+ },
+ {
+ "filename" : "Group 480@2x.png",
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "filename" : "Group 480@3x.png",
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/HabitRPG/Images.xcassets/empty/Customizations.imageset/Group 480.png b/HabitRPG/Images.xcassets/empty/Customizations.imageset/Group 480.png
new file mode 100644
index 000000000..0dd5aa4cf
Binary files /dev/null and b/HabitRPG/Images.xcassets/empty/Customizations.imageset/Group 480.png differ
diff --git a/HabitRPG/Images.xcassets/empty/Customizations.imageset/Group 480@2x.png b/HabitRPG/Images.xcassets/empty/Customizations.imageset/Group 480@2x.png
new file mode 100644
index 000000000..e2f14931d
Binary files /dev/null and b/HabitRPG/Images.xcassets/empty/Customizations.imageset/Group 480@2x.png differ
diff --git a/HabitRPG/Images.xcassets/empty/Customizations.imageset/Group 480@3x.png b/HabitRPG/Images.xcassets/empty/Customizations.imageset/Group 480@3x.png
new file mode 100644
index 000000000..1729c7032
Binary files /dev/null and b/HabitRPG/Images.xcassets/empty/Customizations.imageset/Group 480@3x.png differ
diff --git a/HabitRPG/Storyboards/Base.lproj/Main.storyboard b/HabitRPG/Storyboards/Base.lproj/Main.storyboard
index 2923b1909..70c787398 100644
--- a/HabitRPG/Storyboards/Base.lproj/Main.storyboard
+++ b/HabitRPG/Storyboards/Base.lproj/Main.storyboard
@@ -831,7 +831,7 @@
-
+
@@ -902,32 +902,36 @@
-
-
+
+
-
+
+
+
+
+
-
+
-
-
-
+
+
@@ -938,10 +942,10 @@
-
-
-
-
+
+
+
+
@@ -951,13 +955,18 @@
-
-
+
+
+
+
+
+
-
-
-
+
+
+
+
@@ -969,7 +978,7 @@
-
+
diff --git a/HabitRPG/Strings/Base.lproj/Mainstrings.strings b/HabitRPG/Strings/Base.lproj/Mainstrings.strings
index 537ec7488..0a7bb62f9 100644
--- a/HabitRPG/Strings/Base.lproj/Mainstrings.strings
+++ b/HabitRPG/Strings/Base.lproj/Mainstrings.strings
@@ -1036,6 +1036,10 @@
"empty.notifications.title" = "You're all caught up!";
"empty.notifications.description" = "The notification fairies give you a raucous round of applause! Well done!";
"empty.inbox.description" = "Start chatting below! Remember to be friendly and follow the Community Guidelines.";
+"empty.no_items" = "You don’t own any of these items";
+"empty.want_more_items" = "Looking for more?";
+"empty.no_items_description" = "Head over to the Customization Shop to browse the many ways you can customize your avatar!";
+"empty.want_more_items_description" = "Check out the Customization Shop for even more ways to customize your avatar!";
"achievements.onboarding" = "Onboarding Achievements";
"achievements.basic" = "Basic Achievements";
"achievements.seasonal" = "Seasonal Achievements";
@@ -1350,3 +1354,5 @@
"summer" = "Summer";
"fall" = "Fall";
"winter" = "Winter";
+
+"customization_shop" = "Customization Shop";
diff --git a/HabitRPG/TableViewDataSources/AvatarDetailViewDataSource.swift b/HabitRPG/TableViewDataSources/AvatarDetailViewDataSource.swift
index 41fce786c..53be5e747 100644
--- a/HabitRPG/TableViewDataSources/AvatarDetailViewDataSource.swift
+++ b/HabitRPG/TableViewDataSources/AvatarDetailViewDataSource.swift
@@ -8,6 +8,7 @@
import Foundation
import Habitica_Models
+import SwiftUIX
class AvatarDetailViewDataSource: BaseReactiveCollectionViewDataSource {
@@ -211,32 +212,43 @@ class AvatarDetailViewDataSource: BaseReactiveCollectionViewDataSource CGSize {
+ return CGSize(width: collectionView.frame.width, height: 80)
+ }
+
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
+
let view = super.collectionView(collectionView, viewForSupplementaryElementOfKind: kind, at: indexPath)
let section = sections[indexPath.section]
if let footerView = view as? CustomizationFooterView {
- if !newCustomizationLayout {
+ if newCustomizationLayout && indexPath.section == collectionView.numberOfSections - 1 {
footerView.purchaseButton.isHidden = true
- }
- if let set = customizationSets[section.key ?? ""] {
- footerView.configure(customizationSet: set)
- let individualPrice = set.setItems?.filter { (customization) -> Bool in
- return !self.owns(customization: customization)
- }.map { $0.price }.reduce(0, +) ?? 0
- if individualPrice >= set.setPrice && set.setPrice != 0 && set.key?.contains("timeTravel") != true && set.key?.contains("incentive") != true {
- footerView.purchaseButton.isHidden = false
+ footerView.hostingView.isHidden = false
+ let hostView = UIHostingView(rootView: CTAFooterView(type: customizationType, hasItems: !ownedCustomizations.isEmpty))
+ footerView.hostingView.addSubview(hostView)
+ hostView.frame = CGRect(x: 0, y: 0, width: collectionView.frame.width, height: 200)
+ } else {
+ footerView.hostingView.isHidden = true
+ if let set = customizationSets[section.key ?? ""] {
+ footerView.configure(customizationSet: set)
+ let individualPrice = set.setItems?.filter { (customization) -> Bool in
+ return !self.owns(customization: customization)
+ }.map { $0.price }.reduce(0, +) ?? 0
+ if individualPrice >= set.setPrice && set.setPrice != 0 && set.key?.contains("timeTravel") != true && set.key?.contains("incentive") != true {
+ footerView.purchaseButton.isHidden = false
+ } else {
+ footerView.purchaseButton.isHidden = true
+ }
+
+ footerView.purchaseButtonTapped = {
+ if let action = self.purchaseSet {
+ action(set)
+ }
+ }
} else {
footerView.purchaseButton.isHidden = true
}
-
- footerView.purchaseButtonTapped = {
- if let action = self.purchaseSet {
- action(set)
- }
- }
- } else {
- footerView.purchaseButton.isHidden = true
}
} else if let headerView = view as? CustomizationHeaderView {
if let set = customizationSets[section.key ?? ""] {
diff --git a/HabitRPG/TableviewCells/CustomizationFooterView.swift b/HabitRPG/TableviewCells/CustomizationFooterView.swift
index 0ead987e9..fb99842b7 100644
--- a/HabitRPG/TableviewCells/CustomizationFooterView.swift
+++ b/HabitRPG/TableviewCells/CustomizationFooterView.swift
@@ -8,9 +8,11 @@
import Foundation
import Habitica_Models
+import SwiftUIX
class CustomizationFooterView: UICollectionReusableView {
+ @IBOutlet weak var hostingView: UIView!
@IBOutlet weak var currencyView: CurrencyCountView!
@IBOutlet weak var purchaseButton: UIView!
@IBOutlet weak var buyAllLabel: UILabel!
diff --git a/HabitRPG/UI/Inventory/AvatarDetailViewController.swift b/HabitRPG/UI/Inventory/AvatarDetailViewController.swift
index 746ee2587..951407cbe 100644
--- a/HabitRPG/UI/Inventory/AvatarDetailViewController.swift
+++ b/HabitRPG/UI/Inventory/AvatarDetailViewController.swift
@@ -43,12 +43,15 @@ class AvatarDetailViewController: BaseCollectionViewController, UICollectionView
}
}
HabiticaAnalytics.shared.logNavigationEvent("navigated \(customizationType ?? "") screen")
-
- let flowlayout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
- if newCustomizationLayout {
- flowlayout?.footerReferenceSize = CGSize(width: collectionView.frame.width, height: 20)
+ }
+
+ func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
+ if section == collectionView.numberOfSections - 1 && newCustomizationLayout {
+ return CGSize(width: collectionView.frame.width, height: 200)
+ } else if newCustomizationLayout {
+ return CGSize(width: collectionView.frame.width, height: 20)
}
-
+ return CGSize(width: collectionView.frame.width, height: 60)
}
override func applyTheme(theme: Theme) {
diff --git a/HabitRPG/Utilities/RouterHandler.swift b/HabitRPG/Utilities/RouterHandler.swift
index 75a493c70..5ed43f947 100644
--- a/HabitRPG/Utilities/RouterHandler.swift
+++ b/HabitRPG/Utilities/RouterHandler.swift
@@ -56,12 +56,14 @@ enum Route {
case seasonalShop
case timeTravelers
case subscription
+ case customizationShop
var url: String {
// swiftlint:disable
switch self {
case .market: return "/inventory/market"
case .questShop: return "/inventory/quests"
+ case .customizationShop: return "/inventory/customizations"
case .seasonalShop: return "/inventory/seasonal"
case .timeTravelers: return "/inventory/time"
case .subscription: return "/user/settings/subscription"
@@ -150,6 +152,12 @@ class RouterHandler {
viewController.shopIdentifier = Constants.TimeTravelersShopKey
self.push(viewController)
}
+ register(.customizationShop) {
+ self.displayTab(index: 4)
+ let viewController = StoryboardScene.Shop.shopViewController.instantiate()
+ viewController.shopIdentifier = Constants.CustomizationShopKey
+ self.push(viewController)
+ }
register("/inventory/items") {
self.displayTab(index: 4)
self.push(StoryboardScene.Main.itemsViewController.instantiate())
diff --git a/HabitRPG/Views/CTAFooterView.swift b/HabitRPG/Views/CTAFooterView.swift
new file mode 100644
index 000000000..dae75aa9e
--- /dev/null
+++ b/HabitRPG/Views/CTAFooterView.swift
@@ -0,0 +1,79 @@
+//
+// CTAFooterView.swift
+// Habitica
+//
+// Created by Phillip Thelen on 06.05.24.
+// Copyright © 2024 HabitRPG Inc. All rights reserved.
+//
+
+import SwiftUI
+
+struct CTAFooterView: View {
+ let type: String
+ let hasItems: Bool
+
+ private var image: UIImage {
+ switch type {
+ case "background":
+ return Asset.Empty.backgrounds.image
+ case "customizations":
+ return Asset.Empty.customizations.image
+ default:
+ return Asset.Empty.customizations.image
+ }
+ }
+
+ private func splitDescription() -> [String] {
+ let text = (hasItems ? L10n.Empty.wantMoreItemsDescription : L10n.Empty.noItemsDescription)
+ var textElements = [String]()
+ if let range = text.range(of: L10n.customizationShop) {
+ textElements.append(String(text.prefix(upTo: range.lowerBound)))
+ textElements.append(String(text[range]))
+ textElements.append(String(text.suffix(from: range.upperBound)))
+ } else {
+ textElements.append(text)
+ }
+ return textElements
+ }
+
+ @ViewBuilder
+ private func description() -> some View {
+ let textElements = splitDescription()
+ if textElements.count == 3 {
+ Group {
+ Text(textElements[0]) +
+ Text(textElements[1]).foregroundColor(Color(ThemeService.shared.theme.tintColor)) +
+ Text(textElements[2])
+ }
+ } else {
+ Text(textElements.first ?? "")
+ }
+ }
+
+ var body: some View {
+ VStack(spacing: 4) {
+ Image(uiImage: image).padding(.bottom, 8)
+ Text(hasItems ? L10n.Empty.wantMoreItems : L10n.Empty.noItems)
+ .fontWeight(.semibold)
+ .foregroundColor(Color(ThemeService.shared.theme.primaryTextColor))
+ description()
+ .foregroundColor(Color(ThemeService.shared.theme.secondaryTextColor))
+ .multilineTextAlignment(.center)
+ }
+ .font(.system(size: 13))
+ .ignoresSafeArea()
+ .onTapGesture {
+ RouterHandler.shared.handle(.customizationShop)
+ }
+ .frame(maxWidth: 260)
+ .padding(.top, 50)
+ }
+}
+
+#Preview {
+ Group {
+ CTAFooterView(type: "background", hasItems: true)
+ CTAFooterView(type: "background", hasItems: false)
+ CTAFooterView(type: "hair", hasItems: true)
+ }
+}
diff --git a/Habitica Models/Habitica ModelsTests/Test Models/TestFlags.swift b/Habitica Models/Habitica ModelsTests/Test Models/TestFlags.swift
index 7beb71e6f..60dd8e255 100644
--- a/Habitica Models/Habitica ModelsTests/Test Models/TestFlags.swift
+++ b/Habitica Models/Habitica ModelsTests/Test Models/TestFlags.swift
@@ -10,6 +10,8 @@ import Foundation
@testable import Habitica_Models
class TestFlags: FlagsProtocol {
+ var chatShadowMuted: Bool = false
+
var tutorials: [TutorialStepProtocol] = []
var verifiedUsername: Bool = true
diff --git a/Habitica Models/Habitica ModelsTests/Test Models/TestPreferences.swift b/Habitica Models/Habitica ModelsTests/Test Models/TestPreferences.swift
index 01385cb1b..3eb58de9f 100644
--- a/Habitica Models/Habitica ModelsTests/Test Models/TestPreferences.swift
+++ b/Habitica Models/Habitica ModelsTests/Test Models/TestPreferences.swift
@@ -10,6 +10,10 @@ import Foundation
@testable import Habitica_Models
class TestPreferences: PreferencesProtocol {
+ var dateFormat: String?
+
+ var tasks: (any Habitica_Models.TaskPreferencesProtocol)?
+
var autoEquip: Bool = true
var pushNotifications: PushNotificationsProtocol?
diff --git a/Habitica Models/Habitica ModelsTests/Test Models/TestUser.swift b/Habitica Models/Habitica ModelsTests/Test Models/TestUser.swift
index 925b2bd72..75df85806 100644
--- a/Habitica Models/Habitica ModelsTests/Test Models/TestUser.swift
+++ b/Habitica Models/Habitica ModelsTests/Test Models/TestUser.swift
@@ -10,6 +10,8 @@ import Foundation
@testable import Habitica_Models
class TestUser: UserProtocol {
+ var permissions: (any Habitica_Models.PermissionsProtocol)?
+
var backer: BackerProtocol?
var needsCron: Bool = false
diff --git a/Habitica.xcodeproj/project.pbxproj b/Habitica.xcodeproj/project.pbxproj
index 69ae99ed0..22fc71776 100644
--- a/Habitica.xcodeproj/project.pbxproj
+++ b/Habitica.xcodeproj/project.pbxproj
@@ -171,6 +171,7 @@
293282161F389DBB00D42961 /* SetupCustomizationItemView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 293282151F389DBB00D42961 /* SetupCustomizationItemView.xib */; };
293282181F39E1A400D42961 /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 293282171F39E1A400D42961 /* WelcomeViewController.swift */; };
2932821A1F39E28000D42961 /* TypingTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 293282191F39E28000D42961 /* TypingTextViewController.swift */; };
+ 29359A522BE8F8F8009790DE /* CTAFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29359A512BE8F8F8009790DE /* CTAFooterView.swift */; };
2938ED2E28B4EDC700E995A2 /* FirebaseAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 2938ED2D28B4EDC700E995A2 /* FirebaseAnalytics */; };
2938ED3028B4EDC700E995A2 /* FirebaseCrashlytics in Frameworks */ = {isa = PBXBuildFile; productRef = 2938ED2F28B4EDC700E995A2 /* FirebaseCrashlytics */; };
2938ED3228B4EDC700E995A2 /* FirebaseMessaging in Frameworks */ = {isa = PBXBuildFile; productRef = 2938ED3128B4EDC700E995A2 /* FirebaseMessaging */; };
@@ -839,6 +840,7 @@
293282171F39E1A400D42961 /* WelcomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = ""; };
293282191F39E28000D42961 /* TypingTextViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingTextViewController.swift; sourceTree = ""; };
2935440B20F5088500857CE8 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Mainstrings.strings"; sourceTree = ""; };
+ 29359A512BE8F8F8009790DE /* CTAFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CTAFooterView.swift; sourceTree = ""; };
2936525520F5058B00261740 /* bg */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = bg; path = bg.lproj/Mainstrings.strings; sourceTree = ""; };
2936526920F5060C00261740 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Mainstrings.strings; sourceTree = ""; };
2936527320F5064400261740 /* da */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = da; path = da.lproj/Mainstrings.strings; sourceTree = ""; };
@@ -2491,6 +2493,7 @@
D91CCACF1BCF11CC001C6A0C /* UserTopHeader.xib */,
29AB257B2B02799900A3C1D1 /* PixelArtView.swift */,
2945AE982B2868CA0022F3CC /* HostingPanModal.swift */,
+ 29359A512BE8F8F8009790DE /* CTAFooterView.swift */,
);
path = Views;
sourceTree = "";
@@ -3350,6 +3353,7 @@
49E3784F200912BA00E0DA3D /* ResizableTableViewCell.swift in Sources */,
29706C9827D66C77006EC139 /* MessagesViewController.swift in Sources */,
29A9038826EA33A400442B93 /* FlagInformationOverlayView.swift in Sources */,
+ 29359A522BE8F8F8009790DE /* CTAFooterView.swift in Sources */,
29FC8866201B7B5B002B3D3F /* QuestMenuHeader.swift in Sources */,
290922F02242628600645D0E /* LanguageHandler.swift in Sources */,
29A98593254841C500F04665 /* KeyboardManager.swift in Sources */,