Reorganized the MonsterDetail view to get around the 10 items per group limit.
Adds layout for resistances, immunities, and languages.
This commit is contained in:
		
							
								
								
									
										40
									
								
								iOS/MonsterCards/Helpers/StringHelper.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								iOS/MonsterCards/Helpers/StringHelper.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| // | ||||
| //  StringHelper.swift | ||||
| //  MonsterCards | ||||
| // | ||||
| //  Created by Tom Hicks on 3/21/21. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| class StringHelper { | ||||
|     static func oxfordJoin( | ||||
|         _ strings: [String], | ||||
|         _ separator: String, | ||||
|         _ lastSeparator: String, | ||||
|         _ onlySeparator: String | ||||
|     ) -> String { | ||||
|         let numStrings = strings.count | ||||
|         if (numStrings < 1) { | ||||
|             return ""; | ||||
|         } else if (numStrings == 2) { | ||||
|             return strings[0] + onlySeparator + strings[1] | ||||
|         } else { | ||||
|             var joined = "" | ||||
|             var index = 0 | ||||
|             let lastIndex = numStrings - 1 | ||||
|              | ||||
|             strings.forEach { | ||||
|                 if index > 0 && index < lastIndex { | ||||
|                     joined.append(separator) | ||||
|                 } else if (index > 0 && index >= lastIndex) { | ||||
|                     joined.append(lastSeparator) | ||||
|                 } | ||||
|                 joined.append($0) | ||||
|                 index = index + 1 | ||||
|             } | ||||
|              | ||||
|             return joined | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										50
									
								
								iOS/MonsterCards/Models/Enums/ArmorType.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								iOS/MonsterCards/Models/Enums/ArmorType.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| // | ||||
| //  ArmorType.swift | ||||
| //  MonsterCards | ||||
| // | ||||
| //  Created by Tom Hicks on 3/21/21. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| enum ArmorType: String, CaseIterable, Identifiable { | ||||
|     case none = "none" | ||||
|     case naturalArmor = "natural armor" | ||||
|     case mageArmor = "mage armor" | ||||
|     case padded = "padded" | ||||
|     case leather = "leather" | ||||
|     case studdedLeather = "studded" | ||||
|     case hide = "hide" | ||||
|     case chainShirt = "chain shirt" | ||||
|     case scaleMail = "scale mail" | ||||
|     case breastplate = "breastplate" | ||||
|     case halfPlate = "half plate" | ||||
|     case ringMail = "ring mail" | ||||
|     case chainMail = "chain mail" | ||||
|     case splintMail = "splint" | ||||
|     case plateMail = "plate" | ||||
|     case other = "other" | ||||
|      | ||||
|     var id: ArmorType { self } | ||||
|      | ||||
|     var displayName: String { | ||||
|         switch self { | ||||
|             case .none: return "None" | ||||
|             case .naturalArmor: return "Natural Armor" | ||||
|             case .mageArmor: return "Mage Armor" | ||||
|             case .padded: return "Padded" | ||||
|             case .leather: return "Leather" | ||||
|             case .studdedLeather: return "Studded Leather" | ||||
|             case .hide: return "Hide" | ||||
|             case .chainShirt: return "Chain Shirt" | ||||
|             case .scaleMail: return "Scale Mail" | ||||
|             case .breastplate: return "Breastplate" | ||||
|             case .halfPlate: return "Half Plate" | ||||
|             case .ringMail: return "Ring Mail" | ||||
|             case .chainMail: return "Chain Mail" | ||||
|             case .splintMail: return "Splint Mail" | ||||
|             case .plateMail: return "Plate Mail" | ||||
|             case .other: return "Other" | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										53
									
								
								iOS/MonsterCards/Models/Enums/ChallengeRating.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								iOS/MonsterCards/Models/Enums/ChallengeRating.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| // | ||||
| //  ChallengeRating.swift | ||||
| //  MonsterCards | ||||
| // | ||||
| //  Created by Tom Hicks on 3/21/21. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| enum ChallengeRating: String, CaseIterable, Identifiable { | ||||
|     case zero = "0" | ||||
|     case oneEighth = "1/8" | ||||
|     case oneQuarter = "1/4" | ||||
|     case oneHalf = "1/2" | ||||
|     case one = "1" | ||||
|     case two = "2" | ||||
|     case three = "3" | ||||
|     case four = "4" | ||||
|     case five = "5" | ||||
|     case six = "6" | ||||
|     case seven = "7" | ||||
|     case eight = "8" | ||||
|     case nine = "9" | ||||
|     case ten = "10" | ||||
|     case eleven = "11" | ||||
|     case twelve = "12" | ||||
|     case thirteen = "13" | ||||
|     case fourteen = "14" | ||||
|     case fifteen = "15" | ||||
|     case sixteen = "16" | ||||
|     case seventeen = "17" | ||||
|     case eighteen = "18" | ||||
|     case nineteen = "19" | ||||
|     case twenty = "20" | ||||
|     case twentyOne = "21" | ||||
|     case twentyTwo = "22" | ||||
|     case twentyThree = "23" | ||||
|     case twentyFour = "24" | ||||
|     case twentyFive = "25" | ||||
|     case twentySix = "26" | ||||
|     case twentySeven = "27" | ||||
|     case twentyEight = "28" | ||||
|     case twentyNine = "29" | ||||
|     case thirty = "30" | ||||
|     case custom = "*" | ||||
|          | ||||
|     var id: ChallengeRating { self } | ||||
|      | ||||
|     // Probably don't need this | ||||
|     var displayName: String { | ||||
|         return rawValue | ||||
|     } | ||||
| } | ||||
							
								
								
									
										30
									
								
								iOS/MonsterCards/Models/Enums/SizeType.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								iOS/MonsterCards/Models/Enums/SizeType.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| // | ||||
| //  SizeType.swift | ||||
| //  MonsterCards | ||||
| // | ||||
| //  Created by Tom Hicks on 3/21/21. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
|  | ||||
| enum SizeType: String, CaseIterable, Identifiable { | ||||
|     case tiny = "tiny" | ||||
|     case small = "small" | ||||
|     case medium = "medium" | ||||
|     case large = "large" | ||||
|     case huge = "huge" | ||||
|     case gargantuan = "gargantuan" | ||||
|      | ||||
|     var id: SizeType { self } | ||||
|      | ||||
|     var displayName: String { | ||||
|         switch self { | ||||
|             case .tiny: return "Tiny" | ||||
|             case .small: return "Small" | ||||
|             case .medium: return "Medium" | ||||
|             case .large: return "Large" | ||||
|             case .huge: return "Huge" | ||||
|         case .gargantuan: return "gargantuan" | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -625,115 +625,84 @@ public class Monster: NSManagedObject { | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: OTHER | ||||
|      | ||||
|     var damageVulnerabilitiesArray: [String] { | ||||
|         get { | ||||
|             return ["Fire", "Poison", "Psychic"] | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var damageVulnerabilitiesDescription: String { | ||||
|         get { | ||||
|             let sortedVulnerabilities = self.damageVulnerabilitiesArray.sorted() | ||||
|              | ||||
|             return StringHelper.oxfordJoin(sortedVulnerabilities, ", ", ", and ", " and ") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var damageResistancesArray: [String] { | ||||
|         get { | ||||
|             return ["Ice", "Electric"] | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var damageResistancesDescription: String { | ||||
|         get { | ||||
|             let sortedResistances = self.damageResistancesArray.sorted() | ||||
|             return StringHelper.oxfordJoin(sortedResistances, ", ", ", and ", " and ") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var damageImmunitiesArray: [String] { | ||||
|         get { | ||||
|             return ["Slashing"] | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var damageImmunitiesDescription: String { | ||||
|         get { | ||||
|             let sortedImmunities = self.damageImmunitiesArray.sorted() | ||||
|             return StringHelper.oxfordJoin(sortedImmunities, ", ", ", and ", " and ") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var conditionImmunitiesArray: [String] { | ||||
|         get { | ||||
|             return [] | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var conditionImmunitiesDescription: String { | ||||
|         get { | ||||
|             let sortedImmunities = self.conditionImmunitiesArray.sorted() | ||||
|             return StringHelper.oxfordJoin(sortedImmunities, ", ", ", and ", " and ") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var languagesArray: [String] { | ||||
|         get { | ||||
|             return ["Common", "Goblin"] | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var languagesDescription: String { | ||||
|         get { | ||||
|             let sortedLanguages = self.languagesArray.sorted() | ||||
|             return StringHelper.oxfordJoin(sortedLanguages, ", ", ", and ", " and ") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var challengeRatingDescription: String { | ||||
|         get { | ||||
|             return ""; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // MARK: End | ||||
|  | ||||
| } | ||||
|  | ||||
| enum ArmorType: String, CaseIterable, Identifiable { | ||||
|     case none = "none" | ||||
|     case naturalArmor = "natural armor" | ||||
|     case mageArmor = "mage armor" | ||||
|     case padded = "padded" | ||||
|     case leather = "leather" | ||||
|     case studdedLeather = "studded" | ||||
|     case hide = "hide" | ||||
|     case chainShirt = "chain shirt" | ||||
|     case scaleMail = "scale mail" | ||||
|     case breastplate = "breastplate" | ||||
|     case halfPlate = "half plate" | ||||
|     case ringMail = "ring mail" | ||||
|     case chainMail = "chain mail" | ||||
|     case splintMail = "splint" | ||||
|     case plateMail = "plate" | ||||
|     case other = "other" | ||||
|      | ||||
|     var id: ArmorType { self } | ||||
|      | ||||
|     var displayName: String { | ||||
|         switch self { | ||||
|             case .none: return "None" | ||||
|             case .naturalArmor: return "Natural Armor" | ||||
|             case .mageArmor: return "Mage Armor" | ||||
|             case .padded: return "Padded" | ||||
|             case .leather: return "Leather" | ||||
|             case .studdedLeather: return "Studded Leather" | ||||
|             case .hide: return "Hide" | ||||
|             case .chainShirt: return "Chain Shirt" | ||||
|             case .scaleMail: return "Scale Mail" | ||||
|             case .breastplate: return "Breastplate" | ||||
|             case .halfPlate: return "Half Plate" | ||||
|             case .ringMail: return "Ring Mail" | ||||
|             case .chainMail: return "Chain Mail" | ||||
|             case .splintMail: return "Splint Mail" | ||||
|             case .plateMail: return "Plate Mail" | ||||
|             case .other: return "Other" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| enum SizeType: String, CaseIterable, Identifiable { | ||||
|     case tiny = "tiny" | ||||
|     case small = "small" | ||||
|     case medium = "medium" | ||||
|     case large = "large" | ||||
|     case huge = "huge" | ||||
|     case gargantuan = "gargantuan" | ||||
|      | ||||
|     var id: SizeType { self } | ||||
|      | ||||
|     var displayName: String { | ||||
|         switch self { | ||||
|             case .tiny: return "Tiny" | ||||
|             case .small: return "Small" | ||||
|             case .medium: return "Medium" | ||||
|             case .large: return "Large" | ||||
|             case .huge: return "Huge" | ||||
|         case .gargantuan: return "gargantuan" | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| enum ChallengeRating: String, CaseIterable, Identifiable { | ||||
|     case zero = "0" | ||||
|     case oneEighth = "1/8" | ||||
|     case oneQuarter = "1/4" | ||||
|     case oneHalf = "1/2" | ||||
|     case one = "1" | ||||
|     case two = "2" | ||||
|     case three = "3" | ||||
|     case four = "4" | ||||
|     case five = "5" | ||||
|     case six = "6" | ||||
|     case seven = "7" | ||||
|     case eight = "8" | ||||
|     case nine = "9" | ||||
|     case ten = "10" | ||||
|     case eleven = "11" | ||||
|     case twelve = "12" | ||||
|     case thirteen = "13" | ||||
|     case fourteen = "14" | ||||
|     case fifteen = "15" | ||||
|     case sixteen = "16" | ||||
|     case seventeen = "17" | ||||
|     case eighteen = "18" | ||||
|     case nineteen = "19" | ||||
|     case twenty = "20" | ||||
|     case twentyOne = "21" | ||||
|     case twentyTwo = "22" | ||||
|     case twentyThree = "23" | ||||
|     case twentyFour = "24" | ||||
|     case twentyFive = "25" | ||||
|     case twentySix = "26" | ||||
|     case twentySeven = "27" | ||||
|     case twentyEight = "28" | ||||
|     case twentyNine = "29" | ||||
|     case thirty = "30" | ||||
|     case custom = "*" | ||||
|          | ||||
|     var id: ChallengeRating { self } | ||||
|      | ||||
|     // Probably don't need this | ||||
|     var displayName: String { | ||||
|         return rawValue | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -70,6 +70,127 @@ struct SmallAbilityScore: View { | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct BasicInfoView: View { | ||||
|     @ObservedObject var monster: Monster | ||||
|      | ||||
|     var body: some View { | ||||
|         let monsterMeta = monster.meta | ||||
|         let monsterArmorClassDescription = monster.armorClassDescription | ||||
|         let monsterHitPoints = monster.hitPoints | ||||
|         let monsterSpeed = monster.speed | ||||
|          | ||||
|         // meta: "(large humanoid (elf) lawful evil" | ||||
|         if (!monsterMeta.isEmpty) { | ||||
|             Text(monsterMeta) | ||||
|                 .font(.subheadline) | ||||
|                 .foregroundColor(.secondary) | ||||
|         } | ||||
|          | ||||
|         SectionDivider() | ||||
|  | ||||
|         // AC | ||||
|         if (!monsterArmorClassDescription.isEmpty) { | ||||
|             LabeledField("Armor Class") { | ||||
|                 Text(monsterArmorClassDescription)// armor class | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // HP | ||||
|         if (!monsterHitPoints.isEmpty) { | ||||
|             LabeledField("Hit Points") { | ||||
|                 Text(monsterHitPoints) // hit points | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Speed | ||||
|         if (!monsterSpeed.isEmpty) { | ||||
|             LabeledField("Speed") { | ||||
|                 Text(monsterSpeed) // speed | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct AbilityScoresView: View { | ||||
|     @ObservedObject var monster: Monster | ||||
|      | ||||
|     var body: some View { | ||||
|         SectionDivider() | ||||
|          | ||||
|         // Ability Scores | ||||
|         HStack { | ||||
|             SmallAbilityScore("STR", monster.strengthScore, monster.strengthModifier) | ||||
|             SmallAbilityScore("DEX", monster.dexterityScore, monster.dexterityModifier) | ||||
|             SmallAbilityScore("CON", monster.constitutionScore, monster.constitutionModifier) | ||||
|             SmallAbilityScore("INT", monster.intelligenceScore, monster.intelligenceModifier) | ||||
|             SmallAbilityScore("WIS", monster.wisdomScore, monster.wisdomModifier) | ||||
|             SmallAbilityScore("CHA", monster.charismaScore, monster.charismaModifier) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ResistancesAndImmunitiesView: View { | ||||
|     @ObservedObject var monster: Monster | ||||
|      | ||||
|     var body: some View { | ||||
|         let monsterDamageVulnerabilitiesDescription = monster.damageVulnerabilitiesDescription | ||||
|         let monsterDamageResistancesDescription = monster.damageResistancesDescription | ||||
|         let monsterDamageImmunitiesDescription = monster.damageImmunitiesDescription | ||||
|         let monsterConditionImmunitiesDescription = monster.conditionImmunitiesDescription | ||||
|          | ||||
|         // Damage Vulnerabilities | ||||
|         if (!monsterDamageVulnerabilitiesDescription.isEmpty) { | ||||
|             LabeledField("Damage Vulnerabilities") { | ||||
|                 Text(monsterDamageVulnerabilitiesDescription) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Damage Resistances | ||||
|         if (!monsterDamageResistancesDescription.isEmpty) { | ||||
|             LabeledField("Damage Resistances") { | ||||
|                 Text(monsterDamageResistancesDescription) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Damage Immunities | ||||
|         if (!monsterDamageImmunitiesDescription.isEmpty) { | ||||
|             LabeledField("Damage Immunities") { | ||||
|                 Text(monsterDamageImmunitiesDescription) | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         // Condition Immunities | ||||
|         if (!monsterConditionImmunitiesDescription.isEmpty) { | ||||
|             LabeledField("Condition Immunities") { | ||||
|                 Text(monsterConditionImmunitiesDescription) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct SavingThrowsAndSkillsView: View { | ||||
|     @ObservedObject var monster: Monster | ||||
|      | ||||
|     var body: some View { | ||||
|         let savingThrowsDescription = monster.savingThrowsDescription | ||||
|         let skillsDescription = monster.skillsDescription | ||||
|          | ||||
|         // Saving Throws | ||||
|         if (!savingThrowsDescription.isEmpty) { | ||||
|             LabeledField("Saving Throws") { | ||||
|                 Text(savingThrowsDescription) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Skills | ||||
|         if (!skillsDescription.isEmpty) { | ||||
|             LabeledField("Skills") { | ||||
|                 Text(skillsDescription) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct MonsterDetail: View { | ||||
|     let kTextColor: Color = Color(hex: 0x982818) | ||||
|      | ||||
| @@ -78,72 +199,45 @@ struct MonsterDetail: View { | ||||
|     var body: some View { | ||||
|         ScrollView { | ||||
|             VStack (alignment: .leading) { | ||||
|                 let monsterMeta = monster.meta | ||||
|                 let monsterArmorClassDescription = monster.armorClassDescription | ||||
|                 let monsterHitPoints = monster.hitPoints | ||||
|                 let monsterSpeed = monster.speed | ||||
|                 let monsterLanguagesDescription = monster.languagesDescription | ||||
|                 let monsterChallengeRatingDescription = monster.challengeRatingDescription | ||||
|                  | ||||
|                 if (!monsterMeta.isEmpty) { | ||||
|                     // meta: "(large humanoid (elf) lawful evil" | ||||
|                     Text(monsterMeta) | ||||
|                         .font(.subheadline) | ||||
|                         .foregroundColor(.secondary) | ||||
|                 } | ||||
|                 BasicInfoView(monster: monster) | ||||
|                  | ||||
|                 // TODO: Find a way to hide unnecessarry dividiers. | ||||
|                 // if sections 0, 1, 2, and 3 are present there should be a divider between each of them | ||||
|                 // if section 1 is not present there should be one and only one divider between sections 0 and 2 as well as the one between 2 and 3 | ||||
|                 // if sections 1 and 2 are not present there should be a single divider between sections 0 and 3 | ||||
|                 SectionDivider() | ||||
|                  | ||||
|  | ||||
|                 if (!monsterArmorClassDescription.isEmpty) { | ||||
|                     // AC | ||||
|                     LabeledField("Armor Class") { | ||||
|                         Text(monsterArmorClassDescription)// armor class | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 if (!monsterHitPoints.isEmpty) { | ||||
|                     // HP | ||||
|                     LabeledField("Hit Points") { | ||||
|                         Text(monsterHitPoints) // hit points | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 // Speed | ||||
|                 if (!monsterSpeed.isEmpty) { | ||||
|                     LabeledField("Speed") { | ||||
|                         Text(monsterSpeed) // speed | ||||
|                     } | ||||
|                 } | ||||
|                 AbilityScoresView(monster: monster) | ||||
|  | ||||
|                 SectionDivider() | ||||
|                  | ||||
|                 // Ability Scores | ||||
|                 HStack { | ||||
|                     SmallAbilityScore("STR", monster.strengthScore, monster.strengthModifier) | ||||
|                     SmallAbilityScore("DEX", monster.dexterityScore, monster.dexterityModifier) | ||||
|                     SmallAbilityScore("CON", monster.constitutionScore, monster.constitutionModifier) | ||||
|                     SmallAbilityScore("INT", monster.intelligenceScore, monster.intelligenceModifier) | ||||
|                     SmallAbilityScore("WIS", monster.wisdomScore, monster.wisdomModifier) | ||||
|                     SmallAbilityScore("CHA", monster.charismaScore, monster.charismaModifier) | ||||
|                 } | ||||
|  | ||||
|                 SectionDivider() | ||||
|                 SavingThrowsAndSkillsView(monster: monster) | ||||
|                  | ||||
|                 let savingThrowsDescription = monster.savingThrowsDescription | ||||
|                 if (!savingThrowsDescription.isEmpty) { | ||||
|                     LabeledField("Saving Throws") { | ||||
|                         Text(savingThrowsDescription) | ||||
|                 ResistancesAndImmunitiesView(monster: monster) | ||||
|                  | ||||
|                 // Languages | ||||
|                 if (!monsterLanguagesDescription.isEmpty) { | ||||
|                     LabeledField("Languages") { | ||||
|                         Text(monsterLanguagesDescription) | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 let skillsDescription = monster.skillsDescription | ||||
|                 if (!skillsDescription.isEmpty) { | ||||
|                     LabeledField("Skills") { | ||||
|                         Text(skillsDescription) | ||||
|                 // Challenge Rating | ||||
|                 if (!monsterChallengeRatingDescription.isEmpty) { | ||||
|                     LabeledField("Challenge Rating") { | ||||
|                         Text(monsterChallengeRatingDescription) | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 // Abilities | ||||
|                  | ||||
|                 // Actions | ||||
|                  | ||||
|                 // Legendary Actions | ||||
|                  | ||||
|             } | ||||
|             .padding(.horizontal) | ||||
|             .foregroundColor(kTextColor) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user