Fixes the xcode build and adds stubs for missing classes.
This commit is contained in:
@@ -3,11 +3,13 @@
|
|||||||
archiveVersion = 1;
|
archiveVersion = 1;
|
||||||
classes = {
|
classes = {
|
||||||
};
|
};
|
||||||
objectVersion = 58;
|
objectVersion = 54;
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
E20209D325D8DD9600EFE733 /* Skill+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */; };
|
E20209D325D8DD9600EFE733 /* Skill+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */; };
|
||||||
|
E20209D425D8DD9600EFE734 /* Collection+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209D425D8DD9600EFE736 /* Collection+CoreDataClass.swift */; };
|
||||||
|
E20209D525D8DD9600EFE735 /* CollectionMonster+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209D525D8DD9600EFE737 /* CollectionMonster+CoreDataClass.swift */; };
|
||||||
E20209E825D8DEC100EFE733 /* AbilityScore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209E725D8DEC100EFE733 /* AbilityScore.swift */; };
|
E20209E825D8DEC100EFE733 /* AbilityScore.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209E725D8DEC100EFE733 /* AbilityScore.swift */; };
|
||||||
E20209F425D8E04300EFE733 /* ProficiencyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209F225D8E04300EFE733 /* ProficiencyType.swift */; };
|
E20209F425D8E04300EFE733 /* ProficiencyType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209F225D8E04300EFE733 /* ProficiencyType.swift */; };
|
||||||
E20209F525D8E04300EFE733 /* AdvantageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209F325D8E04300EFE733 /* AdvantageType.swift */; };
|
E20209F525D8E04300EFE733 /* AdvantageType.swift in Sources */ = {isa = PBXBuildFile; fileRef = E20209F325D8E04300EFE733 /* AdvantageType.swift */; };
|
||||||
@@ -100,6 +102,11 @@
|
|||||||
E2CB0DE1260887ED00142591 /* StringViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DE0260887ED00142591 /* StringViewModel.swift */; };
|
E2CB0DE1260887ED00142591 /* StringViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DE0260887ED00142591 /* StringViewModel.swift */; };
|
||||||
E2CB0DE626088CE400142591 /* EditStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DE526088CE400142591 /* EditStrings.swift */; };
|
E2CB0DE626088CE400142591 /* EditStrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2CB0DE526088CE400142591 /* EditStrings.swift */; };
|
||||||
E2D473FD25B532C900CB36D7 /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D473FC25B532C900CB36D7 /* Color+Hex.swift */; };
|
E2D473FD25B532C900CB36D7 /* Color+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D473FC25B532C900CB36D7 /* Color+Hex.swift */; };
|
||||||
|
E2FE0021261FE0000055B23B /* EntityImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0001261FE0000083EAC5 /* EntityImporter.swift */; };
|
||||||
|
E2FE0022261FE0000055B23B /* BinderImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0002261FE0000083EAC5 /* BinderImporter.swift */; };
|
||||||
|
E2FE0023261FE0000055B23B /* BinderExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0003261FE0000083EAC5 /* BinderExporter.swift */; };
|
||||||
|
E2FE0024261FE0000055B23B /* BinderImportModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0004261FE0000083EAC5 /* BinderImportModels.swift */; };
|
||||||
|
E2FE0025261FE0000055B23B /* TetraCubeMonsterImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2FE0005261FE0000083EAC5 /* TetraCubeMonsterImporter.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -142,6 +149,8 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Skill+CoreDataClass.swift"; sourceTree = "<group>"; };
|
E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Skill+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
|
E20209D425D8DD9600EFE736 /* Collection+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Collection+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
|
E20209D525D8DD9600EFE737 /* CollectionMonster+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CollectionMonster+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
E20209E725D8DEC100EFE733 /* AbilityScore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbilityScore.swift; sourceTree = "<group>"; };
|
E20209E725D8DEC100EFE733 /* AbilityScore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbilityScore.swift; sourceTree = "<group>"; };
|
||||||
E20209F225D8E04300EFE733 /* ProficiencyType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProficiencyType.swift; sourceTree = "<group>"; };
|
E20209F225D8E04300EFE733 /* ProficiencyType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProficiencyType.swift; sourceTree = "<group>"; };
|
||||||
E20209F325D8E04300EFE733 /* AdvantageType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvantageType.swift; sourceTree = "<group>"; };
|
E20209F325D8E04300EFE733 /* AdvantageType.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvantageType.swift; sourceTree = "<group>"; };
|
||||||
@@ -217,6 +226,11 @@
|
|||||||
E2CB0DE0260887ED00142591 /* StringViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringViewModel.swift; sourceTree = "<group>"; };
|
E2CB0DE0260887ED00142591 /* StringViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringViewModel.swift; sourceTree = "<group>"; };
|
||||||
E2CB0DE526088CE400142591 /* EditStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditStrings.swift; sourceTree = "<group>"; };
|
E2CB0DE526088CE400142591 /* EditStrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditStrings.swift; sourceTree = "<group>"; };
|
||||||
E2D473FC25B532C900CB36D7 /* Color+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Hex.swift"; sourceTree = "<group>"; };
|
E2D473FC25B532C900CB36D7 /* Color+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Hex.swift"; sourceTree = "<group>"; };
|
||||||
|
E2FE0001261FE0000083EAC5 /* EntityImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EntityImporter.swift; sourceTree = "<group>"; };
|
||||||
|
E2FE0002261FE0000083EAC5 /* BinderImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinderImporter.swift; sourceTree = "<group>"; };
|
||||||
|
E2FE0003261FE0000083EAC5 /* BinderExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinderExporter.swift; sourceTree = "<group>"; };
|
||||||
|
E2FE0004261FE0000083EAC5 /* BinderImportModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BinderImportModels.swift; sourceTree = "<group>"; };
|
||||||
|
E2FE0005261FE0000083EAC5 /* TetraCubeMonsterImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TetraCubeMonsterImporter.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -316,6 +330,7 @@
|
|||||||
E20CF008261FF37700B55D72 /* MonsterCards.entitlements */,
|
E20CF008261FF37700B55D72 /* MonsterCards.entitlements */,
|
||||||
E2570FBC25B1AC550055B23B /* Assets.xcassets */,
|
E2570FBC25B1AC550055B23B /* Assets.xcassets */,
|
||||||
E2D473FB25B5328800CB36D7 /* Helpers */,
|
E2D473FB25B5328800CB36D7 /* Helpers */,
|
||||||
|
E2FE0000261FE0000083EAC5 /* ImportExport */,
|
||||||
E2570FC625B1AC550055B23B /* Info.plist */,
|
E2570FC625B1AC550055B23B /* Info.plist */,
|
||||||
E257101225B1B2790055B23B /* Models */,
|
E257101225B1B2790055B23B /* Models */,
|
||||||
E2570FC325B1AC550055B23B /* MonsterCards.xcdatamodeld */,
|
E2570FC325B1AC550055B23B /* MonsterCards.xcdatamodeld */,
|
||||||
@@ -406,6 +421,8 @@
|
|||||||
E216E46C261FDE5600FD9262 /* MonsterViewModel+CoreData.swift */,
|
E216E46C261FDE5600FD9262 /* MonsterViewModel+CoreData.swift */,
|
||||||
E219247F261989F700C84E12 /* SavingThrowDTO.swift */,
|
E219247F261989F700C84E12 /* SavingThrowDTO.swift */,
|
||||||
E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */,
|
E20209D225D8DD9600EFE733 /* Skill+CoreDataClass.swift */,
|
||||||
|
E20209D425D8DD9600EFE736 /* Collection+CoreDataClass.swift */,
|
||||||
|
E20209D525D8DD9600EFE737 /* CollectionMonster+CoreDataClass.swift */,
|
||||||
E219248426198A1200C84E12 /* SkillDTO.swift */,
|
E219248426198A1200C84E12 /* SkillDTO.swift */,
|
||||||
E20209F925D8E19100EFE733 /* SkillViewModel.swift */,
|
E20209F925D8E19100EFE733 /* SkillViewModel.swift */,
|
||||||
E216E471261FDF3200FD9262 /* SkillViewModel+CoreData.swift */,
|
E216E471261FDF3200FD9262 /* SkillViewModel+CoreData.swift */,
|
||||||
@@ -425,6 +442,18 @@
|
|||||||
path = Helpers;
|
path = Helpers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
E2FE0000261FE0000083EAC5 /* ImportExport */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
E2FE0001261FE0000083EAC5 /* EntityImporter.swift */,
|
||||||
|
E2FE0002261FE0000083EAC5 /* BinderImporter.swift */,
|
||||||
|
E2FE0003261FE0000083EAC5 /* BinderExporter.swift */,
|
||||||
|
E2FE0004261FE0000083EAC5 /* BinderImportModels.swift */,
|
||||||
|
E2FE0005261FE0000083EAC5 /* TetraCubeMonsterImporter.swift */,
|
||||||
|
);
|
||||||
|
path = ImportExport;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -650,6 +679,11 @@
|
|||||||
E2570FF525B1ADEB0055B23B /* Dashboard.swift in Sources */,
|
E2570FF525B1ADEB0055B23B /* Dashboard.swift in Sources */,
|
||||||
E2CB0DB826081A2F00142591 /* MCAbilityScorePicker.swift in Sources */,
|
E2CB0DB826081A2F00142591 /* MCAbilityScorePicker.swift in Sources */,
|
||||||
E219249926198E0D00C84E12 /* MonsterImportHelper.swift in Sources */,
|
E219249926198E0D00C84E12 /* MonsterImportHelper.swift in Sources */,
|
||||||
|
E2FE0021261FE0000055B23B /* EntityImporter.swift in Sources */,
|
||||||
|
E2FE0022261FE0000055B23B /* BinderImporter.swift in Sources */,
|
||||||
|
E2FE0023261FE0000055B23B /* BinderExporter.swift in Sources */,
|
||||||
|
E2FE0024261FE0000055B23B /* BinderImportModels.swift in Sources */,
|
||||||
|
E2FE0025261FE0000055B23B /* TetraCubeMonsterImporter.swift in Sources */,
|
||||||
E2CB0DC026086E3C00142591 /* ChallengeRating.swift in Sources */,
|
E2CB0DC026086E3C00142591 /* ChallengeRating.swift in Sources */,
|
||||||
E257100425B1AF4A0055B23B /* SearchBar.swift in Sources */,
|
E257100425B1AF4A0055B23B /* SearchBar.swift in Sources */,
|
||||||
E20209F525D8E04300EFE733 /* AdvantageType.swift in Sources */,
|
E20209F525D8E04300EFE733 /* AdvantageType.swift in Sources */,
|
||||||
@@ -675,6 +709,8 @@
|
|||||||
E2192480261989F700C84E12 /* SavingThrowDTO.swift in Sources */,
|
E2192480261989F700C84E12 /* SavingThrowDTO.swift in Sources */,
|
||||||
E216B7B7260C5A9800FB205F /* ChallengeRatingViewModel.swift in Sources */,
|
E216B7B7260C5A9800FB205F /* ChallengeRatingViewModel.swift in Sources */,
|
||||||
E20209D325D8DD9600EFE733 /* Skill+CoreDataClass.swift in Sources */,
|
E20209D325D8DD9600EFE733 /* Skill+CoreDataClass.swift in Sources */,
|
||||||
|
E20209D425D8DD9600EFE734 /* Collection+CoreDataClass.swift in Sources */,
|
||||||
|
E20209D525D8DD9600EFE735 /* CollectionMonster+CoreDataClass.swift in Sources */,
|
||||||
E24ACE652607F55D009BF703 /* EditSavingThrows.swift in Sources */,
|
E24ACE652607F55D009BF703 /* EditSavingThrows.swift in Sources */,
|
||||||
E2BD702C25B3A8D70058ED69 /* MCTextField.swift in Sources */,
|
E2BD702C25B3A8D70058ED69 /* MCTextField.swift in Sources */,
|
||||||
E216B7BC260C691400FB205F /* EditChallengeRating.swift in Sources */,
|
E216B7BC260C691400FB205F /* EditChallengeRating.swift in Sources */,
|
||||||
@@ -738,6 +774,7 @@
|
|||||||
E216E48A261FE76F00FD9262 /* Debug */ = {
|
E216E48A261FE76F00FD9262 /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = J793L9LQJ2;
|
DEVELOPMENT_TEAM = J793L9LQJ2;
|
||||||
@@ -760,6 +797,7 @@
|
|||||||
E216E48B261FE76F00FD9262 /* Release */ = {
|
E216E48B261FE76F00FD9262 /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
DEVELOPMENT_TEAM = J793L9LQJ2;
|
DEVELOPMENT_TEAM = J793L9LQJ2;
|
||||||
@@ -782,6 +820,7 @@
|
|||||||
E2570FDD25B1AC550055B23B /* Debug */ = {
|
E2570FDD25B1AC550055B23B /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
@@ -830,7 +869,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
ONLY_ACTIVE_ARCH = YES;
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
@@ -843,6 +882,7 @@
|
|||||||
E2570FDE25B1AC550055B23B /* Release */ = {
|
E2570FDE25B1AC550055B23B /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
CLANG_ANALYZER_NONNULL = YES;
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
@@ -885,7 +925,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
SDKROOT = iphoneos;
|
SDKROOT = iphoneos;
|
||||||
@@ -898,6 +938,7 @@
|
|||||||
E2570FE025B1AC550055B23B /* Debug */ = {
|
E2570FE025B1AC550055B23B /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
@@ -924,6 +965,7 @@
|
|||||||
E2570FE125B1AC550055B23B /* Release */ = {
|
E2570FE125B1AC550055B23B /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
@@ -950,12 +992,13 @@
|
|||||||
E2570FE325B1AC550055B23B /* Debug */ = {
|
E2570FE325B1AC550055B23B /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = J793L9LQJ2;
|
DEVELOPMENT_TEAM = J793L9LQJ2;
|
||||||
INFOPLIST_FILE = MonsterCardsTests/Info.plist;
|
INFOPLIST_FILE = MonsterCardsTests/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -972,12 +1015,13 @@
|
|||||||
E2570FE425B1AC550055B23B /* Release */ = {
|
E2570FE425B1AC550055B23B /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = J793L9LQJ2;
|
DEVELOPMENT_TEAM = J793L9LQJ2;
|
||||||
INFOPLIST_FILE = MonsterCardsTests/Info.plist;
|
INFOPLIST_FILE = MonsterCardsTests/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
@@ -994,6 +1038,7 @@
|
|||||||
E2570FE625B1AC550055B23B /* Debug */ = {
|
E2570FE625B1AC550055B23B /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = J793L9LQJ2;
|
DEVELOPMENT_TEAM = J793L9LQJ2;
|
||||||
@@ -1014,6 +1059,7 @@
|
|||||||
E2570FE725B1AC550055B23B /* Release */ = {
|
E2570FE725B1AC550055B23B /* Release */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_VERSION = 6.0;
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
DEVELOPMENT_TEAM = J793L9LQJ2;
|
DEVELOPMENT_TEAM = J793L9LQJ2;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"originHash" : "427e2f78970f33b38feca26c7986bebb5f56cf55a388a29b6bade958aaa6607e",
|
||||||
"pins" : [
|
"pins" : [
|
||||||
{
|
{
|
||||||
"identity" : "markdownui",
|
"identity" : "markdownui",
|
||||||
@@ -23,10 +24,10 @@
|
|||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/swiftlang/swift-cmark",
|
"location" : "https://github.com/swiftlang/swift-cmark",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "924936d0427cb25a61169739a7660230bffa6ea6",
|
"revision" : "08ddb528923cc1a6527e02b7a1aee9e516ca749a",
|
||||||
"version" : "0.8.0"
|
"version" : "0.9.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"version" : 2
|
"version" : 3
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,26 +109,3 @@ class MonsterImportHelper {
|
|||||||
return monster
|
return monster
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Generic JSON import (auto-detects schema via EntityImporter)
|
|
||||||
|
|
||||||
extension MonsterImportHelper {
|
|
||||||
|
|
||||||
/// Attempts to parse raw JSON into a ``MonsterViewModel`` by trying all registered
|
|
||||||
/// ``EntityImporter`` implementations in turn. Returns `nil` if no importer recognises the input.
|
|
||||||
static func fromJSON(_ json: String) -> MonsterViewModel? {
|
|
||||||
// Try each EntityImporter until one says it can handle this format.
|
|
||||||
let importers: [EntityImporter.Type] = [TetraCubeMonsterImporter.self, BinderImporter.self]
|
|
||||||
|
|
||||||
for importer in importers where importer.canImport(json) {
|
|
||||||
do {
|
|
||||||
return try importer.parse(json)
|
|
||||||
} catch {
|
|
||||||
print("⚠️ EntityImporter \(importer) threw while parsing: \(error.localizedDescription)")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
// BinderExporter.swift
|
// BinderExporter.swift
|
||||||
// MonsterCards
|
// MonsterCards
|
||||||
//
|
//
|
||||||
// Port of com.majinnaibu.monstercards.exporters.BinderExporter.java
|
// Serialises MonsterViewModel -> Binder platform JSON.
|
||||||
// Serialises MonsterViewModel → Binder platform JSON (pretty-printed, Gson-compatible).
|
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
// MARK: - Binder export DTOs
|
// MARK: - Binder export DTOs
|
||||||
|
|
||||||
@@ -19,17 +19,16 @@ struct BinderExport {
|
|||||||
let id: String
|
let id: String
|
||||||
let name: String
|
let name: String
|
||||||
let description: String
|
let description: String
|
||||||
let cards: [MonsterExport]
|
let cards: [[String: Any]]
|
||||||
|
|
||||||
init(id: String, name: String, description: String, cards: [MonsterExport]) {
|
init(id: String, name: String, description: String, cards: [[String: Any]]) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
// Preserve empty list explicitly (Android uses new ArrayList<>())
|
|
||||||
self.cards = cards.count > 0 ? cards : []
|
self.cards = cards.count > 0 ? cards : []
|
||||||
}
|
}
|
||||||
|
|
||||||
static func named(_ name: String, cards: [MonsterExport]) -> CollectionExport {
|
static func named(_ name: String, cards: [[String: Any]]) -> CollectionExport {
|
||||||
CollectionExport(id: UUID().uuidString, name: name, description: "", cards: cards)
|
CollectionExport(id: UUID().uuidString, name: name, description: "", cards: cards)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,6 +36,7 @@ struct BinderExport {
|
|||||||
|
|
||||||
struct MonsterExport {
|
struct MonsterExport {
|
||||||
/// Build a JSON-serialisable dictionary from a `MonsterViewModel`.
|
/// Build a JSON-serialisable dictionary from a `MonsterViewModel`.
|
||||||
|
@MainActor
|
||||||
static func makeDictionary(monster: MonsterViewModel, idOverride: UUID? = nil) -> [String: Any] {
|
static func makeDictionary(monster: MonsterViewModel, idOverride: UUID? = nil) -> [String: Any] {
|
||||||
var d: [String: Any] = [
|
var d: [String: Any] = [
|
||||||
"$schema": "https://majinnaibu.com/schemas/monster-card.schema.json",
|
"$schema": "https://majinnaibu.com/schemas/monster-card.schema.json",
|
||||||
@@ -44,13 +44,13 @@ struct MonsterExport {
|
|||||||
|
|
||||||
// Core identity
|
// Core identity
|
||||||
"id": idOverride?.uuidString ?? UUID().uuidString,
|
"id": idOverride?.uuidString ?? UUID().uuidString,
|
||||||
"name": monster.name ?? "",
|
"name": monster.name,
|
||||||
"size": monster.size ?? "",
|
"size": monster.size,
|
||||||
"type": monster.type ?? "",
|
"type": monster.type,
|
||||||
"subtype": monster.subType ?? "",
|
"subtype": monster.subType,
|
||||||
"alignment": monster.alignment ?? "",
|
"alignment": monster.alignment,
|
||||||
|
|
||||||
// Ability scores — keys match Android Monster.java Gson output literally.
|
// Ability scores
|
||||||
"strengthScore": monster.strengthScore,
|
"strengthScore": monster.strengthScore,
|
||||||
"dexterityScore": monster.dexterityScore,
|
"dexterityScore": monster.dexterityScore,
|
||||||
"constitutionScore": monster.constitutionScore,
|
"constitutionScore": monster.constitutionScore,
|
||||||
@@ -58,7 +58,7 @@ struct MonsterExport {
|
|||||||
"wisdomScore": monster.wisdomScore,
|
"wisdomScore": monster.wisdomScore,
|
||||||
"charismaScore": monster.charismaScore,
|
"charismaScore": monster.charismaScore,
|
||||||
|
|
||||||
// Saving throw proficiencies & advantages (raw string from Android enum.toString()).
|
// Saving throw proficiencies & advantages
|
||||||
"strengthSavingThrowProficiency": monster.strengthSavingThrowProficiency.rawValue,
|
"strengthSavingThrowProficiency": monster.strengthSavingThrowProficiency.rawValue,
|
||||||
"strengthSavingThrowAdvantage": monster.strengthSavingThrowAdvantage.rawValue,
|
"strengthSavingThrowAdvantage": monster.strengthSavingThrowAdvantage.rawValue,
|
||||||
"dexteritySavingThrowProficiency": monster.dexteritySavingThrowProficiency.rawValue,
|
"dexteritySavingThrowProficiency": monster.dexteritySavingThrowProficiency.rawValue,
|
||||||
@@ -76,12 +76,12 @@ struct MonsterExport {
|
|||||||
"armorType": monster.armorType.rawValue,
|
"armorType": monster.armorType.rawValue,
|
||||||
"shieldBonus": Int32(monster.shieldBonus),
|
"shieldBonus": Int32(monster.shieldBonus),
|
||||||
"naturalArmorBonus": monster.naturalArmorBonus,
|
"naturalArmorBonus": monster.naturalArmorBonus,
|
||||||
"otherArmorDescription": monster.otherArmorDescription ?? "",
|
"otherArmorDescription": monster.customArmor,
|
||||||
|
|
||||||
// Hit points
|
// Hit points
|
||||||
"hitDice": monster.hitDice,
|
"hitDice": monster.hitDice,
|
||||||
"hasCustomHP": monster.hasCustomHP,
|
"hasCustomHP": monster.hasCustomHP,
|
||||||
"customHitPointsDescription": monster.customHP ?? "",
|
"customHitPointsDescription": monster.customHP,
|
||||||
|
|
||||||
// Speeds
|
// Speeds
|
||||||
"walkSpeed": monster.walkSpeed,
|
"walkSpeed": monster.walkSpeed,
|
||||||
@@ -91,47 +91,44 @@ struct MonsterExport {
|
|||||||
"canHover": monster.canHover,
|
"canHover": monster.canHover,
|
||||||
"swimSpeed": monster.swimSpeed,
|
"swimSpeed": monster.swimSpeed,
|
||||||
"hasCustomSpeed": monster.hasCustomSpeed,
|
"hasCustomSpeed": monster.hasCustomSpeed,
|
||||||
"customSpeedDescription": monster.customSpeed ?? "",
|
"customSpeedDescription": monster.customSpeed,
|
||||||
|
|
||||||
// Challenge rating
|
// Challenge rating
|
||||||
"challengeRating": monster.challengeRating.rawValue,
|
"challengeRating": monster.challengeRating.rawValue,
|
||||||
"customChallengeRatingDescription": monster.customChallengeRating ?? "",
|
"customChallengeRatingDescription": monster.customChallengeRating,
|
||||||
"customProficiencyBonus": monster.customProficiencyBonus,
|
"customProficiencyBonus": monster.customProficiencyBonus,
|
||||||
|
|
||||||
// Telepathy & understanding
|
// Telepathy & understanding
|
||||||
"telepathyRange": monster.telepathy,
|
"telepathyRange": monster.telepathy,
|
||||||
"understandsButDescription": monster.understandsBut ?? "",
|
"understandsButDescription": monster.understandsBut,
|
||||||
|
|
||||||
// Lists (strings) — keys match Android Monster.java @ColumnInfo / Gson names.
|
// Lists
|
||||||
"senses": monster.senses.map { ($0.name ?? "") },
|
"senses": monster.senses.map { $0.name },
|
||||||
"damageImmunities": monster.damageImmunities.map { ($0.name ?? "") },
|
"damageImmunities": monster.damageImmunities.map { $0.name },
|
||||||
"damageResistances": monster.damageResistances.map { ($0.name ?? "") },
|
"damageResistances": monster.damageResistances.map { $0.name },
|
||||||
"damageVulnerabilities": monster.damageVulnerabilities.map { ($0.name ?? "") },
|
"damageVulnerabilities": monster.damageVulnerabilities.map { $0.name },
|
||||||
"conditionImmunities": monster.conditionImmunities.map { ($0.name ?? "") },
|
"conditionImmunities": monster.conditionImmunities.map { $0.name },
|
||||||
|
|
||||||
// Skills — each is {"name": ..., "stat": "...", "proficiency": "..."}
|
// Skills
|
||||||
"skills": monster.skills.map { s -> [String: Any] in
|
"skills": monster.skills.map { s -> [String: Any] in
|
||||||
return ["name": s.name ?? "", "stat": s.stat.rawValue, "proficiency": s.proficiency.rawValue]
|
return ["name": s.name, "stat": s.abilityScore.rawValue, "proficiency": s.proficiency.rawValue]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Languages — each is {"name": ..., "speaks": Bool}
|
// Languages
|
||||||
"languages": monster.languages.map { l -> [String: Any] in
|
"languages": monster.languages.map { l -> [String: Any] in
|
||||||
return ["name": l.name ?? "", "speaks": l.speaks]
|
return ["name": l.name, "speaks": l.speaks]
|
||||||
},
|
},
|
||||||
|
|
||||||
// Combat lists — each is {"name": ..., "desc": ...}
|
// Combat lists
|
||||||
"abilities": monster.abilities.map { ["name": $0.name ?? "", "desc": $0.description ?? ""] },
|
"abilities": monster.abilities.map { ["name": $0.name, "desc": $0.description] },
|
||||||
"actions": monster.actions.map { ["name": $0.name ?? "", "desc": $0.description ?? ""] },
|
"actions": monster.actions.map { ["name": $0.name, "desc": $0.description] },
|
||||||
"reactions": monster.reactions.map { ["name": $0.name ?? "", "desc": $0.description ?? ""] },
|
"reactions": monster.reactions.map { ["name": $0.name, "desc": $0.description] },
|
||||||
"legendaryActions": monster.legendaryActions.map { ["name": $0.name ?? "", "desc": $0.description ?? ""] },
|
"legendaryActions": monster.legendaryActions.map { ["name": $0.name, "desc": $0.description] },
|
||||||
"lairActions": monster.lairActions.map { ["name": $0.name ?? "", "desc": $0.description ?? ""] },
|
"lairActions": monster.lairActions.map { ["name": $0.name, "desc": $0.description] },
|
||||||
"regionalActions": monster.regionalActions.map { ["name": $0.name ?? "", "desc": $0.description ?? ""] },
|
"regionalActions": monster.regionalActions.map { ["name": $0.name, "desc": $0.description] },
|
||||||
"bonusActions": monster.bonusActions.map { ["name": $0.name ?? "", "desc": $0.description ?? ""] },
|
|
||||||
|
|
||||||
// Mythic actions — Android Monster model has this; iOS lacks it.
|
|
||||||
"mythicActions": [] as [[String: Any]],
|
"mythicActions": [] as [[String: Any]],
|
||||||
|
|
||||||
// Personality / physical description — Android-only fields on Monster.java.
|
|
||||||
"playerName": "",
|
"playerName": "",
|
||||||
"background": "",
|
"background": "",
|
||||||
"personalityTraits": "",
|
"personalityTraits": "",
|
||||||
@@ -153,20 +150,16 @@ struct MonsterExport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - BinderExporter (mirrors public API of com.majinnaibu.monstercards.exporters.BinderExporter)
|
// MARK: - BinderExporter
|
||||||
|
|
||||||
struct BinderExporter {
|
struct BinderExporter {
|
||||||
|
|
||||||
// MARK: -- Utility — pretty-print JSON [String:Any] → string (Gson-compatible format)
|
|
||||||
|
|
||||||
private static func toJsonString(_ obj: Any) -> String? {
|
private static func toJsonString(_ obj: Any) -> String? {
|
||||||
guard let data = try? JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted]) else { return nil }
|
guard let data = try? JSONSerialization.data(withJSONObject: obj, options: [.prettyPrinted]) else { return nil }
|
||||||
return String(data: data, encoding: .utf8)
|
return String(data: data, encoding: .utf8)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: -- Public API matching Android exactly
|
@MainActor
|
||||||
|
|
||||||
/// `exportBinder(String collectionName, List<Monster> monsters)`
|
|
||||||
static func exportBinder(collectionName: String, monsters: [MonsterViewModel], idOverride: UUID? = nil) -> String {
|
static func exportBinder(collectionName: String, monsters: [MonsterViewModel], idOverride: UUID? = nil) -> String {
|
||||||
let col = BinderExport.CollectionExport(
|
let col = BinderExport.CollectionExport(
|
||||||
id: UUID().uuidString,
|
id: UUID().uuidString,
|
||||||
@@ -189,10 +182,9 @@ struct BinderExporter {
|
|||||||
return toJsonString(root) ?? "{}"
|
return toJsonString(root) ?? "{}"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `exportBinder(Collection collection, List<Monster> monsters)`
|
@MainActor
|
||||||
static func exportBinder(collection: NSManagedObject?, monsters: [MonsterViewModel]) -> String {
|
static func exportBinder(collection: NSManagedObject?, monsters: [MonsterViewModel]) -> String {
|
||||||
guard let colObj = collection else {
|
guard let colObj = collection else {
|
||||||
// Empty-collection fallback (same path as Android with null).
|
|
||||||
return exportBinder(collectionName: "", monsters: monsters)
|
return exportBinder(collectionName: "", monsters: monsters)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -221,14 +213,14 @@ struct BinderExporter {
|
|||||||
return toJsonString(root) ?? "{}"
|
return toJsonString(root) ?? "{}"
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `exportFullBackup(List<CollectionExport> collections, List<Monster> dashboardMonsters)`
|
@MainActor
|
||||||
static func exportFullBackup(collections: [BinderExport.CollectionExport], dashboardMonsters: [MonsterViewModel]?) -> String {
|
static func exportFullBackup(collections: [BinderExport.CollectionExport], dashboardMonsters: [MonsterViewModel]?) -> String {
|
||||||
let colDicts = collections.map { col -> [String: Any] in
|
let colDicts = collections.map { col -> [String: Any] in
|
||||||
return [
|
return [
|
||||||
"id": col.id,
|
"id": col.id,
|
||||||
"name": col.name,
|
"name": col.name,
|
||||||
"description": col.description,
|
"description": col.description,
|
||||||
"cards": col.cards.map { $0 } // already a [String: Any] from makeDictionary
|
"cards": col.cards.map { $0 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,7 +231,6 @@ struct BinderExporter {
|
|||||||
]
|
]
|
||||||
|
|
||||||
guard let dashboard = dashboardMonsters, !dashboard.isEmpty else {
|
guard let dashboard = dashboardMonsters, !dashboard.isEmpty else {
|
||||||
// Android Gson only includes the field when it's populated.
|
|
||||||
return toJsonString(base) ?? "{}"
|
return toJsonString(base) ?? "{}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,16 +3,14 @@
|
|||||||
// MonsterCards
|
// MonsterCards
|
||||||
//
|
//
|
||||||
// Data models for parsing Binder (TRPG platform) JSON format.
|
// Data models for parsing Binder (TRPG platform) JSON format.
|
||||||
// Companion to Android BinderExporter / Monster classes.
|
// Field names match the keys produced by Binder's serializer.
|
||||||
// Field names here must match the keys produced by Binder's Gson serializer,
|
//
|
||||||
// which serializes the Android `Monster` POJO.
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Top-level Binder export file containing collections of monster cards.
|
/// Top-level Binder export file containing collections of monster cards.
|
||||||
struct BinderRoot: Decodable {
|
struct BinderRoot: Decodable {
|
||||||
let schemaVersion: Int?
|
let schemaVersion: Int?
|
||||||
@Environment(\.codingVersion) var codingScheme
|
|
||||||
let collections: [CollectionModel]
|
let collections: [CollectionModel]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,9 +25,7 @@ struct MonsterCard: Decodable {
|
|||||||
|
|
||||||
// MARK: - Basic info
|
// MARK: - Basic info
|
||||||
|
|
||||||
typealias CodingKeys = MonsterCardCodingKey
|
enum CodingKeys: String, CodingKey {
|
||||||
|
|
||||||
enum MonsterCardCodingKey: String, CodingKey {
|
|
||||||
case name, displayName, displayType, alignment, sizeAbbrv
|
case name, displayName, displayType, alignment, sizeAbbrv
|
||||||
case shieldName, baseAcFormula, baseAcVal, shieldAcNum, extraArmorAcMod
|
case shieldName, baseAcFormula, baseAcVal, shieldAcNum, extraArmorAcMod
|
||||||
case hitDice, customHpText, speedText
|
case hitDice, customHpText, speedText
|
||||||
@@ -39,7 +35,6 @@ struct MonsterCard: Decodable {
|
|||||||
case specialdamage, languageLine, sensesDescription
|
case specialdamage, languageLine, sensesDescription
|
||||||
case crDisplay, proficiencyBonusValue
|
case crDisplay, proficiencyBonusValue
|
||||||
case cardTraits, legendaryActionsWithHeader
|
case cardTraits, legendaryActionsWithHeader
|
||||||
case attributes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let name: String?
|
let name: String?
|
||||||
@@ -65,7 +60,7 @@ struct MonsterCard: Decodable {
|
|||||||
/// Additional armor modifier (e.g. "+4", "-1").
|
/// Additional armor modifier (e.g. "+4", "-1").
|
||||||
let extraArmorAcMod: String?
|
let extraArmorAcMod: String?
|
||||||
|
|
||||||
/// Hit dice (e.g. "38d10"). When nil, uses hitDice from attributes[0].
|
/// Hit dice (e.g. "38d10").
|
||||||
let hitDice: String?
|
let hitDice: String?
|
||||||
|
|
||||||
/// Custom HP text when the monster has custom HP.
|
/// Custom HP text when the monster has custom HP.
|
||||||
@@ -124,11 +119,6 @@ struct MonsterCard: Decodable {
|
|||||||
/// Proficiency bonus numeric value.
|
/// Proficiency bonus numeric value.
|
||||||
let proficiencyBonusValue: Int?
|
let proficiencyBonusValue: Int?
|
||||||
|
|
||||||
/// Display armor class line (e.g. "AC 16; no armor, +4 shield (+2 AC), base AC 10").
|
|
||||||
let acLine: String?
|
|
||||||
|
|
||||||
// MARK: - Traits from card
|
|
||||||
|
|
||||||
/// Card trait entries (abilities/actions) with section headers and grouping flags.
|
/// Card trait entries (abilities/actions) with section headers and grouping flags.
|
||||||
let cardTraits: [CardTraitEntry]?
|
let cardTraits: [CardTraitEntry]?
|
||||||
|
|
||||||
@@ -146,7 +136,6 @@ struct CardAttributeBox: Decodable {
|
|||||||
/// A proficiency-based skill entry (e.g. Perception).
|
/// A proficiency-based skill entry (e.g. Perception).
|
||||||
struct ProfSkillBox: Decodable {
|
struct ProfSkillBox: Decodable {
|
||||||
let name: String?
|
let name: String?
|
||||||
@Environment(\.codingVersion) var codingScheme
|
|
||||||
let isExpertise: Int? // 0 or 1 stored as int; nil means false
|
let isExpertise: Int? // 0 or 1 stored as int; nil means false
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
@@ -157,7 +146,6 @@ struct ProfSkillBox: Decodable {
|
|||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
name = try container.decodeIfPresent(String.self, forKey: .name)
|
name = try container.decodeIfPresent(String.self, forKey: .name)
|
||||||
|
|
||||||
// "expertise" field may be 0 or 1 as Int. Also store it in isExpertise.
|
|
||||||
if let expertiseValue = (try? container.decode(Int.self, forKey: .expertise)), expertiseValue == 1 {
|
if let expertiseValue = (try? container.decode(Int.self, forKey: .expertise)), expertiseValue == 1 {
|
||||||
self.isExpertise = 1
|
self.isExpertise = 1
|
||||||
} else {
|
} else {
|
||||||
@@ -177,6 +165,6 @@ struct SpecialDamageBox: Decodable {
|
|||||||
struct CardTraitEntry: Decodable {
|
struct CardTraitEntry: Decodable {
|
||||||
let displayName: String? // action/ability name
|
let displayName: String? // action/ability name
|
||||||
let desc: String? // ability text
|
let desc: String? // ability text
|
||||||
let isActionHeader: Int? // 0 or 1 flag for section headers (e.g. "Spellcasting", "Legendary Actions")
|
let isActionHeader: Int? // 0 or 1 flag for section headers
|
||||||
let actionSection: Int? // grouping key (0=bonus actions, 1=actions, 2=reactions, etc.)
|
let actionSection: Int? // grouping key (0=bonus actions, 1=actions, 2=reactions, etc.)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
// MonsterCards
|
// MonsterCards
|
||||||
//
|
//
|
||||||
// Detects and parses Binder (TRPG platform) JSON format into a MonsterViewModel.
|
// Detects and parses Binder (TRPG platform) JSON format into a MonsterViewModel.
|
||||||
// Companion to Android BinderImporter.java — shares the same detection logic (schema or collections key).
|
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
@@ -20,14 +19,14 @@ struct BinderImporter: EntityImporter {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for $schema reference to binder.schema.json (Android BinderImporter.java line ~32)
|
// Check for $schema reference to binder.schema.json
|
||||||
if let schema = root["$schema"] as? String, schema.contains("binder.schema.json") {
|
if let schema = root["$schema"] as? String, schema.contains("binder.schema.json") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fall back: check known Binder fields from collections array (matches Android logic)
|
// Fall back: check known Binder fields from collections array
|
||||||
if root["collections"] is [Any] {
|
if let collections = root["collections"] as? [Any] {
|
||||||
return !CollectionsCheck.isEmpty(try? JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]])
|
return !collections.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
@@ -35,6 +34,7 @@ struct BinderImporter: EntityImporter {
|
|||||||
|
|
||||||
// MARK: - parse
|
// MARK: - parse
|
||||||
|
|
||||||
|
@MainActor
|
||||||
static func parse(_ raw: String) throws -> MonsterViewModel {
|
static func parse(_ raw: String) throws -> MonsterViewModel {
|
||||||
guard let data = raw.data(using: .utf8) else {
|
guard let data = raw.data(using: .utf8) else {
|
||||||
throw BinderParseError.unsupportedEncoding
|
throw BinderParseError.unsupportedEncoding
|
||||||
@@ -57,59 +57,57 @@ struct BinderImporter: EntityImporter {
|
|||||||
|
|
||||||
// MARK: - Mapping
|
// MARK: - Mapping
|
||||||
|
|
||||||
/// Maps a `MonsterCard` (Binder inner type or top-level export entry) to MonsterViewModel.
|
/// Maps a `MonsterCard` to MonsterViewModel.
|
||||||
|
@MainActor
|
||||||
private static func mapMonster(_ card: MonsterCard) -> MonsterViewModel {
|
private static func mapMonster(_ card: MonsterCard) -> MonsterViewModel {
|
||||||
let monster = MonsterViewModel()
|
let monster = MonsterViewModel()
|
||||||
|
|
||||||
// MARK: Basic info (Android: name, displayType, sizeAbbrv, alignment)
|
// MARK: Basic info
|
||||||
monster.name = card.displayName ?? card.name ?? ""
|
monster.name = card.displayName ?? card.name ?? ""
|
||||||
monster.type = card.displayType ?? ""
|
monster.type = card.displayType ?? ""
|
||||||
// sizeAbbrv maps like Android: "T" → tiny, "S" → small, etc.
|
|
||||||
monster.size = abrvToSize(card.sizeAbbrv)
|
monster.size = abrvToSize(card.sizeAbbrv)
|
||||||
monster.subType = "" // Binder does not have a separate subtype field
|
monster.subType = ""
|
||||||
monster.alignment = card.alignment ?? ""
|
monster.alignment = card.alignment ?? ""
|
||||||
|
|
||||||
// MARK: AC & armor (from Android Monster.java fields: shieldName, baseAcFormula, etc.)
|
// MARK: AC & armor
|
||||||
monster.hasShield = (card.shieldAcNum ?? 0) != 0 || ((card.baseAcFormula?.contains("shield")) ?? false)
|
monster.hasShield = (card.shieldAcNum ?? 0) != 0 || ((card.baseAcFormula?.contains("shield")) ?? false)
|
||||||
monster.shieldBonus = card.shieldAcNum ?? (monster.hasShield ? 2 : 0)
|
monster.shieldBonus = card.shieldAcNum ?? (monster.hasShield ? 2 : 0)
|
||||||
|
|
||||||
if card.baseAcFormula != nil || (card.shieldName != nil && !card.shieldName!.isEmpty) {
|
if card.baseAcFormula != nil || (card.shieldName != nil && !card.shieldName!.isEmpty) {
|
||||||
monster.armorType = .other // unarmored-defense / custom approach
|
monster.armorType = .other
|
||||||
monster.otherArmorDescription = card.shieldName ?? "no shield"
|
monster.customArmor = card.shieldName ?? "no shield"
|
||||||
} else {
|
} else {
|
||||||
monster.armorType = .none
|
monster.armorType = .none
|
||||||
}
|
}
|
||||||
|
|
||||||
if let armorModStr = card.extraArmorAcMod, let modVal = Int(armorModStr) {
|
if let armorModStr = card.extraArmorAcMod, let modVal = Int(armorModStr) {
|
||||||
// For "no armor" style, extraArmorAcMod is added directly to the AC formula text.
|
let suffix = (monster.customArmor.isEmpty ? "" : ", ") + "(+\(modVal))"
|
||||||
let suffix = (card.otherArmorDescription.isEmpty ? "" : ", ") + "(+\(modVal))"
|
monster.customArmor += suffix
|
||||||
monster.otherArmorDescription += suffix
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: HP & hit dice (Android: hitDice → String like "38d10"; customHpText for override)
|
// MARK: HP & hit dice
|
||||||
if card.customHpText != nil && !card.customHpText!.isEmpty {
|
if let customHpText = card.customHpText, !customHpText.isEmpty {
|
||||||
monster.hasCustomHP = true
|
monster.hasCustomHP = true
|
||||||
monster.customHP = card.customHpText!
|
monster.customHP = customHpText
|
||||||
} else {
|
} else {
|
||||||
// Parse hit dice string like "38d10" → hitDice = 38
|
|
||||||
if let hdStr = card.hitDice, let parts = splitHitDice(hdStr) {
|
if let hdStr = card.hitDice, let parts = splitHitDice(hdStr) {
|
||||||
monster.hitDice = Int64(parts.diceCount ?? 0)
|
monster.hitDice = Int64(parts.diceCount ?? 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Speed (Android MonsterImportHelper.speedFromString)
|
// MARK: Speed
|
||||||
if let stxt = card.speedText where !stxt.isEmpty && !stxt.contains("feet") {
|
if let stxt = card.speedText, !stxt.isEmpty, !stxt.contains("feet") {
|
||||||
monster.customSpeed = stxt
|
monster.customSpeed = stxt
|
||||||
monster.hasCustomSpeed = true
|
monster.hasCustomSpeed = true
|
||||||
} else if card.speedText != nil {
|
} else if let speedText = card.speedText {
|
||||||
monster.walkSpeed = Int64(extractSpeedMeters(card.speedText, forSpeed: "walk"))
|
monster.walkSpeed = Int64(extractSpeedMeters(speedText, forSpeed: "walk"))
|
||||||
monster.burrowSpeed = Int64(extractSpeedMeters(card.speedText, forSpeed: "burrow"))
|
monster.burrowSpeed = Int64(extractSpeedMeters(speedText, forSpeed: "burrow"))
|
||||||
monster.climbSpeed = Int64(extractSpeedMeters(card.speedText, forSpeed: "climb"))
|
monster.climbSpeed = Int64(extractSpeedMeters(speedText, forSpeed: "climb"))
|
||||||
monster.flySpeed = Int64(extractSpeedMeters(card.speedText, forSpeed: "fly"))
|
monster.flySpeed = Int64(extractSpeedMeters(speedText, forSpeed: "fly"))
|
||||||
monster.swimSpeed = Int64(extractSpeedMeters(card.speedText, forSpeed: "swim"))
|
monster.swimSpeed = Int64(extractSpeedMeters(speedText, forSpeed: "swim"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Ability scores (Android Monster.primaryAttributes[0…5] in [STR, DEX, CON, INT, WIS, CHA])
|
// MARK: Ability scores
|
||||||
if let attrs = card.primaryAttributes, attrs.count >= 6 {
|
if let attrs = card.primaryAttributes, attrs.count >= 6 {
|
||||||
monster.strengthScore = Int64(attrs[0].value ?? 10)
|
monster.strengthScore = Int64(attrs[0].value ?? 10)
|
||||||
monster.dexterityScore = Int64(attrs[1].value ?? 10)
|
monster.dexterityScore = Int64(attrs[1].value ?? 10)
|
||||||
@@ -119,7 +117,7 @@ struct BinderImporter: EntityImporter {
|
|||||||
monster.charismaScore = Int64(attrs[5].value ?? 10)
|
monster.charismaScore = Int64(attrs[5].value ?? 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Saving throws proficiency (Android Monster.proSavingThrows)
|
// MARK: Saving throws proficiency
|
||||||
for throwName in (card.proSavingThrows ?? []) {
|
for throwName in (card.proSavingThrows ?? []) {
|
||||||
switch throwName.lowercased() {
|
switch throwName.lowercased() {
|
||||||
case "str" : monster.strengthSavingThrowProficiency = .proficient
|
case "str" : monster.strengthSavingThrowProficiency = .proficient
|
||||||
@@ -132,14 +130,13 @@ struct BinderImporter: EntityImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Skills (Android Monster.proficiencySkills)
|
// MARK: Skills
|
||||||
for skillEntry in (card.proficiencySkills ?? []) {
|
for skillEntry in (card.proficiencySkills ?? []) {
|
||||||
guard let sname = skillEntry.name else { continue }
|
guard let sname = skillEntry.name else { continue }
|
||||||
let prof: ProficiencyType = skillEntry.isExpertise == 1 ? .expertise : .proficient
|
let prof: ProficiencyType = skillEntry.isExpertise == 1 ? .expertise : .proficient
|
||||||
if let ability = SkillViewModel.knownSkillForName(sname) {
|
if let ability = SkillViewModel.knownSkillForName(sname) {
|
||||||
monster.skills.append(SkillViewModel(sname, ability, prof))
|
monster.skills.append(SkillViewModel(sname, ability, prof))
|
||||||
} else {
|
} else {
|
||||||
// Fallback
|
|
||||||
monster.skills.append(SkillViewModel(sname, .dexterity, prof))
|
monster.skills.append(SkillViewModel(sname, .dexterity, prof))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -151,24 +148,21 @@ struct BinderImporter: EntityImporter {
|
|||||||
if let dv = card.damageVulnerabilities {
|
if let dv = card.damageVulnerabilities {
|
||||||
monster.damageVulnerabilities = parseDelimitedString(dv)
|
monster.damageVulnerabilities = parseDelimitedString(dv)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Condition immunities
|
|
||||||
if let ci = card.conditionImmunities {
|
if let ci = card.conditionImmunities {
|
||||||
monster.conditionImmunities = parseDelimitedString(ci)
|
monster.conditionImmunities = parseDelimitedString(ci)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Languages (Android MonsterImportHelper.parseLanguageLine)
|
// MARK: Languages
|
||||||
if let langLine = card.languageLine {
|
if let langLine = card.languageLine {
|
||||||
monster.languages = parseLanguages(langLine)
|
monster.languages = parseLanguages(langLine)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Senses (Android sensesDescription → split into sense entries)
|
// MARK: Senses
|
||||||
if let sensesStr = card.sensesDescription, !sensesStr.isEmpty {
|
if let sensesStr = card.sensesDescription, !sensesStr.isEmpty {
|
||||||
// Each item is typically "<senseName> <distance> ft." or just a name.
|
|
||||||
monster.senses = parseDelimitedString(sensesStr)
|
monster.senses = parseDelimitedString(sensesStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Challenge rating (Android Monster.crDisplay)
|
// MARK: Challenge rating
|
||||||
let crText = card.crDisplay ?? "0"
|
let crText = card.crDisplay ?? "0"
|
||||||
if let cr = challengeRating(for: crText) {
|
if let cr = challengeRating(for: crText) {
|
||||||
monster.challengeRating = cr
|
monster.challengeRating = cr
|
||||||
@@ -181,7 +175,7 @@ struct BinderImporter: EntityImporter {
|
|||||||
monster.customProficiencyBonus = Int64(pbVal)
|
monster.customProficiencyBonus = Int64(pbVal)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Traits / actions (from Android Monster.cardTraits grouped by actionSection)
|
// MARK: Traits / actions
|
||||||
processCardTraits(card, into: monster)
|
processCardTraits(card, into: monster)
|
||||||
|
|
||||||
return monster
|
return monster
|
||||||
@@ -192,7 +186,6 @@ struct BinderImporter: EntityImporter {
|
|||||||
|
|
||||||
private extension BinderImporter {
|
private extension BinderImporter {
|
||||||
|
|
||||||
/// Convert a size abbreviation to the full size enum name. (Android uses the same mapping.)
|
|
||||||
static func abrvToSize(_ abbrv: String?) -> String {
|
static func abrvToSize(_ abbrv: String?) -> String {
|
||||||
guard let a = abbrv else { return "" }
|
guard let a = abbrv else { return "" }
|
||||||
switch a.uppercased() {
|
switch a.uppercased() {
|
||||||
@@ -206,7 +199,6 @@ private extension BinderImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse "38d10" → (diceCount: 38, dieSize: 10). Android uses this format.
|
|
||||||
static func splitHitDice(_ hdStr: String) -> (diceCount: Int?, dieSize: Int)? {
|
static func splitHitDice(_ hdStr: String) -> (diceCount: Int?, dieSize: Int)? {
|
||||||
let parts = hdStr.split(separator: "d")
|
let parts = hdStr.split(separator: "d")
|
||||||
guard parts.count == 2 else { return nil }
|
guard parts.count == 2 else { return nil }
|
||||||
@@ -215,7 +207,6 @@ private extension BinderImporter {
|
|||||||
return (count, size)
|
return (count, size)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract a specific movement speed type from formatted string like "40 feet, climb 20 ft."
|
|
||||||
static func extractSpeedMeters(_ text: String?, forSpeed type: String) -> Int {
|
static func extractSpeedMeters(_ text: String?, forSpeed type: String) -> Int {
|
||||||
guard let t = text else { return 0 }
|
guard let t = text else { return 0 }
|
||||||
let pattern = "\\b\(type)\\b"
|
let pattern = "\\b\(type)\\b"
|
||||||
@@ -223,19 +214,13 @@ private extension BinderImporter {
|
|||||||
let range = NSRange(t.startIndex..., in: t)
|
let range = NSRange(t.startIndex..., in: t)
|
||||||
if let match = regex.firstMatch(in: t, options: [], range: range),
|
if let match = regex.firstMatch(in: t, options: [], range: range),
|
||||||
let numRange = Range(match.range(at: 1), in: t) {
|
let numRange = Range(match.range(at: 1), in: t) {
|
||||||
var extractedText = ""
|
let extractedText = String(t[numRange])
|
||||||
#if swift(>=5.0)
|
|
||||||
extractedText = String(t[numRange])
|
|
||||||
#else
|
|
||||||
extractedText = (t as NSString).substring(with: numRange)
|
|
||||||
#endif
|
|
||||||
if let val = Int(extractedText) { return val }
|
if let val = Int(extractedText) { return val }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse comma-separated string into [StringViewModel].
|
|
||||||
static func parseDelimitedString(_ input: String) -> [StringViewModel] {
|
static func parseDelimitedString(_ input: String) -> [StringViewModel] {
|
||||||
return input.components(separatedBy: ",").compactMap {
|
return input.components(separatedBy: ",").compactMap {
|
||||||
let trimmed = $0.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = $0.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
@@ -243,7 +228,6 @@ private extension BinderImporter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a language line like "Common, Deep Speech (150 ft.)" into [LanguageViewModel].
|
|
||||||
static func parseLanguages(_ line: String) -> [LanguageViewModel] {
|
static func parseLanguages(_ line: String) -> [LanguageViewModel] {
|
||||||
var result: [LanguageViewModel] = []
|
var result: [LanguageViewModel] = []
|
||||||
let components = line.components(separatedBy: ",")
|
let components = line.components(separatedBy: ",")
|
||||||
@@ -254,12 +238,10 @@ private extension BinderImporter {
|
|||||||
|
|
||||||
var hasSpeaks = true
|
var hasSpeaks = true
|
||||||
|
|
||||||
// Check for understands-only patterns
|
|
||||||
if trimmed.lowercased().contains("understand") || trimmed.lowercased().contains("(silently)") {
|
if trimmed.lowercased().contains("understand") || trimmed.lowercased().contains("(silently)") {
|
||||||
hasSpeaks = false
|
hasSpeaks = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove trailing range annotation like " (150 ft.)" → keep just the name
|
|
||||||
let parts = trimmed.split(separator: " (")
|
let parts = trimmed.split(separator: " (")
|
||||||
var nameOnly = trimmed
|
var nameOnly = trimmed
|
||||||
if parts.count >= 2 {
|
if parts.count >= 2 {
|
||||||
@@ -272,11 +254,9 @@ private extension BinderImporter {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Determine ChallengeRating from text (Android Monster.crDisplay → challengeRating conversion).
|
|
||||||
static func challengeRating(for text: String?) -> ChallengeRating? {
|
static func challengeRating(for text: String?) -> ChallengeRating? {
|
||||||
guard let crText = text else { return .zero }
|
guard let crText = text else { return .zero }
|
||||||
|
|
||||||
// Handle common text aliases
|
|
||||||
switch crText.lowercased() {
|
switch crText.lowercased() {
|
||||||
case "half": return .oneHalf
|
case "half": return .oneHalf
|
||||||
case "quarter", "one quarter": return .oneQuarter
|
case "quarter", "one quarter": return .oneQuarter
|
||||||
@@ -285,12 +265,10 @@ private extension BinderImporter {
|
|||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try numeric parse first (e.g. "0" → zero, "3" → three)
|
|
||||||
if let val = Int(crText), let cr = ChallengeRating(rawValue: String(val)) {
|
if let val = Int(crText), let cr = ChallengeRating(rawValue: String(val)) {
|
||||||
return cr
|
return cr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle fractions like "1/2", "5/2"
|
|
||||||
if let rangeIdx = crText.range(of: "/") {
|
if let rangeIdx = crText.range(of: "/") {
|
||||||
let numStr = crText[..<rangeIdx.lowerBound]
|
let numStr = crText[..<rangeIdx.lowerBound]
|
||||||
let denStr = crText[rangeIdx.upperBound...]
|
let denStr = crText[rangeIdx.upperBound...]
|
||||||
@@ -309,7 +287,7 @@ private extension BinderImporter {
|
|||||||
return .zero
|
return .zero
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Process cardTraits from Android Monster.exportCards (grouped by actionSection).
|
@MainActor
|
||||||
static func processCardTraits(_ card: MonsterCard, into monster: MonsterViewModel) {
|
static func processCardTraits(_ card: MonsterCard, into monster: MonsterViewModel) {
|
||||||
guard let traits = card.cardTraits else { return }
|
guard let traits = card.cardTraits else { return }
|
||||||
|
|
||||||
@@ -317,24 +295,21 @@ private extension BinderImporter {
|
|||||||
guard let displayName = trait.displayName, !displayName.isEmpty else { continue }
|
guard let displayName = trait.displayName, !displayName.isEmpty else { continue }
|
||||||
|
|
||||||
if let isHeader = trait.isActionHeader, isHeader == 1 {
|
if let isHeader = trait.isActionHeader, isHeader == 1 {
|
||||||
// Section header like "Spellcasting", "Legendary Actions" etc.
|
|
||||||
if displayName.lowercased().contains("legendary") {
|
if displayName.lowercased().contains("legendary") {
|
||||||
monster.legendaryActions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
monster.legendaryActions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
||||||
} else if displayName.lowercased().contains("lair") || displayName.lowercased().contains("mythic") {
|
} else if displayName.lowercased().contains("lair") || displayName.lowercased().contains("mythic") {
|
||||||
monster.lairActions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
monster.lairActions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
||||||
} else {
|
} else {
|
||||||
// Action section header gets added to actions as a category label.
|
|
||||||
monster.actions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
monster.actions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
||||||
}
|
}
|
||||||
} else if let section = trait.actionSection {
|
} else if let section = trait.actionSection {
|
||||||
switch section {
|
switch section {
|
||||||
case 0: monster.bonusActions .append(AbilityViewModel(displayName, trait.desc ?? ""))
|
case 0: monster.actions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
||||||
case 1: monster.actions .append(AbilityViewModel(displayName, trait.desc ?? ""))
|
case 1: monster.actions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
||||||
case 2: monster.reactions .append(AbilityViewModel(displayName, trait.desc ?? ""))
|
case 2: monster.reactions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No section key — add to actions by default (standard Android behavior)
|
|
||||||
monster.actions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
monster.actions.append(AbilityViewModel(displayName, trait.desc ?? ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,31 +2,33 @@
|
|||||||
// EntityImporter.swift
|
// EntityImporter.swift
|
||||||
// MonsterCards
|
// MonsterCards
|
||||||
//
|
//
|
||||||
// Imported from Android commit 044202c — refactors monster importers to a common interface.
|
// Created by Tom Hicks on 4/3/21.
|
||||||
|
// Last Modified by Tom Hicks on 9/23/26.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// Protocol that all entity importers implement, matching the Java `EntityImporter<T>` interface.
|
/// Protocol that all entity importers implement.
|
||||||
/// Each importer decides whether it can handle the input and then parses it into domain objects.
|
/// Each importer decides whether it can handle the input and then parses it into domain objects.
|
||||||
protocol EntityImporter {
|
protocol EntityImporter {
|
||||||
/// Determines whether this importer can handle the given input string or payload.
|
/// Determines whether this importer can handle the given input string or payload.
|
||||||
func canImport(_ input: String) -> Bool
|
static func canImport(_ input: String) -> Bool
|
||||||
|
|
||||||
/// Parses the raw input string/JSON into a `MonsterViewModel`.
|
/// Parses the raw input string/JSON into a `MonsterViewModel`.
|
||||||
func parse(_ input: String) throws -> MonsterViewModel
|
@MainActor
|
||||||
|
static func parse(_ input: String) throws -> MonsterViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registry type that dispatches parsing to the correct importer based on format sniffing,
|
/// Registry type that dispatches parsing to the correct importer based on format sniffing.
|
||||||
/// mirroring the Android `MonsterImportHelper` dispatcher logic.
|
|
||||||
enum ImporterRegistry {
|
enum ImporterRegistry {
|
||||||
|
|
||||||
private static let importers: [EntityImporter] = [
|
@MainActor
|
||||||
MonsterJsonImporter(),
|
private static var importers: [any EntityImporter.Type] {
|
||||||
DnDBeyondImporter(),
|
[
|
||||||
Open5eImporter(),
|
TetraCubeMonsterImporter.self,
|
||||||
BinderImporter(),
|
BinderImporter.self,
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
|
||||||
/// Detects which importer handles the input and delegates parse to it.
|
/// Detects which importer handles the input and delegates parse to it.
|
||||||
@MainActor
|
@MainActor
|
||||||
@@ -40,7 +42,7 @@ enum ImporterRegistry {
|
|||||||
let monster = try importer.parse(input)
|
let monster = try importer.parse(input)
|
||||||
return monster
|
return monster
|
||||||
} catch {
|
} catch {
|
||||||
print("Importer \(type(of: importer)) error: \(error)")
|
print("Importer \(importer) error: \(error)")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
53
iOS/MonsterCards/ImportExport/TetraCubeMonsterImporter.swift
Normal file
53
iOS/MonsterCards/ImportExport/TetraCubeMonsterImporter.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// TetraCubeMonsterImporter.swift
|
||||||
|
// MonsterCards
|
||||||
|
//
|
||||||
|
// Created by Tom Hicks on 9/23/26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Importer for TetraCube monster generator JSON format.
|
||||||
|
struct TetraCubeMonsterImporter: EntityImporter {
|
||||||
|
|
||||||
|
// MARK: - canImport
|
||||||
|
|
||||||
|
static func canImport(_ input: String) -> Bool {
|
||||||
|
guard let data = input.data(using: .utf8),
|
||||||
|
let root = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect TetraCube specific keys
|
||||||
|
return root["name"] != nil && root["strPoints"] != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - parse
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
static func parse(_ input: String) throws -> MonsterViewModel {
|
||||||
|
guard let data = input.data(using: .utf8) else {
|
||||||
|
throw TetraCubeParseError.unsupportedEncoding
|
||||||
|
}
|
||||||
|
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
let monsterDTO = try decoder.decode(MonsterDTO.self, from: data)
|
||||||
|
return MonsterImportHelper.import5ESBMonster(monsterDTO)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Error Type
|
||||||
|
|
||||||
|
enum TetraCubeParseError: LocalizedError {
|
||||||
|
case unsupportedEncoding
|
||||||
|
case invalidFormat(String)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .unsupportedEncoding:
|
||||||
|
return "TetraCube import requires UTF-8 JSON"
|
||||||
|
case .invalidFormat(let msg):
|
||||||
|
return "Invalid TetraCube format: \(msg)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public class AbilityViewModel: NSObject, ObservableObject, Identifiable, NSSecureCoding {
|
public class AbilityViewModel: NSObject, ObservableObject, Identifiable, NSSecureCoding {
|
||||||
public static var supportsSecureCoding = true
|
public static let supportsSecureCoding = true
|
||||||
|
|
||||||
public func encode(with coder: NSCoder) {
|
public func encode(with coder: NSCoder) {
|
||||||
coder.encode(self.name, forKey: "name")
|
coder.encode(self.name, forKey: "name")
|
||||||
|
|||||||
15
iOS/MonsterCards/Models/Collection+CoreDataClass.swift
Normal file
15
iOS/MonsterCards/Models/Collection+CoreDataClass.swift
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
//
|
||||||
|
// Collection+CoreDataClass.swift
|
||||||
|
// MonsterCards
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(Collection)
|
||||||
|
public class Collection: NSManagedObject {
|
||||||
|
// TODO: Add convenience initializer convenience init(context: NSManagedObjectContext, name: String, details: String)
|
||||||
|
// TODO: Add helper method to add a monster to this collection (creating CollectionMonster join entity)
|
||||||
|
// TODO: Add helper method to remove a monster from this collection
|
||||||
|
// TODO: Add sortedMonsters property to return monsters ordered by ordinal
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//
|
||||||
|
// CollectionMonster+CoreDataClass.swift
|
||||||
|
// MonsterCards
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(CollectionMonster)
|
||||||
|
public class CollectionMonster: NSManagedObject {
|
||||||
|
// TODO: Add convenience initializer convenience init(context: NSManagedObjectContext, collection: Collection, monsterId: String, ordinal: Int64)
|
||||||
|
// TODO: Add helper property or fetch method to resolve monsterId to a Monster entity
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import Foundation
|
|||||||
|
|
||||||
// TODO: split this into separate Model and ViewModel classes later.
|
// TODO: split this into separate Model and ViewModel classes later.
|
||||||
public class LanguageViewModel : NSObject, ObservableObject, Comparable, Identifiable, NSSecureCoding {
|
public class LanguageViewModel : NSObject, ObservableObject, Comparable, Identifiable, NSSecureCoding {
|
||||||
public static var supportsSecureCoding = true
|
public static let supportsSecureCoding = true
|
||||||
|
|
||||||
public func encode(with coder: NSCoder) {
|
public func encode(with coder: NSCoder) {
|
||||||
coder.encode(self.name, forKey: "name")
|
coder.encode(self.name, forKey: "name")
|
||||||
|
|||||||
53
iOS/MonsterCards/Models/SearchResultItem.swift
Normal file
53
iOS/MonsterCards/Models/SearchResultItem.swift
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
//
|
||||||
|
// SearchResultItem.swift
|
||||||
|
// MonsterCards
|
||||||
|
//
|
||||||
|
// Ported from Android SearchResultItem.java.
|
||||||
|
// Disambiguation type for search-result items across collections and dashboards.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// Discriminant for a SearchResultItem's concrete entity type.
|
||||||
|
public enum SearchResultItemType: String, Codable, CaseIterable {
|
||||||
|
case monster
|
||||||
|
case collection
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lightweight non-persisted wrapper around either a Monster or a Collection,
|
||||||
|
/// with enough metadata to distinguish cross-collection search results.
|
||||||
|
public struct SearchResultItem: Identifiable, Hashable {
|
||||||
|
|
||||||
|
public let id: String // objectID-based unique key.
|
||||||
|
public let type: SearchResultItemType
|
||||||
|
public let monster: Monster?
|
||||||
|
public let collection: Collection?
|
||||||
|
|
||||||
|
// MARK: - Constructors mirroring Android
|
||||||
|
|
||||||
|
/// Wraps a Monster — sets `type = .monster`.
|
||||||
|
public init(monster: Monster) {
|
||||||
|
self.id = monster.objectID.uriRepresentation().lastPathComponent
|
||||||
|
self.type = .monster
|
||||||
|
self.monster = monster
|
||||||
|
self.collection = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wraps a Collection — sets `type = .collection`.
|
||||||
|
public init(collection: Collection) {
|
||||||
|
self.id = collection.objectID.uriRepresentation().lastPathComponent
|
||||||
|
self.type = .collection
|
||||||
|
self.monster = nil
|
||||||
|
self.collection = collection
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Identifiable / Hashable
|
||||||
|
|
||||||
|
public static func == (lhs: SearchResultItem, rhs: SearchResultItem) -> Bool {
|
||||||
|
lhs.id == rhs.id && lhs.type == rhs.type
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
hasher.combine(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,23 @@ class SkillViewModel: ObservableObject, Identifiable {
|
|||||||
self.advantage = advantage
|
self.advantage = advantage
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func knownSkillForName(_ name: String) -> AbilityScore? {
|
||||||
|
switch name.lowercased() {
|
||||||
|
case "athletics":
|
||||||
|
return .strength
|
||||||
|
case "acrobatics", "sleight of hand", "stealth":
|
||||||
|
return .dexterity
|
||||||
|
case "arcana", "history", "investigation", "nature", "religion":
|
||||||
|
return .intelligence
|
||||||
|
case "animal handling", "insight", "medicine", "perception", "survival":
|
||||||
|
return .wisdom
|
||||||
|
case "deception", "intimidation", "performance", "persuasion":
|
||||||
|
return .charisma
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func modifier(forMonster: MonsterViewModel) -> Int {
|
func modifier(forMonster: MonsterViewModel) -> Int {
|
||||||
let proficiencyBonus = Double(forMonster.proficiencyBonus)
|
let proficiencyBonus = Double(forMonster.proficiencyBonus)
|
||||||
let abilityScoreModifier = Double(forMonster.abilityModifierForAbilityScore(abilityScore))
|
let abilityScoreModifier = Double(forMonster.abilityModifierForAbilityScore(abilityScore))
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D91" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Collection" representedClassName="Collection" syncable="YES" codeGenerationType="category">
|
<entity name="Collection" representedClassName="Collection" syncable="YES" codeGenerationType="category">
|
||||||
<attribute name="description" attributeType="String" defaultValueString=""/>
|
<attribute name="details" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="sortOrder" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="sortOrder" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<relationship name="monsters" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="CollectionMonster" inverseName="collection" inverseEntity="CollectionMonster"/>
|
<relationship name="monsters" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="CollectionMonster" inverseName="collection" inverseEntity="CollectionMonster"/>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<attribute name="collectionId" attributeType="String" defaultValueString="00000000-0000-0000-0000-000000000000"/>
|
<attribute name="collectionId" attributeType="String" defaultValueString="00000000-0000-0000-0000-000000000000"/>
|
||||||
<attribute name="monsterId" attributeType="String" defaultValueString="00000000-0000-0000-0000-000000000000"/>
|
<attribute name="monsterId" attributeType="String" defaultValueString="00000000-0000-0000-0000-000000000000"/>
|
||||||
<attribute name="ordinal" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="ordinal" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<relationship name="collection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Collection" inverseName="monsters" inverseEntity="Collection"/>
|
<relationship name="collection" optional="YES" deletionRule="Nullify" destinationEntity="Collection" inverseName="monsters" inverseEntity="Collection"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Monster" representedClassName="Monster" syncable="YES" codeGenerationType="category">
|
<entity name="Monster" representedClassName="Monster" syncable="YES" codeGenerationType="category">
|
||||||
<attribute name="abilities" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
|
<attribute name="abilities" optional="YES" attributeType="Transformable" valueTransformerName="AbilityViewModelValueTransformer" customClassName="[AbilityViewModel]"/>
|
||||||
@@ -84,7 +84,7 @@
|
|||||||
<attribute name="advantage" attributeType="String" defaultValueString="none"/>
|
<attribute name="advantage" attributeType="String" defaultValueString="none"/>
|
||||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="proficiency" attributeType="String" defaultValueString="none"/>
|
<attribute name="proficiency" attributeType="String" defaultValueString="none"/>
|
||||||
<relationship name="monster" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Monster" inverseName="skills" inverseEntity="Monster"/>
|
<relationship name="monster" optional="YES" deletionRule="Nullify" destinationEntity="Monster" inverseName="skills" inverseEntity="Monster"/>
|
||||||
</entity>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="Monster" positionX="-63" positionY="-18" width="128" height="1154"/>
|
<element name="Monster" positionX="-63" positionY="-18" width="128" height="1154"/>
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import CoreData
|
|||||||
struct PersistenceController {
|
struct PersistenceController {
|
||||||
static let shared = PersistenceController()
|
static let shared = PersistenceController()
|
||||||
|
|
||||||
static var preview: PersistenceController = {
|
@MainActor
|
||||||
|
static let preview: PersistenceController = {
|
||||||
let result = PersistenceController(inMemory: true)
|
let result = PersistenceController(inMemory: true)
|
||||||
let viewContext = result.container.viewContext
|
let viewContext = result.container.viewContext
|
||||||
let monsters: [Monster] = [
|
let monsters: [Monster] = [
|
||||||
@@ -21,10 +22,9 @@ struct PersistenceController {
|
|||||||
do {
|
do {
|
||||||
try viewContext.save()
|
try viewContext.save()
|
||||||
} catch {
|
} catch {
|
||||||
// Replace this implementation with code to handle the error appropriately.
|
// TODO: Replace this implementation with code to handle the error appropriately.
|
||||||
// fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
|
|
||||||
let nsError = error as NSError
|
let nsError = error as NSError
|
||||||
// fatalError("TOMHICKS_Unresolved error \(nsError), \(nsError.userInfo)")
|
print("Unresolved error \(nsError), \(nsError.userInfo)")
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}()
|
}()
|
||||||
@@ -39,6 +39,7 @@ struct PersistenceController {
|
|||||||
container.viewContext.automaticallyMergesChangesFromParent = true
|
container.viewContext.automaticallyMergesChangesFromParent = true
|
||||||
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
container.loadPersistentStores(completionHandler: { (storeDescription, error) in
|
||||||
if let error = error as NSError? {
|
if let error = error as NSError? {
|
||||||
|
// TODO: Handle persistent store load failure gracefully instead of fatalError
|
||||||
fatalError("Unresolved error \(error), \(error.userInfo)")
|
fatalError("Unresolved error \(error), \(error.userInfo)")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,16 +6,70 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import CoreData
|
||||||
|
|
||||||
struct Collections: View {
|
struct Collections: View {
|
||||||
// @State var allCollections: [MonsterCollection] = []
|
@Environment(\.managedObjectContext) private var viewContext
|
||||||
|
|
||||||
|
@FetchRequest(
|
||||||
|
sortDescriptors: [NSSortDescriptor(keyPath: \Collection.sortOrder, ascending: true)],
|
||||||
|
animation: .default
|
||||||
|
)
|
||||||
|
private var collections: FetchedResults<Collection>
|
||||||
|
|
||||||
|
// TODO: Add state for showing 'Create Collection' dialog or sheet
|
||||||
|
// TODO: Add state for search/filter within collections
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Text("Collections")
|
NavigationStack {
|
||||||
|
List {
|
||||||
|
if collections.isEmpty {
|
||||||
|
ContentUnavailableView(
|
||||||
|
"No Collections",
|
||||||
|
systemImage: "folder.badge.plus",
|
||||||
|
description: Text("Tap '+' to create your first monster collection.")
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ForEach(collections) { collection in
|
||||||
|
NavigationLink {
|
||||||
|
// TODO: Implement CollectionDetailView to show monsters in this collection
|
||||||
|
Text(collection.name ?? "Unnamed Collection")
|
||||||
|
} label: {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text(collection.name ?? "Unnamed Collection")
|
||||||
|
.font(.headline)
|
||||||
|
if let details = collection.details, !details.isEmpty {
|
||||||
|
Text(details)
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onDelete(perform: deleteCollections)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Collections")
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
Button(action: addCollection) {
|
||||||
|
Label("Add Collection", systemImage: "plus")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addCollection() {
|
||||||
|
// TODO: Implement collection creation logic (e.g. presenting sheet or creating new Collection managed object)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deleteCollections(offsets: IndexSet) {
|
||||||
|
// TODO: Implement collection deletion logic with context saving
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Collections_Previews: PreviewProvider {
|
#Preview {
|
||||||
static var previews: some View {
|
Collections()
|
||||||
Collections().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
.environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,41 +3,60 @@
|
|||||||
// MonsterPreview
|
// MonsterPreview
|
||||||
//
|
//
|
||||||
// Created by Tom Hicks on 4/7/21.
|
// Created by Tom Hicks on 4/7/21.
|
||||||
|
// Last modified by Tom Hicks on 9/23/26.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import QuickLook
|
import QuickLook
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
class PreviewViewController: UIViewController, QLPreviewingController {
|
@MainActor
|
||||||
|
final class PreviewViewController: UIViewController, @preconcurrency QLPreviewingController {
|
||||||
|
|
||||||
|
// MARK: - QLPreviewingController
|
||||||
|
|
||||||
func preparePreviewOfFile(at url: URL, completionHandler handler: @escaping (Error?) -> Void) {
|
func preparePreviewOfFile(at url: URL, completionHandler handler: @escaping (Error?) -> Void) {
|
||||||
|
Task { @MainActor in
|
||||||
let document = MonsterDocument(fileURL: url)
|
let document = MonsterDocument(fileURL: url)
|
||||||
document.open(completionHandler: {[weak self](_) in
|
|
||||||
self?.presentMonsterViewController(for: document)
|
// Asynchronously open the document on the MainActor
|
||||||
|
let isOpen = await document.open()
|
||||||
|
|
||||||
|
guard isOpen else {
|
||||||
|
handler(CocoaError(.fileReadCorruptFile))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the model and display
|
||||||
|
let dto = document.monsterDTO ?? MonsterDTO()
|
||||||
|
self.presentMonsterViewController(for: dto)
|
||||||
|
|
||||||
|
// Notify QuickLook that preview preparation succeeded
|
||||||
handler(nil)
|
handler(nil)
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentMonsterViewController(for document: MonsterDocument) {
|
// MARK: - Private Presentation
|
||||||
let monsterViewModel = MonsterImportHelper.import5ESBMonster(document.monsterDTO ?? MonsterDTO())
|
|
||||||
let monsterViewController = UIHostingController(rootView: MonsterDetailView(viewModel: monsterViewModel))
|
private func presentMonsterViewController(for dto: MonsterDTO) {
|
||||||
monsterViewController.loadViewIfNeeded()
|
let monsterViewModel = MonsterImportHelper.import5ESBMonster(dto)
|
||||||
monsterViewController.view.layoutIfNeeded()
|
let hostingController = UIHostingController(rootView: MonsterDetailView(viewModel: monsterViewModel))
|
||||||
addChild(monsterViewController)
|
|
||||||
view.addSubview(monsterViewController.view)
|
// Add as child view controller
|
||||||
monsterViewController.didMove(toParent: self)
|
addChild(hostingController)
|
||||||
|
view.addSubview(hostingController.view)
|
||||||
|
|
||||||
|
// Configure layout constraints
|
||||||
|
let subview = hostingController.view!
|
||||||
|
subview.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
if let monsterView = monsterViewController.view {
|
|
||||||
monsterView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
monsterView.leftAnchor.constraint(equalTo: view.leftAnchor),
|
subview.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
monsterView.rightAnchor.constraint(equalTo: view.rightAnchor),
|
subview.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
monsterView.topAnchor.constraint(equalTo: view.topAnchor),
|
subview.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
monsterView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
subview.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||||
])
|
])
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
hostingController.didMove(toParent: self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user