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 */,