diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Info.plist b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Info.plist
new file mode 100644
index 0000000..01d943f
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Info.plist
@@ -0,0 +1,42 @@
+
+
+
+
+ BuildMachineOSBuild
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ SwiftGen_SwiftGenCLI
+ CFBundleIdentifier
+ SwiftGen.SwiftGenCLI.resources
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ SwiftGen_SwiftGenCLI
+ CFBundlePackageType
+ BNDL
+ CFBundleSupportedPlatforms
+
+ MacOSX
+
+ DTCompiler
+ com.apple.compilers.llvm.clang.1_0
+ DTPlatformBuild
+ 13A233
+ DTPlatformName
+ macosx
+ DTPlatformVersion
+ 11.3
+ DTSDKBuild
+ 20E214
+ DTSDKName
+ macosx11.3
+ DTXcode
+ 1300
+ DTXcodeBuild
+ 13A233
+ LSMinimumSystemVersion
+ 10.11
+
+
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/literals-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/literals-swift4.stencil
new file mode 100644
index 0000000..af60477
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/literals-swift4.stencil
@@ -0,0 +1,43 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if palettes %}
+{% set enumName %}{{param.enumName|default:"ColorName"}}{% endset %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+#if os(macOS)
+ import AppKit
+ {% if enumName != 'NSColor' %}{{accessModifier}} enum {{enumName}} { }{% endif %}
+#elseif os(iOS) || os(tvOS) || os(watchOS)
+ import UIKit
+ {% if enumName != 'UIColor' %}{{accessModifier}} enum {{enumName}} { }{% endif %}
+#endif
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - Colors
+
+// swiftlint:disable identifier_name line_length type_body_length
+{{accessModifier}} extension {{enumName}} {
+{% macro h2f hex %}{{hex|hexToInt|int255toFloat}}{% endmacro %}
+{% macro enumBlock colors accessPrefix %}
+ {% for color in colors %}
+ /// 0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}} (r: {{color.red|hexToInt}}, g: {{color.green|hexToInt}}, b: {{color.blue|hexToInt}}, a: {{color.alpha|hexToInt}})
+ {{accessPrefix}}static let {{color.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = #colorLiteral(red: {% call h2f color.red %}, green: {% call h2f color.green %}, blue: {% call h2f color.blue %}, alpha: {% call h2f color.alpha %})
+ {% endfor %}
+{% endmacro %}
+ {% if palettes.count > 1 or param.forceFileNameEnum %}
+ {% set accessPrefix %}{{accessModifier}} {% endset %}
+ {% for palette in palettes %}
+ enum {{palette.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call enumBlock palette.colors accessPrefix %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call enumBlock palettes.first.colors "" %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length type_body_length
+{% else %}
+// No color found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/literals-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/literals-swift5.stencil
new file mode 100644
index 0000000..af60477
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/literals-swift5.stencil
@@ -0,0 +1,43 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if palettes %}
+{% set enumName %}{{param.enumName|default:"ColorName"}}{% endset %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+#if os(macOS)
+ import AppKit
+ {% if enumName != 'NSColor' %}{{accessModifier}} enum {{enumName}} { }{% endif %}
+#elseif os(iOS) || os(tvOS) || os(watchOS)
+ import UIKit
+ {% if enumName != 'UIColor' %}{{accessModifier}} enum {{enumName}} { }{% endif %}
+#endif
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - Colors
+
+// swiftlint:disable identifier_name line_length type_body_length
+{{accessModifier}} extension {{enumName}} {
+{% macro h2f hex %}{{hex|hexToInt|int255toFloat}}{% endmacro %}
+{% macro enumBlock colors accessPrefix %}
+ {% for color in colors %}
+ /// 0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}} (r: {{color.red|hexToInt}}, g: {{color.green|hexToInt}}, b: {{color.blue|hexToInt}}, a: {{color.alpha|hexToInt}})
+ {{accessPrefix}}static let {{color.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = #colorLiteral(red: {% call h2f color.red %}, green: {% call h2f color.green %}, blue: {% call h2f color.blue %}, alpha: {% call h2f color.alpha %})
+ {% endfor %}
+{% endmacro %}
+ {% if palettes.count > 1 or param.forceFileNameEnum %}
+ {% set accessPrefix %}{{accessModifier}} {% endset %}
+ {% for palette in palettes %}
+ enum {{palette.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call enumBlock palette.colors accessPrefix %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call enumBlock palettes.first.colors "" %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length type_body_length
+{% else %}
+// No color found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/swift4.stencil
new file mode 100644
index 0000000..57c2d79
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/swift4.stencil
@@ -0,0 +1,84 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if palettes %}
+{% set colorAlias %}{{param.colorAliasName|default:"Color"}}{% endset %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+#if os(macOS)
+ import AppKit.NSColor
+ {{accessModifier}} typealias {{colorAlias}} = NSColor
+#elseif os(iOS) || os(tvOS) || os(watchOS)
+ import UIKit.UIColor
+ {{accessModifier}} typealias {{colorAlias}} = UIColor
+#endif
+
+// swiftlint:disable superfluous_disable_command file_length implicit_return
+
+// MARK: - Colors
+
+// swiftlint:disable identifier_name line_length type_body_length
+{% set enumName %}{{param.enumName|default:"ColorName"}}{% endset %}
+{{accessModifier}} struct {{enumName}} {
+ {{accessModifier}} let rgbaValue: UInt32
+ {{accessModifier}} var color: {{colorAlias}} { return {{colorAlias}}(named: self) }
+
+{% macro rgbaValue color %}0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}}{% endmacro %}
+{% macro enumBlock colors %}
+ {% for color in colors %}
+ ///
+ /// Alpha: {{color.alpha|hexToInt|int255toFloat|percent}}
(0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}})
+ {{accessModifier}} static let {{color.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}(rgbaValue: {% call rgbaValue color %})
+ {% endfor %}
+{% endmacro %}
+ {% if palettes.count > 1 or param.forceFileNameEnum %}
+ {% for palette in palettes %}
+ {{accessModifier}} enum {{palette.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call enumBlock palette.colors %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call enumBlock palettes.first.colors %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length type_body_length
+
+// MARK: - Implementation Details
+
+internal extension {{colorAlias}} {
+ convenience init(rgbaValue: UInt32) {
+ let components = RGBAComponents(rgbaValue: rgbaValue).normalized
+ self.init(red: components[0], green: components[1], blue: components[2], alpha: components[3])
+ }
+}
+
+private struct RGBAComponents {
+ let rgbaValue: UInt32
+
+ private var shifts: [UInt32] {
+ [
+ rgbaValue >> 24, // red
+ rgbaValue >> 16, // green
+ rgbaValue >> 8, // blue
+ rgbaValue // alpha
+ ]
+ }
+
+ private var components: [CGFloat] {
+ shifts.map {
+ CGFloat($0 & 0xff)
+ }
+ }
+
+ var normalized: [CGFloat] {
+ components.map { $0 / 255.0 }
+ }
+}
+
+{{accessModifier}} extension {{colorAlias}} {
+ convenience init(named color: {{enumName}}) {
+ self.init(rgbaValue: color.rgbaValue)
+ }
+}
+{% else %}
+// No color found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/swift5.stencil
new file mode 100644
index 0000000..57c2d79
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/colors/swift5.stencil
@@ -0,0 +1,84 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if palettes %}
+{% set colorAlias %}{{param.colorAliasName|default:"Color"}}{% endset %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+#if os(macOS)
+ import AppKit.NSColor
+ {{accessModifier}} typealias {{colorAlias}} = NSColor
+#elseif os(iOS) || os(tvOS) || os(watchOS)
+ import UIKit.UIColor
+ {{accessModifier}} typealias {{colorAlias}} = UIColor
+#endif
+
+// swiftlint:disable superfluous_disable_command file_length implicit_return
+
+// MARK: - Colors
+
+// swiftlint:disable identifier_name line_length type_body_length
+{% set enumName %}{{param.enumName|default:"ColorName"}}{% endset %}
+{{accessModifier}} struct {{enumName}} {
+ {{accessModifier}} let rgbaValue: UInt32
+ {{accessModifier}} var color: {{colorAlias}} { return {{colorAlias}}(named: self) }
+
+{% macro rgbaValue color %}0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}}{% endmacro %}
+{% macro enumBlock colors %}
+ {% for color in colors %}
+ ///
+ /// Alpha: {{color.alpha|hexToInt|int255toFloat|percent}}
(0x{{color.red}}{{color.green}}{{color.blue}}{{color.alpha}})
+ {{accessModifier}} static let {{color.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}(rgbaValue: {% call rgbaValue color %})
+ {% endfor %}
+{% endmacro %}
+ {% if palettes.count > 1 or param.forceFileNameEnum %}
+ {% for palette in palettes %}
+ {{accessModifier}} enum {{palette.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call enumBlock palette.colors %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call enumBlock palettes.first.colors %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length type_body_length
+
+// MARK: - Implementation Details
+
+internal extension {{colorAlias}} {
+ convenience init(rgbaValue: UInt32) {
+ let components = RGBAComponents(rgbaValue: rgbaValue).normalized
+ self.init(red: components[0], green: components[1], blue: components[2], alpha: components[3])
+ }
+}
+
+private struct RGBAComponents {
+ let rgbaValue: UInt32
+
+ private var shifts: [UInt32] {
+ [
+ rgbaValue >> 24, // red
+ rgbaValue >> 16, // green
+ rgbaValue >> 8, // blue
+ rgbaValue // alpha
+ ]
+ }
+
+ private var components: [CGFloat] {
+ shifts.map {
+ CGFloat($0 & 0xff)
+ }
+ }
+
+ var normalized: [CGFloat] {
+ components.map { $0 / 255.0 }
+ }
+}
+
+{{accessModifier}} extension {{colorAlias}} {
+ convenience init(named color: {{enumName}}) {
+ self.init(rgbaValue: color.rgbaValue)
+ }
+}
+{% else %}
+// No color found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/coredata/swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/coredata/swift4.stencil
new file mode 100644
index 0000000..9832876
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/coredata/swift4.stencil
@@ -0,0 +1,211 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+// swiftlint:disable superfluous_disable_command implicit_return
+// swiftlint:disable sorted_imports
+import CoreData
+import Foundation
+{% for import in param.extraImports %}
+import {{ import }}
+{% empty %}
+{# If extraImports is a single String instead of an array, `for` considers it empty but we still have to check if there's a single String value #}
+{% if param.extraImports %}import {{ param.extraImports }}{% endif %}
+{% endfor %}
+
+// swiftlint:disable attributes file_length vertical_whitespace_closing_braces
+// swiftlint:disable identifier_name line_length type_body_length
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+
+{% for model in models %}
+{% for name, entity in model.entities %}
+{% set superclass %}{{ model.entities[entity.superEntity].className|default:"NSManagedObject" }}{% endset %}
+{% set entityClassName %}{{ entity.className|default:"NSManagedObject" }}{% endset %}
+// MARK: - {{ entity.name }}
+
+{% if not entity.shouldGenerateCode %}
+// Note: '{{ entity.name }}' has codegen enabled for Xcode, skipping code generation.
+
+{% elif entityClassName|contains:"." %}
+// Warning: '{{ entityClassName }}' cannot be a valid type name, skipping code generation.
+
+{% else %}
+{% if param.generateObjcName %}
+@objc({{ entityClassName }})
+{% endif %}
+{{ accessModifier }} class {{ entityClassName }}: {{ superclass }} {
+ {% set override %}{% if superclass != "NSManagedObject" %}override {% endif %}{% endset %}
+ {{ override }}{{ accessModifier }} class var entityName: String {
+ return "{{ entity.name }}"
+ }
+
+ {{ override }}{{ accessModifier }} class func entity(in managedObjectContext: NSManagedObjectContext) -> NSEntityDescription? {
+ return NSEntityDescription.entity(forEntityName: entityName, in: managedObjectContext)
+ }
+
+ @available(*, deprecated, renamed: "makeFetchRequest", message: "To avoid collisions with the less concrete method in `NSManagedObject`, please use `makeFetchRequest()` instead.")
+ @nonobjc {{ accessModifier }} class func fetchRequest() -> NSFetchRequest<{{ entityClassName }}> {
+ return NSFetchRequest<{{ entityClassName }}>(entityName: entityName)
+ }
+
+ @nonobjc {{ accessModifier }} class func makeFetchRequest() -> NSFetchRequest<{{ entityClassName }}> {
+ return NSFetchRequest<{{ entityClassName }}>(entityName: entityName)
+ }
+
+ // swiftlint:disable discouraged_optional_boolean discouraged_optional_collection
+ {% for attribute in entity.attributes %}
+ {% if attribute.userInfo.RawType %}
+ {% set rawType attribute.userInfo.RawType %}
+ {% set unwrapOptional attribute.userInfo.unwrapOptional %}
+ {{ accessModifier }} var {{ attribute.name }}: {{ rawType }}{% if not unwrapOptional %}?{% endif %} {
+ get {
+ let key = "{{ attribute.name }}"
+ willAccessValue(forKey: key)
+ defer { didAccessValue(forKey: key) }
+
+ {% if unwrapOptional %}
+ guard let value = primitiveValue(forKey: key) as? {{ rawType }}.RawValue,
+ let result = {{ rawType }}(rawValue: value) else {
+ fatalError("Could not convert value for key '\(key)' to type '{{ rawType }}'")
+ }
+ return result
+ {% else %}
+ guard let value = primitiveValue(forKey: key) as? {{ rawType }}.RawValue else {
+ return nil
+ }
+ return {{ rawType }}(rawValue: value)
+ {% endif %}
+ }
+ set {
+ let key = "{{ attribute.name }}"
+ willChangeValue(forKey: key)
+ defer { didChangeValue(forKey: key) }
+
+ setPrimitiveValue(newValue{% if not unwrapOptional %}?{% endif %}.rawValue, forKey: key)
+ }
+ }
+ {% elif attribute.usesScalarValueType and attribute.isOptional %}
+ {{ accessModifier }} var {{ attribute.name }}: {{ attribute.typeName }}? {
+ get {
+ let key = "{{ attribute.name }}"
+ willAccessValue(forKey: key)
+ defer { didAccessValue(forKey: key) }
+
+ return primitiveValue(forKey: key) as? {{ attribute.typeName }}
+ }
+ set {
+ let key = "{{ attribute.name }}"
+ willChangeValue(forKey: key)
+ defer { didChangeValue(forKey: key) }
+
+ setPrimitiveValue(newValue, forKey: key)
+ }
+ }
+ {% else %}
+ @NSManaged {{ accessModifier }} var {{ attribute.name }}: {{ attribute.typeName }}{% if attribute.isOptional %}?{% endif %}
+ {% endif %}
+ {% endfor %}
+ {% for relationship in entity.relationships %}
+ {% if relationship.isToMany %}
+ @NSManaged {{ accessModifier }} var {{ relationship.name }}: {% if relationship.isOrdered %}NSOrderedSet{% else %}Set<{{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}>{% endif %}{% if relationship.isOptional %}?{% endif %}
+ {% else %}
+ @NSManaged {{ accessModifier }} var {{ relationship.name }}: {{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}{% if relationship.isOptional %}?{% endif %}
+ {% endif %}
+ {% endfor %}
+ {% for fetchedProperty in entity.fetchedProperties %}
+ @NSManaged {{ accessModifier }} var {{ fetchedProperty.name }}: [{{ fetchedProperty.fetchRequest.entity }}]
+ {% endfor %}
+ // swiftlint:enable discouraged_optional_boolean discouraged_optional_collection
+}
+
+{% for relationship in entity.relationships where relationship.isToMany %}
+{% set destinationEntityClassName %}{{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}{% endset %}
+{% set collectionClassName %}{% if relationship.isOrdered %}NSOrderedSet{% else %}Set<{{ destinationEntityClassName }}>{% endif %}{% endset %}
+{% set relationshipName %}{{ relationship.name | upperFirstLetter }}{% endset %}
+// MARK: Relationship {{ relationshipName }}
+
+extension {{ entityClassName }} {
+ {% if relationship.isOrdered %}
+ @objc(insertObject:in{{ relationshipName }}AtIndex:)
+ @NSManaged public func insertInto{{ relationshipName }}(_ value: {{ destinationEntityClassName }}, at idx: Int)
+
+ @objc(removeObjectFrom{{ relationshipName }}AtIndex:)
+ @NSManaged public func removeFrom{{ relationshipName }}(at idx: Int)
+
+ @objc(insert{{ relationshipName }}:atIndexes:)
+ @NSManaged public func insertInto{{ relationshipName }}(_ values: [{{ destinationEntityClassName }}], at indexes: NSIndexSet)
+
+ @objc(remove{{ relationshipName }}AtIndexes:)
+ @NSManaged public func removeFrom{{ relationshipName }}(at indexes: NSIndexSet)
+
+ @objc(replaceObjectIn{{ relationshipName }}AtIndex:withObject:)
+ @NSManaged public func replace{{ relationshipName }}(at idx: Int, with value: {{ destinationEntityClassName }})
+
+ @objc(replace{{ relationshipName }}AtIndexes:with{{ relationshipName }}:)
+ @NSManaged public func replace{{ relationshipName }}(at indexes: NSIndexSet, with values: [{{ destinationEntityClassName }}])
+
+ {% endif %}
+ @objc(add{{ relationshipName }}Object:)
+ @NSManaged public func addTo{{ relationshipName }}(_ value: {{ destinationEntityClassName }})
+
+ @objc(remove{{ relationshipName }}Object:)
+ @NSManaged public func removeFrom{{ relationshipName }}(_ value: {{ destinationEntityClassName }})
+
+ @objc(add{{ relationshipName }}:)
+ @NSManaged public func addTo{{ relationshipName }}(_ values: {{ collectionClassName }})
+
+ @objc(remove{{ relationshipName }}:)
+ @NSManaged public func removeFrom{{ relationshipName }}(_ values: {{ collectionClassName }})
+}
+
+{% endfor %}
+{% if model.fetchRequests[entity.name].count > 0 %}
+// MARK: Fetch Requests
+
+extension {{ entityClassName }} {
+ {% for fetchRequest in model.fetchRequests[entity.name] %}
+ {% set resultTypeName %}{% filter removeNewlines:"leading" %}
+ {% if fetchRequest.resultType == "Object" %}
+ {{ entityClassName }}
+ {% elif fetchRequest.resultType == "Object ID" %}
+ NSManagedObjectID
+ {% elif fetchRequest.resultType == "Dictionary" %}
+ [String: Any]
+ {% endif %}
+ {% endfilter %}{% endset %}
+ class func fetch{{ fetchRequest.name | upperFirstLetter }}({% filter removeNewlines:"leading" %}
+ managedObjectContext: NSManagedObjectContext
+ {% for variableName, variableType in fetchRequest.substitutionVariables %}
+ , {{ variableName | lowerFirstWord }}: {{ variableType }}
+ {% endfor %}
+ {% endfilter %}) throws -> [{{ resultTypeName }}] {
+ guard let persistentStoreCoordinator = managedObjectContext.persistentStoreCoordinator else {
+ fatalError("Managed object context has no persistent store coordinator for getting fetch request templates")
+ }
+ let model = persistentStoreCoordinator.managedObjectModel
+ let substitutionVariables: [String: Any] = [
+ {% for variableName, variableType in fetchRequest.substitutionVariables %}
+ "{{ variableName }}": {{ variableName | lowerFirstWord }}{{ "," if not forloop.last }}
+ {% empty %}
+ :
+ {% endfor %}
+ ]
+
+ guard let fetchRequest = model.fetchRequestFromTemplate(withName: "{{ fetchRequest.name }}", substitutionVariables: substitutionVariables) else {
+ fatalError("No fetch request template named '{{ fetchRequest.name }}' found.")
+ }
+
+ guard let result = try managedObjectContext.fetch(fetchRequest) as? [{{ resultTypeName }}] else {
+ fatalError("Unable to cast fetch result to correct result type.")
+ }
+
+ return result
+ }
+
+ {% endfor %}
+}
+
+{% endif %}
+{% endif %}
+{% endfor %}
+{% endfor %}
+// swiftlint:enable identifier_name line_length type_body_length
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/coredata/swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/coredata/swift5.stencil
new file mode 100644
index 0000000..9832876
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/coredata/swift5.stencil
@@ -0,0 +1,211 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+// swiftlint:disable superfluous_disable_command implicit_return
+// swiftlint:disable sorted_imports
+import CoreData
+import Foundation
+{% for import in param.extraImports %}
+import {{ import }}
+{% empty %}
+{# If extraImports is a single String instead of an array, `for` considers it empty but we still have to check if there's a single String value #}
+{% if param.extraImports %}import {{ param.extraImports }}{% endif %}
+{% endfor %}
+
+// swiftlint:disable attributes file_length vertical_whitespace_closing_braces
+// swiftlint:disable identifier_name line_length type_body_length
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+
+{% for model in models %}
+{% for name, entity in model.entities %}
+{% set superclass %}{{ model.entities[entity.superEntity].className|default:"NSManagedObject" }}{% endset %}
+{% set entityClassName %}{{ entity.className|default:"NSManagedObject" }}{% endset %}
+// MARK: - {{ entity.name }}
+
+{% if not entity.shouldGenerateCode %}
+// Note: '{{ entity.name }}' has codegen enabled for Xcode, skipping code generation.
+
+{% elif entityClassName|contains:"." %}
+// Warning: '{{ entityClassName }}' cannot be a valid type name, skipping code generation.
+
+{% else %}
+{% if param.generateObjcName %}
+@objc({{ entityClassName }})
+{% endif %}
+{{ accessModifier }} class {{ entityClassName }}: {{ superclass }} {
+ {% set override %}{% if superclass != "NSManagedObject" %}override {% endif %}{% endset %}
+ {{ override }}{{ accessModifier }} class var entityName: String {
+ return "{{ entity.name }}"
+ }
+
+ {{ override }}{{ accessModifier }} class func entity(in managedObjectContext: NSManagedObjectContext) -> NSEntityDescription? {
+ return NSEntityDescription.entity(forEntityName: entityName, in: managedObjectContext)
+ }
+
+ @available(*, deprecated, renamed: "makeFetchRequest", message: "To avoid collisions with the less concrete method in `NSManagedObject`, please use `makeFetchRequest()` instead.")
+ @nonobjc {{ accessModifier }} class func fetchRequest() -> NSFetchRequest<{{ entityClassName }}> {
+ return NSFetchRequest<{{ entityClassName }}>(entityName: entityName)
+ }
+
+ @nonobjc {{ accessModifier }} class func makeFetchRequest() -> NSFetchRequest<{{ entityClassName }}> {
+ return NSFetchRequest<{{ entityClassName }}>(entityName: entityName)
+ }
+
+ // swiftlint:disable discouraged_optional_boolean discouraged_optional_collection
+ {% for attribute in entity.attributes %}
+ {% if attribute.userInfo.RawType %}
+ {% set rawType attribute.userInfo.RawType %}
+ {% set unwrapOptional attribute.userInfo.unwrapOptional %}
+ {{ accessModifier }} var {{ attribute.name }}: {{ rawType }}{% if not unwrapOptional %}?{% endif %} {
+ get {
+ let key = "{{ attribute.name }}"
+ willAccessValue(forKey: key)
+ defer { didAccessValue(forKey: key) }
+
+ {% if unwrapOptional %}
+ guard let value = primitiveValue(forKey: key) as? {{ rawType }}.RawValue,
+ let result = {{ rawType }}(rawValue: value) else {
+ fatalError("Could not convert value for key '\(key)' to type '{{ rawType }}'")
+ }
+ return result
+ {% else %}
+ guard let value = primitiveValue(forKey: key) as? {{ rawType }}.RawValue else {
+ return nil
+ }
+ return {{ rawType }}(rawValue: value)
+ {% endif %}
+ }
+ set {
+ let key = "{{ attribute.name }}"
+ willChangeValue(forKey: key)
+ defer { didChangeValue(forKey: key) }
+
+ setPrimitiveValue(newValue{% if not unwrapOptional %}?{% endif %}.rawValue, forKey: key)
+ }
+ }
+ {% elif attribute.usesScalarValueType and attribute.isOptional %}
+ {{ accessModifier }} var {{ attribute.name }}: {{ attribute.typeName }}? {
+ get {
+ let key = "{{ attribute.name }}"
+ willAccessValue(forKey: key)
+ defer { didAccessValue(forKey: key) }
+
+ return primitiveValue(forKey: key) as? {{ attribute.typeName }}
+ }
+ set {
+ let key = "{{ attribute.name }}"
+ willChangeValue(forKey: key)
+ defer { didChangeValue(forKey: key) }
+
+ setPrimitiveValue(newValue, forKey: key)
+ }
+ }
+ {% else %}
+ @NSManaged {{ accessModifier }} var {{ attribute.name }}: {{ attribute.typeName }}{% if attribute.isOptional %}?{% endif %}
+ {% endif %}
+ {% endfor %}
+ {% for relationship in entity.relationships %}
+ {% if relationship.isToMany %}
+ @NSManaged {{ accessModifier }} var {{ relationship.name }}: {% if relationship.isOrdered %}NSOrderedSet{% else %}Set<{{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}>{% endif %}{% if relationship.isOptional %}?{% endif %}
+ {% else %}
+ @NSManaged {{ accessModifier }} var {{ relationship.name }}: {{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}{% if relationship.isOptional %}?{% endif %}
+ {% endif %}
+ {% endfor %}
+ {% for fetchedProperty in entity.fetchedProperties %}
+ @NSManaged {{ accessModifier }} var {{ fetchedProperty.name }}: [{{ fetchedProperty.fetchRequest.entity }}]
+ {% endfor %}
+ // swiftlint:enable discouraged_optional_boolean discouraged_optional_collection
+}
+
+{% for relationship in entity.relationships where relationship.isToMany %}
+{% set destinationEntityClassName %}{{ model.entities[relationship.destinationEntity].className|default:"NSManagedObject" }}{% endset %}
+{% set collectionClassName %}{% if relationship.isOrdered %}NSOrderedSet{% else %}Set<{{ destinationEntityClassName }}>{% endif %}{% endset %}
+{% set relationshipName %}{{ relationship.name | upperFirstLetter }}{% endset %}
+// MARK: Relationship {{ relationshipName }}
+
+extension {{ entityClassName }} {
+ {% if relationship.isOrdered %}
+ @objc(insertObject:in{{ relationshipName }}AtIndex:)
+ @NSManaged public func insertInto{{ relationshipName }}(_ value: {{ destinationEntityClassName }}, at idx: Int)
+
+ @objc(removeObjectFrom{{ relationshipName }}AtIndex:)
+ @NSManaged public func removeFrom{{ relationshipName }}(at idx: Int)
+
+ @objc(insert{{ relationshipName }}:atIndexes:)
+ @NSManaged public func insertInto{{ relationshipName }}(_ values: [{{ destinationEntityClassName }}], at indexes: NSIndexSet)
+
+ @objc(remove{{ relationshipName }}AtIndexes:)
+ @NSManaged public func removeFrom{{ relationshipName }}(at indexes: NSIndexSet)
+
+ @objc(replaceObjectIn{{ relationshipName }}AtIndex:withObject:)
+ @NSManaged public func replace{{ relationshipName }}(at idx: Int, with value: {{ destinationEntityClassName }})
+
+ @objc(replace{{ relationshipName }}AtIndexes:with{{ relationshipName }}:)
+ @NSManaged public func replace{{ relationshipName }}(at indexes: NSIndexSet, with values: [{{ destinationEntityClassName }}])
+
+ {% endif %}
+ @objc(add{{ relationshipName }}Object:)
+ @NSManaged public func addTo{{ relationshipName }}(_ value: {{ destinationEntityClassName }})
+
+ @objc(remove{{ relationshipName }}Object:)
+ @NSManaged public func removeFrom{{ relationshipName }}(_ value: {{ destinationEntityClassName }})
+
+ @objc(add{{ relationshipName }}:)
+ @NSManaged public func addTo{{ relationshipName }}(_ values: {{ collectionClassName }})
+
+ @objc(remove{{ relationshipName }}:)
+ @NSManaged public func removeFrom{{ relationshipName }}(_ values: {{ collectionClassName }})
+}
+
+{% endfor %}
+{% if model.fetchRequests[entity.name].count > 0 %}
+// MARK: Fetch Requests
+
+extension {{ entityClassName }} {
+ {% for fetchRequest in model.fetchRequests[entity.name] %}
+ {% set resultTypeName %}{% filter removeNewlines:"leading" %}
+ {% if fetchRequest.resultType == "Object" %}
+ {{ entityClassName }}
+ {% elif fetchRequest.resultType == "Object ID" %}
+ NSManagedObjectID
+ {% elif fetchRequest.resultType == "Dictionary" %}
+ [String: Any]
+ {% endif %}
+ {% endfilter %}{% endset %}
+ class func fetch{{ fetchRequest.name | upperFirstLetter }}({% filter removeNewlines:"leading" %}
+ managedObjectContext: NSManagedObjectContext
+ {% for variableName, variableType in fetchRequest.substitutionVariables %}
+ , {{ variableName | lowerFirstWord }}: {{ variableType }}
+ {% endfor %}
+ {% endfilter %}) throws -> [{{ resultTypeName }}] {
+ guard let persistentStoreCoordinator = managedObjectContext.persistentStoreCoordinator else {
+ fatalError("Managed object context has no persistent store coordinator for getting fetch request templates")
+ }
+ let model = persistentStoreCoordinator.managedObjectModel
+ let substitutionVariables: [String: Any] = [
+ {% for variableName, variableType in fetchRequest.substitutionVariables %}
+ "{{ variableName }}": {{ variableName | lowerFirstWord }}{{ "," if not forloop.last }}
+ {% empty %}
+ :
+ {% endfor %}
+ ]
+
+ guard let fetchRequest = model.fetchRequestFromTemplate(withName: "{{ fetchRequest.name }}", substitutionVariables: substitutionVariables) else {
+ fatalError("No fetch request template named '{{ fetchRequest.name }}' found.")
+ }
+
+ guard let result = try managedObjectContext.fetch(fetchRequest) as? [{{ resultTypeName }}] else {
+ fatalError("Unable to cast fetch result to correct result type.")
+ }
+
+ return result
+ }
+
+ {% endfor %}
+}
+
+{% endif %}
+{% endif %}
+{% endfor %}
+{% endfor %}
+// swiftlint:enable identifier_name line_length type_body_length
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/flat-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/flat-swift4.stencil
new file mode 100644
index 0000000..09df24d
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/flat-swift4.stencil
@@ -0,0 +1,103 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if groups.count > 0 %}
+{% set enumName %}{{param.enumName|default:"Files"}}{% endset %}
+{% set useExt %}{% if param.useExtension|default:"true" %}true{% endif %}{% endset %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set resourceType %}{{param.resourceTypeName|default:"File"}}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command file_length line_length implicit_return
+
+// MARK: - Files
+
+{% macro groupBlock group %}
+ {% for file in group.files %}
+ {% call fileBlock file %}
+ {% endfor %}
+ {% for dir in group.directories %}
+ {% call dirBlock dir %}
+ {% endfor %}
+{% endmacro %}
+{% macro fileBlock file %}
+ /// {% if file.path and param.preservePath %}{{file.path}}/{% endif %}{{file.name}}{% if file.ext %}.{{file.ext}}{% endif %}
+ {% set identifier %}{{ file.name }}{% if useExt %}.{{ file.ext }}{% endif %}{% endset %}
+ {{accessModifier}} static let {{identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{resourceType}}(name: "{{file.name}}", ext: {% if file.ext %}"{{file.ext}}"{% else %}nil{% endif %}, relativePath: "{{file.path if param.preservePath}}", mimeType: "{{file.mimeType}}")
+{% endmacro %}
+{% macro dirBlock directory %}
+ {% for file in directory.files %}
+ {% call fileBlock file %}
+ {% endfor %}
+ {% for dir in directory.directories %}
+ {% call dirBlock dir %}
+ {% endfor %}
+{% endmacro %}
+// swiftlint:disable explicit_type_interface identifier_name
+// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
+{{accessModifier}} enum {{enumName}} {
+ {% if groups.count > 1 or param.forceFileNameEnum %}
+ {% for group in groups %}
+ {{accessModifier}} enum {{group.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call groupBlock group %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call groupBlock groups.first %}
+ {% endif %}
+}
+// swiftlint:enable explicit_type_interface identifier_name
+// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
+
+// MARK: - Implementation Details
+
+{{accessModifier}} struct {{resourceType}} {
+ {{accessModifier}} let name: String
+ {{accessModifier}} let ext: String?
+ {{accessModifier}} let relativePath: String
+ {{accessModifier}} let mimeType: String
+
+ {{accessModifier}} var url: URL {
+ return url(locale: nil)
+ }
+
+ {{accessModifier}} func url(locale: Locale?) -> URL {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ let url = bundle.url(
+ forResource: name,
+ withExtension: ext,
+ subdirectory: relativePath,
+ localization: locale?.identifier
+ )
+ guard let result = url else {
+ let file = name + (ext.flatMap { ".\($0)" } ?? "")
+ fatalError("Could not locate file named \(file)")
+ }
+ return result
+ }
+
+ {{accessModifier}} var path: String {
+ return path(locale: nil)
+ }
+
+ {{accessModifier}} func path(locale: Locale?) -> String {
+ return url(locale: locale).path
+ }
+}
+{% if not param.bundle %}
+
+// swiftlint:disable convenience_type explicit_type_interface
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type explicit_type_interface
+{% endif %}
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/flat-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/flat-swift5.stencil
new file mode 100644
index 0000000..09df24d
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/flat-swift5.stencil
@@ -0,0 +1,103 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if groups.count > 0 %}
+{% set enumName %}{{param.enumName|default:"Files"}}{% endset %}
+{% set useExt %}{% if param.useExtension|default:"true" %}true{% endif %}{% endset %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set resourceType %}{{param.resourceTypeName|default:"File"}}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command file_length line_length implicit_return
+
+// MARK: - Files
+
+{% macro groupBlock group %}
+ {% for file in group.files %}
+ {% call fileBlock file %}
+ {% endfor %}
+ {% for dir in group.directories %}
+ {% call dirBlock dir %}
+ {% endfor %}
+{% endmacro %}
+{% macro fileBlock file %}
+ /// {% if file.path and param.preservePath %}{{file.path}}/{% endif %}{{file.name}}{% if file.ext %}.{{file.ext}}{% endif %}
+ {% set identifier %}{{ file.name }}{% if useExt %}.{{ file.ext }}{% endif %}{% endset %}
+ {{accessModifier}} static let {{identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{resourceType}}(name: "{{file.name}}", ext: {% if file.ext %}"{{file.ext}}"{% else %}nil{% endif %}, relativePath: "{{file.path if param.preservePath}}", mimeType: "{{file.mimeType}}")
+{% endmacro %}
+{% macro dirBlock directory %}
+ {% for file in directory.files %}
+ {% call fileBlock file %}
+ {% endfor %}
+ {% for dir in directory.directories %}
+ {% call dirBlock dir %}
+ {% endfor %}
+{% endmacro %}
+// swiftlint:disable explicit_type_interface identifier_name
+// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
+{{accessModifier}} enum {{enumName}} {
+ {% if groups.count > 1 or param.forceFileNameEnum %}
+ {% for group in groups %}
+ {{accessModifier}} enum {{group.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call groupBlock group %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call groupBlock groups.first %}
+ {% endif %}
+}
+// swiftlint:enable explicit_type_interface identifier_name
+// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
+
+// MARK: - Implementation Details
+
+{{accessModifier}} struct {{resourceType}} {
+ {{accessModifier}} let name: String
+ {{accessModifier}} let ext: String?
+ {{accessModifier}} let relativePath: String
+ {{accessModifier}} let mimeType: String
+
+ {{accessModifier}} var url: URL {
+ return url(locale: nil)
+ }
+
+ {{accessModifier}} func url(locale: Locale?) -> URL {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ let url = bundle.url(
+ forResource: name,
+ withExtension: ext,
+ subdirectory: relativePath,
+ localization: locale?.identifier
+ )
+ guard let result = url else {
+ let file = name + (ext.flatMap { ".\($0)" } ?? "")
+ fatalError("Could not locate file named \(file)")
+ }
+ return result
+ }
+
+ {{accessModifier}} var path: String {
+ return path(locale: nil)
+ }
+
+ {{accessModifier}} func path(locale: Locale?) -> String {
+ return url(locale: locale).path
+ }
+}
+{% if not param.bundle %}
+
+// swiftlint:disable convenience_type explicit_type_interface
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type explicit_type_interface
+{% endif %}
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/structured-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/structured-swift4.stencil
new file mode 100644
index 0000000..6d6db96
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/structured-swift4.stencil
@@ -0,0 +1,107 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if groups.count > 0 %}
+{% set enumName %}{{param.enumName|default:"Files"}}{% endset %}
+{% set useExt %}{% if param.useExtension|default:"true" %}true{% endif %}{% endset %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set resourceType %}{{param.resourceTypeName|default:"File"}}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command file_length line_length implicit_return
+
+// MARK: - Files
+
+{% macro groupBlock group %}
+ {% for file in group.files %}
+ {% call fileBlock file %}
+ {% endfor %}
+ {% for dir in group.directories %}
+ {% call dirBlock dir "" %}
+ {% endfor %}
+{% endmacro %}
+{% macro fileBlock file %}
+ /// {% if file.path and param.preservePath %}{{file.path}}/{% endif %}{{file.name}}{% if file.ext %}.{{file.ext}}{% endif %}
+ {% set identifier %}{{ file.name }}{% if useExt %}.{{ file.ext }}{% endif %}{% endset %}
+ {{accessModifier}} static let {{identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{resourceType}}(name: "{{file.name}}", ext: {% if file.ext %}"{{file.ext}}"{% else %}nil{% endif %}, relativePath: "{{file.path if param.preservePath}}", mimeType: "{{file.mimeType}}")
+{% endmacro %}
+{% macro dirBlock directory parent %}
+ {% set fullDir %}{{parent}}{{directory.name}}/{% endset %}
+ /// {{ fullDir }}
+ {{accessModifier}} enum {{directory.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% for file in directory.files %}
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ {% endfor %}
+ {% for dir in directory.directories %}
+ {% filter indent:2 %}{% call dirBlock dir fullDir %}{% endfilter %}
+ {% endfor %}
+ }
+{% endmacro %}
+// swiftlint:disable explicit_type_interface identifier_name
+// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
+{{accessModifier}} enum {{enumName}} {
+ {% if groups.count > 1 or param.forceFileNameEnum %}
+ {% for group in groups %}
+ {{accessModifier}} enum {{group.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call groupBlock group %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call groupBlock groups.first %}
+ {% endif %}
+}
+// swiftlint:enable explicit_type_interface identifier_name
+// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
+
+// MARK: - Implementation Details
+
+{{accessModifier}} struct {{resourceType}} {
+ {{accessModifier}} let name: String
+ {{accessModifier}} let ext: String?
+ {{accessModifier}} let relativePath: String
+ {{accessModifier}} let mimeType: String
+
+ {{accessModifier}} var url: URL {
+ return url(locale: nil)
+ }
+
+ {{accessModifier}} func url(locale: Locale?) -> URL {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ let url = bundle.url(
+ forResource: name,
+ withExtension: ext,
+ subdirectory: relativePath,
+ localization: locale?.identifier
+ )
+ guard let result = url else {
+ let file = name + (ext.flatMap { ".\($0)" } ?? "")
+ fatalError("Could not locate file named \(file)")
+ }
+ return result
+ }
+
+ {{accessModifier}} var path: String {
+ return path(locale: nil)
+ }
+
+ {{accessModifier}} func path(locale: Locale?) -> String {
+ return url(locale: locale).path
+ }
+}
+{% if not param.bundle %}
+
+// swiftlint:disable convenience_type explicit_type_interface
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type explicit_type_interface
+{% endif %}
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/structured-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/structured-swift5.stencil
new file mode 100644
index 0000000..6d6db96
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/files/structured-swift5.stencil
@@ -0,0 +1,107 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if groups.count > 0 %}
+{% set enumName %}{{param.enumName|default:"Files"}}{% endset %}
+{% set useExt %}{% if param.useExtension|default:"true" %}true{% endif %}{% endset %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set resourceType %}{{param.resourceTypeName|default:"File"}}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command file_length line_length implicit_return
+
+// MARK: - Files
+
+{% macro groupBlock group %}
+ {% for file in group.files %}
+ {% call fileBlock file %}
+ {% endfor %}
+ {% for dir in group.directories %}
+ {% call dirBlock dir "" %}
+ {% endfor %}
+{% endmacro %}
+{% macro fileBlock file %}
+ /// {% if file.path and param.preservePath %}{{file.path}}/{% endif %}{{file.name}}{% if file.ext %}.{{file.ext}}{% endif %}
+ {% set identifier %}{{ file.name }}{% if useExt %}.{{ file.ext }}{% endif %}{% endset %}
+ {{accessModifier}} static let {{identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{resourceType}}(name: "{{file.name}}", ext: {% if file.ext %}"{{file.ext}}"{% else %}nil{% endif %}, relativePath: "{{file.path if param.preservePath}}", mimeType: "{{file.mimeType}}")
+{% endmacro %}
+{% macro dirBlock directory parent %}
+ {% set fullDir %}{{parent}}{{directory.name}}/{% endset %}
+ /// {{ fullDir }}
+ {{accessModifier}} enum {{directory.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% for file in directory.files %}
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ {% endfor %}
+ {% for dir in directory.directories %}
+ {% filter indent:2 %}{% call dirBlock dir fullDir %}{% endfilter %}
+ {% endfor %}
+ }
+{% endmacro %}
+// swiftlint:disable explicit_type_interface identifier_name
+// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
+{{accessModifier}} enum {{enumName}} {
+ {% if groups.count > 1 or param.forceFileNameEnum %}
+ {% for group in groups %}
+ {{accessModifier}} enum {{group.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call groupBlock group %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call groupBlock groups.first %}
+ {% endif %}
+}
+// swiftlint:enable explicit_type_interface identifier_name
+// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
+
+// MARK: - Implementation Details
+
+{{accessModifier}} struct {{resourceType}} {
+ {{accessModifier}} let name: String
+ {{accessModifier}} let ext: String?
+ {{accessModifier}} let relativePath: String
+ {{accessModifier}} let mimeType: String
+
+ {{accessModifier}} var url: URL {
+ return url(locale: nil)
+ }
+
+ {{accessModifier}} func url(locale: Locale?) -> URL {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ let url = bundle.url(
+ forResource: name,
+ withExtension: ext,
+ subdirectory: relativePath,
+ localization: locale?.identifier
+ )
+ guard let result = url else {
+ let file = name + (ext.flatMap { ".\($0)" } ?? "")
+ fatalError("Could not locate file named \(file)")
+ }
+ return result
+ }
+
+ {{accessModifier}} var path: String {
+ return path(locale: nil)
+ }
+
+ {{accessModifier}} func path(locale: Locale?) -> String {
+ return url(locale: locale).path
+ }
+}
+{% if not param.bundle %}
+
+// swiftlint:disable convenience_type explicit_type_interface
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type explicit_type_interface
+{% endif %}
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/fonts/swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/fonts/swift4.stencil
new file mode 100644
index 0000000..744d6a4
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/fonts/swift4.stencil
@@ -0,0 +1,110 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if families %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set fontType %}{{param.fontTypeName|default:"FontConvertible"}}{% endset %}
+#if os(macOS)
+ import AppKit.NSFont
+#elseif os(iOS) || os(tvOS) || os(watchOS)
+ import UIKit.UIFont
+#endif
+
+// Deprecated typealiases
+@available(*, deprecated, renamed: "{{fontType}}.Font", message: "This typealias will be removed in SwiftGen 7.0")
+{{accessModifier}} typealias {{param.fontAliasName|default:"Font"}} = {{fontType}}.Font
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+// swiftlint:disable implicit_return
+
+// MARK: - Fonts
+
+// swiftlint:disable identifier_name line_length type_body_length
+{% macro transformPath path %}{% filter removeNewlines %}
+ {% if param.preservePath %}
+ {{path}}
+ {% else %}
+ {{path|basename}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{{accessModifier}} enum {{param.enumName|default:"FontFamily"}} {
+ {% for family in families %}
+ {{accessModifier}} enum {{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% for font in family.fonts %}
+ {{accessModifier}} static let {{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{fontType}}(name: "{{font.name}}", family: "{{family.name}}", path: "{% call transformPath font.path %}")
+ {% endfor %}
+ {{accessModifier}} static let all: [{{fontType}}] = [{% for font in family.fonts %}{{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{{ ", " if not forloop.last }}{% endfor %}]
+ }
+ {% endfor %}
+ {{accessModifier}} static let allCustomFonts: [{{fontType}}] = [{% for family in families %}{{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.all{{ ", " if not forloop.last }}{% endfor %}].flatMap { $0 }
+ {{accessModifier}} static func registerAllCustomFonts() {
+ allCustomFonts.forEach { $0.register() }
+ }
+}
+// swiftlint:enable identifier_name line_length type_body_length
+
+// MARK: - Implementation Details
+
+{{accessModifier}} struct {{fontType}} {
+ {{accessModifier}} let name: String
+ {{accessModifier}} let family: String
+ {{accessModifier}} let path: String
+
+ #if os(macOS)
+ {{accessModifier}} typealias Font = NSFont
+ #elseif os(iOS) || os(tvOS) || os(watchOS)
+ {{accessModifier}} typealias Font = UIFont
+ #endif
+
+ {{accessModifier}} func font(size: CGFloat) -> Font! {
+ return Font(font: self, size: size)
+ }
+
+ {{accessModifier}} func register() {
+ // swiftlint:disable:next conditional_returns_on_newline
+ guard let url = url else { return }
+ CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
+ }
+
+ fileprivate var url: URL? {
+ {% if param.lookupFunction %}
+ return {{param.lookupFunction}}(name, family, path)
+ {% else %}
+ return {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil)
+ {% endif %}
+ }
+}
+
+{{accessModifier}} extension {{fontType}}.Font {
+ convenience init?(font: {{fontType}}, size: CGFloat) {
+ #if os(iOS) || os(tvOS) || os(watchOS)
+ if !UIFont.fontNames(forFamilyName: font.family).contains(font.name) {
+ font.register()
+ }
+ #elseif os(macOS)
+ if let url = font.url, CTFontManagerGetScopeForURL(url as CFURL) == .none {
+ font.register()
+ }
+ #endif
+
+ self.init(name: font.name, size: size)
+ }
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No fonts found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/fonts/swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/fonts/swift5.stencil
new file mode 100644
index 0000000..5a268b5
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/fonts/swift5.stencil
@@ -0,0 +1,113 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if families %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set fontType %}{{param.fontTypeName|default:"FontConvertible"}}{% endset %}
+#if os(macOS)
+ import AppKit.NSFont
+#elseif os(iOS) || os(tvOS) || os(watchOS)
+ import UIKit.UIFont
+#endif
+
+// Deprecated typealiases
+@available(*, deprecated, renamed: "{{fontType}}.Font", message: "This typealias will be removed in SwiftGen 7.0")
+{{accessModifier}} typealias {{param.fontAliasName|default:"Font"}} = {{fontType}}.Font
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - Fonts
+
+// swiftlint:disable identifier_name line_length type_body_length
+{% macro transformPath path %}{% filter removeNewlines %}
+ {% if param.preservePath %}
+ {{path}}
+ {% else %}
+ {{path|basename}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{{accessModifier}} enum {{param.enumName|default:"FontFamily"}} {
+ {% for family in families %}
+ {{accessModifier}} enum {{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% for font in family.fonts %}
+ {{accessModifier}} static let {{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{fontType}}(name: "{{font.name}}", family: "{{family.name}}", path: "{% call transformPath font.path %}")
+ {% endfor %}
+ {{accessModifier}} static let all: [{{fontType}}] = [{% for font in family.fonts %}{{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{{ ", " if not forloop.last }}{% endfor %}]
+ }
+ {% endfor %}
+ {{accessModifier}} static let allCustomFonts: [{{fontType}}] = [{% for family in families %}{{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.all{{ ", " if not forloop.last }}{% endfor %}].flatMap { $0 }
+ {{accessModifier}} static func registerAllCustomFonts() {
+ allCustomFonts.forEach { $0.register() }
+ }
+}
+// swiftlint:enable identifier_name line_length type_body_length
+
+// MARK: - Implementation Details
+
+{{accessModifier}} struct {{fontType}} {
+ {{accessModifier}} let name: String
+ {{accessModifier}} let family: String
+ {{accessModifier}} let path: String
+
+ #if os(macOS)
+ {{accessModifier}} typealias Font = NSFont
+ #elseif os(iOS) || os(tvOS) || os(watchOS)
+ {{accessModifier}} typealias Font = UIFont
+ #endif
+
+ {{accessModifier}} func font(size: CGFloat) -> Font {
+ guard let font = Font(font: self, size: size) else {
+ fatalError("Unable to initialize font '\(name)' (\(family))")
+ }
+ return font
+ }
+
+ {{accessModifier}} func register() {
+ // swiftlint:disable:next conditional_returns_on_newline
+ guard let url = url else { return }
+ CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
+ }
+
+ fileprivate var url: URL? {
+ // swiftlint:disable:next implicit_return
+ {% if param.lookupFunction %}
+ return {{param.lookupFunction}}(name, family, path)
+ {% else %}
+ return {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil)
+ {% endif %}
+ }
+}
+
+{{accessModifier}} extension {{fontType}}.Font {
+ convenience init?(font: {{fontType}}, size: CGFloat) {
+ #if os(iOS) || os(tvOS) || os(watchOS)
+ if !UIFont.fontNames(forFamilyName: font.family).contains(font.name) {
+ font.register()
+ }
+ #elseif os(macOS)
+ if let url = font.url, CTFontManagerGetScopeForURL(url as CFURL) == .none {
+ font.register()
+ }
+ #endif
+
+ self.init(name: font.name, size: size)
+ }
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No fonts found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/scenes-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/scenes-swift4.stencil
new file mode 100644
index 0000000..9ad52ff
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/scenes-swift4.stencil
@@ -0,0 +1,157 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if platform and storyboards %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set isAppKit %}{% if platform == "macOS" %}true{% endif %}{% endset %}
+{% set prefix %}{% if isAppKit %}NS{% else %}UI{% endif %}{% endset %}
+{% set controller %}{% if isAppKit %}Controller{% else %}ViewController{% endif %}{% endset %}
+// swiftlint:disable sorted_imports
+import Foundation
+{% for module in modules where module != env.PRODUCT_MODULE_NAME and module != param.module %}
+import {{module}}
+{% endfor %}
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length implicit_return
+
+// MARK: - Storyboard Scenes
+
+// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
+{% macro moduleName item %}{% filter removeNewlines %}
+ {% if item.moduleIsPlaceholder %}
+ {{ env.PRODUCT_MODULE_NAME|default:param.module }}
+ {% else %}
+ {{ item.module }}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro className item %}{% filter removeNewlines %}
+ {% set module %}{% call moduleName item %}{% endset %}
+ {% if module and ( not param.ignoreTargetModule or module != env.PRODUCT_MODULE_NAME and module != param.module ) %}
+ {{module}}.
+ {% endif %}
+ {{item.type}}
+{% endfilter %}{% endmacro %}
+{{accessModifier}} enum {{param.enumName|default:"StoryboardScene"}} {
+ {% for storyboard in storyboards %}
+ {% set storyboardName %}{{storyboard.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}{% endset %}
+ {{accessModifier}} enum {{storyboardName}}: StoryboardType {
+ {{accessModifier}} static let storyboardName = "{{storyboard.name}}"
+ {% if storyboard.initialScene %}
+
+ {% set sceneClass %}{% call className storyboard.initialScene %}{% endset %}
+ {{accessModifier}} static let initialScene = InitialSceneType<{{sceneClass}}>(storyboard: {{storyboardName}}.self)
+ {% endif %}
+ {% for scene in storyboard.scenes %}
+
+ {% set sceneID %}{{scene.identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set sceneClass %}{% call className scene %}{% endset %}
+ {{accessModifier}} static let {{sceneID}} = SceneType<{{sceneClass}}>(storyboard: {{storyboardName}}.self, identifier: "{{scene.identifier}}")
+ {% endfor %}
+ }
+ {% endfor %}
+}
+// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name
+
+// MARK: - Implementation Details
+
+{{accessModifier}} protocol StoryboardType {
+ static var storyboardName: String { get }
+}
+
+{{accessModifier}} extension StoryboardType {
+ static var storyboard: {{prefix}}Storyboard {
+ let name = {% if isAppKit %}NSStoryboard.Name({% endif %}self.storyboardName{% if isAppKit %}){% endif %}
+ {% if param.lookupFunction %}
+ return {{param.lookupFunction}}(name)
+ {% else %}
+ return {{prefix}}Storyboard(name: name, bundle: {{param.bundle|default:"BundleToken.bundle"}})
+ {% endif %}
+ }
+}
+
+{{accessModifier}} struct SceneType {
+ {{accessModifier}} let storyboard: StoryboardType.Type
+ {{accessModifier}} let identifier: String
+
+ {{accessModifier}} func instantiate() -> T {
+ let identifier = {% if isAppKit %}NSStoryboard.SceneIdentifier({% endif %}self.identifier{% if isAppKit %}){% endif %}
+ guard let controller = storyboard.storyboard.instantiate{{controller}}(withIdentifier: identifier) as? T else {
+ fatalError("{{controller}} '\(identifier)' is not of the expected class \(T.self).")
+ }
+ return controller
+ }
+
+ {% if isAppKit %}
+ @available(macOS 10.15, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSViewController {
+ return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
+ }
+
+ @available(macOS 10.15, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSWindowController {
+ return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
+ }
+ {% else %}
+ @available(iOS 13.0, tvOS 13.0, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T {
+ return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
+ }
+ {% endif %}
+}
+
+{{accessModifier}} struct InitialSceneType {
+ {{accessModifier}} let storyboard: StoryboardType.Type
+
+ {{accessModifier}} func instantiate() -> T {
+ guard let controller = storyboard.storyboard.instantiateInitial{{controller}}() as? T else {
+ fatalError("{{controller}} is not of the expected class \(T.self).")
+ }
+ return controller
+ }
+
+ {% if isAppKit %}
+ @available(macOS 10.15, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSViewController {
+ guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
+ fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
+ }
+ return controller
+ }
+
+ @available(macOS 10.15, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSWindowController {
+ guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
+ fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
+ }
+ return controller
+ }
+ {% else %}
+ @available(iOS 13.0, tvOS 13.0, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T {
+ guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
+ fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
+ }
+ return controller
+ }
+ {% endif %}
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% elif storyboards %}
+// Mixed AppKit and UIKit storyboard files found, please invoke swiftgen with these separately
+{% else %}
+// No storyboard found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/scenes-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/scenes-swift5.stencil
new file mode 100644
index 0000000..5f29f8b
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/scenes-swift5.stencil
@@ -0,0 +1,159 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if platform and storyboards %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set isAppKit %}{% if platform == "macOS" %}true{% endif %}{% endset %}
+{% set prefix %}{% if isAppKit %}NS{% else %}UI{% endif %}{% endset %}
+{% set controller %}{% if isAppKit %}Controller{% else %}ViewController{% endif %}{% endset %}
+// swiftlint:disable sorted_imports
+import Foundation
+{% for module in modules where module != env.PRODUCT_MODULE_NAME and module != param.module %}
+import {{module}}
+{% endfor %}
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length implicit_return
+
+// MARK: - Storyboard Scenes
+
+// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
+{% macro moduleName item %}{% filter removeNewlines %}
+ {% if item.moduleIsPlaceholder %}
+ {{ env.PRODUCT_MODULE_NAME|default:param.module }}
+ {% else %}
+ {{ item.module }}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro className item %}{% filter removeNewlines %}
+ {% set module %}{% call moduleName item %}{% endset %}
+ {% if module and ( not param.ignoreTargetModule or module != env.PRODUCT_MODULE_NAME and module != param.module ) %}
+ {{module}}.
+ {% endif %}
+ {{item.type}}
+{% endfilter %}{% endmacro %}
+{{accessModifier}} enum {{param.enumName|default:"StoryboardScene"}} {
+ {% for storyboard in storyboards %}
+ {% set storyboardName %}{{storyboard.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}{% endset %}
+ {{accessModifier}} enum {{storyboardName}}: StoryboardType {
+ {{accessModifier}} static let storyboardName = "{{storyboard.name}}"
+ {% if storyboard.initialScene %}
+
+ {% set sceneClass %}{% call className storyboard.initialScene %}{% endset %}
+ {{accessModifier}} static let initialScene = InitialSceneType<{{sceneClass}}>(storyboard: {{storyboardName}}.self)
+ {% endif %}
+ {% for scene in storyboard.scenes %}
+
+ {% set sceneID %}{{scene.identifier|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set sceneClass %}{% call className scene %}{% endset %}
+ {{accessModifier}} static let {{sceneID}} = SceneType<{{sceneClass}}>(storyboard: {{storyboardName}}.self, identifier: "{{scene.identifier}}")
+ {% endfor %}
+ }
+ {% endfor %}
+}
+// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name
+
+// MARK: - Implementation Details
+
+{{accessModifier}} protocol StoryboardType {
+ static var storyboardName: String { get }
+}
+
+{{accessModifier}} extension StoryboardType {
+ static var storyboard: {{prefix}}Storyboard {
+ let name = {% if isAppKit %}NSStoryboard.Name({% endif %}self.storyboardName{% if isAppKit %}){% endif %}
+ {% if param.lookupFunction %}
+ return {{param.lookupFunction}}(name)
+ {% else %}
+ return {{prefix}}Storyboard(name: name, bundle: {{param.bundle|default:"BundleToken.bundle"}})
+ {% endif %}
+ }
+}
+
+{{accessModifier}} struct SceneType {
+ {{accessModifier}} let storyboard: StoryboardType.Type
+ {{accessModifier}} let identifier: String
+
+ {{accessModifier}} func instantiate() -> T {
+ let identifier = {% if isAppKit %}NSStoryboard.SceneIdentifier({% endif %}self.identifier{% if isAppKit %}){% endif %}
+ guard let controller = storyboard.storyboard.instantiate{{controller}}(withIdentifier: identifier) as? T else {
+ fatalError("{{controller}} '\(identifier)' is not of the expected class \(T.self).")
+ }
+ return controller
+ }
+
+ {% if isAppKit %}
+ @available(macOS 10.15, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSViewController {
+ let identifier = NSStoryboard.SceneIdentifier(self.identifier)
+ return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
+ }
+
+ @available(macOS 10.15, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSWindowController {
+ let identifier = NSStoryboard.SceneIdentifier(self.identifier)
+ return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
+ }
+ {% else %}
+ @available(iOS 13.0, tvOS 13.0, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T {
+ return storyboard.storyboard.instantiate{{controller}}(identifier: identifier, creator: block)
+ }
+ {% endif %}
+}
+
+{{accessModifier}} struct InitialSceneType {
+ {{accessModifier}} let storyboard: StoryboardType.Type
+
+ {{accessModifier}} func instantiate() -> T {
+ guard let controller = storyboard.storyboard.instantiateInitial{{controller}}() as? T else {
+ fatalError("{{controller}} is not of the expected class \(T.self).")
+ }
+ return controller
+ }
+
+ {% if isAppKit %}
+ @available(macOS 10.15, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSViewController {
+ guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
+ fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
+ }
+ return controller
+ }
+
+ @available(macOS 10.15, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T where T: NSWindowController {
+ guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
+ fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
+ }
+ return controller
+ }
+ {% else %}
+ @available(iOS 13.0, tvOS 13.0, *)
+ {{accessModifier}} func instantiate(creator block: @escaping (NSCoder) -> T?) -> T {
+ guard let controller = storyboard.storyboard.instantiateInitial{{controller}}(creator: block) else {
+ fatalError("Storyboard \(storyboard.storyboardName) does not have an initial scene.")
+ }
+ return controller
+ }
+ {% endif %}
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% elif storyboards %}
+// Mixed AppKit and UIKit storyboard files found, please invoke swiftgen with these separately
+{% else %}
+// No storyboard found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/segues-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/segues-swift4.stencil
new file mode 100644
index 0000000..476d546
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/segues-swift4.stencil
@@ -0,0 +1,60 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if platform and storyboards %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set isAppKit %}{% if platform == "macOS" %}true{% endif %}{% endset %}
+// swiftlint:disable sorted_imports
+import Foundation
+{% for module in modules where module != env.PRODUCT_MODULE_NAME and module != param.module %}
+import {{module}}
+{% endfor %}
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - Storyboard Segues
+
+// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
+{{accessModifier}} enum {{param.enumName|default:"StoryboardSegue"}} {
+ {% for storyboard in storyboards where storyboard.segues %}
+ {{accessModifier}} enum {{storyboard.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}: String, SegueType {
+ {% for segue in storyboard.segues %}
+ {% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
+ case {{segueID|escapeReservedKeywords}}{% if segueID != segue.identifier %} = "{{segue.identifier}}"{% endif %}
+ {% endfor %}
+ }
+ {% endfor %}
+}
+// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name
+
+// MARK: - Implementation Details
+
+{{accessModifier}} protocol SegueType: RawRepresentable {}
+
+{{accessModifier}} extension {% if isAppKit %}NSSeguePerforming{% else %}UIViewController{% endif %} {
+ func perform(segue: S, sender: Any? = nil) where S.RawValue == String {
+ let identifier = {% if isAppKit %}NSStoryboardSegue.Identifier({% endif %}segue.rawValue{% if isAppKit %}){% endif %}
+ performSegue{% if isAppKit %}?{% endif %}(withIdentifier: identifier, sender: sender)
+ }
+}
+
+{{accessModifier}} extension SegueType where RawValue == String {
+ init?(_ segue: {% if isAppKit %}NS{% else %}UI{% endif %}StoryboardSegue) {
+ {% if isAppKit %}
+ #if swift(>=4.2)
+ guard let identifier = segue.identifier else { return nil }
+ #else
+ guard let identifier = segue.identifier?.rawValue else { return nil }
+ #endif
+ {% else %}
+ guard let identifier = segue.identifier else { return nil }
+ {% endif %}
+ self.init(rawValue: identifier)
+ }
+}
+{% elif storyboards %}
+// Mixed AppKit and UIKit storyboard files found, please invoke swiftgen with these separately
+{% else %}
+// No storyboard found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/segues-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/segues-swift5.stencil
new file mode 100644
index 0000000..476d546
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/ib/segues-swift5.stencil
@@ -0,0 +1,60 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if platform and storyboards %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set isAppKit %}{% if platform == "macOS" %}true{% endif %}{% endset %}
+// swiftlint:disable sorted_imports
+import Foundation
+{% for module in modules where module != env.PRODUCT_MODULE_NAME and module != param.module %}
+import {{module}}
+{% endfor %}
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - Storyboard Segues
+
+// swiftlint:disable explicit_type_interface identifier_name line_length type_body_length type_name
+{{accessModifier}} enum {{param.enumName|default:"StoryboardSegue"}} {
+ {% for storyboard in storyboards where storyboard.segues %}
+ {{accessModifier}} enum {{storyboard.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}: String, SegueType {
+ {% for segue in storyboard.segues %}
+ {% set segueID %}{{segue.identifier|swiftIdentifier:"pretty"|lowerFirstWord}}{% endset %}
+ case {{segueID|escapeReservedKeywords}}{% if segueID != segue.identifier %} = "{{segue.identifier}}"{% endif %}
+ {% endfor %}
+ }
+ {% endfor %}
+}
+// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name
+
+// MARK: - Implementation Details
+
+{{accessModifier}} protocol SegueType: RawRepresentable {}
+
+{{accessModifier}} extension {% if isAppKit %}NSSeguePerforming{% else %}UIViewController{% endif %} {
+ func perform(segue: S, sender: Any? = nil) where S.RawValue == String {
+ let identifier = {% if isAppKit %}NSStoryboardSegue.Identifier({% endif %}segue.rawValue{% if isAppKit %}){% endif %}
+ performSegue{% if isAppKit %}?{% endif %}(withIdentifier: identifier, sender: sender)
+ }
+}
+
+{{accessModifier}} extension SegueType where RawValue == String {
+ init?(_ segue: {% if isAppKit %}NS{% else %}UI{% endif %}StoryboardSegue) {
+ {% if isAppKit %}
+ #if swift(>=4.2)
+ guard let identifier = segue.identifier else { return nil }
+ #else
+ guard let identifier = segue.identifier?.rawValue else { return nil }
+ #endif
+ {% else %}
+ guard let identifier = segue.identifier else { return nil }
+ {% endif %}
+ self.init(rawValue: identifier)
+ }
+}
+{% elif storyboards %}
+// Mixed AppKit and UIKit storyboard files found, please invoke swiftgen with these separately
+{% else %}
+// No storyboard found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/inline-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/inline-swift4.stencil
new file mode 100644
index 0000000..62ca48d
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/inline-swift4.stencil
@@ -0,0 +1,82 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if files %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - JSON Files
+{% macro fileBlock file %}
+ {% call documentBlock file file.document %}
+{% endmacro %}
+{% macro documentBlock file document %}
+ {% set rootType %}{% call typeBlock document.metadata %}{% endset %}
+ {% if document.metadata.type == "Array" %}
+ {{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% elif document.metadata.type == "Dictionary" %}
+ {% for key,value in document.metadata.properties %}
+ {{accessModifier}} {% call propertyBlock key value document.data %}
+ {% endfor %}
+ {% else %}
+ {{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% endif %}
+{% endmacro %}
+{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "Array" %}
+ [{% call typeBlock metadata.element %}]
+ {% elif metadata.type == "Dictionary" %}
+ [String: Any]
+ {% elif metadata.type == "Optional" %}
+ Any?
+ {% else %}
+ {{metadata.type}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
+ {% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set propertyType %}{% call typeBlock metadata %}{% endset %}
+ static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
+{% endfilter %}{% endmacro %}
+{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "String" %}
+ "{{ value }}"
+ {% elif metadata.type == "Optional" %}
+ nil
+ {% elif metadata.type == "Array" and value %}
+ [{% for value in value %}
+ {% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}]
+ {% elif metadata.type == "Dictionary" %}
+ [{% for key,value in value %}
+ "{{key}}": {% call valueBlock value metadata.properties[key] %}
+ {{ ", " if not forloop.last }}
+ {% empty %}
+ :
+ {% endfor %}]
+ {% elif metadata.type == "Bool" %}
+ {% if value %}true{% else %}false{% endif %}
+ {% else %}
+ {{ value }}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+
+// swiftlint:disable identifier_name line_length number_separator type_body_length
+{{accessModifier}} enum {{param.enumName|default:"JSONFiles"}} {
+ {% if files.count > 1 or param.forceFileNameEnum %}
+ {% for file in files %}
+ {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call fileBlock files.first %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length number_separator type_body_length
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/inline-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/inline-swift5.stencil
new file mode 100644
index 0000000..62ca48d
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/inline-swift5.stencil
@@ -0,0 +1,82 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if files %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - JSON Files
+{% macro fileBlock file %}
+ {% call documentBlock file file.document %}
+{% endmacro %}
+{% macro documentBlock file document %}
+ {% set rootType %}{% call typeBlock document.metadata %}{% endset %}
+ {% if document.metadata.type == "Array" %}
+ {{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% elif document.metadata.type == "Dictionary" %}
+ {% for key,value in document.metadata.properties %}
+ {{accessModifier}} {% call propertyBlock key value document.data %}
+ {% endfor %}
+ {% else %}
+ {{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% endif %}
+{% endmacro %}
+{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "Array" %}
+ [{% call typeBlock metadata.element %}]
+ {% elif metadata.type == "Dictionary" %}
+ [String: Any]
+ {% elif metadata.type == "Optional" %}
+ Any?
+ {% else %}
+ {{metadata.type}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
+ {% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set propertyType %}{% call typeBlock metadata %}{% endset %}
+ static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
+{% endfilter %}{% endmacro %}
+{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "String" %}
+ "{{ value }}"
+ {% elif metadata.type == "Optional" %}
+ nil
+ {% elif metadata.type == "Array" and value %}
+ [{% for value in value %}
+ {% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}]
+ {% elif metadata.type == "Dictionary" %}
+ [{% for key,value in value %}
+ "{{key}}": {% call valueBlock value metadata.properties[key] %}
+ {{ ", " if not forloop.last }}
+ {% empty %}
+ :
+ {% endfor %}]
+ {% elif metadata.type == "Bool" %}
+ {% if value %}true{% else %}false{% endif %}
+ {% else %}
+ {{ value }}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+
+// swiftlint:disable identifier_name line_length number_separator type_body_length
+{{accessModifier}} enum {{param.enumName|default:"JSONFiles"}} {
+ {% if files.count > 1 or param.forceFileNameEnum %}
+ {% for file in files %}
+ {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call fileBlock files.first %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length number_separator type_body_length
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/runtime-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/runtime-swift4.stencil
new file mode 100644
index 0000000..c2466c7
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/runtime-swift4.stencil
@@ -0,0 +1,112 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if files %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - JSON Files
+{% macro fileBlock file %}
+ {% call documentBlock file file.document %}
+{% endmacro %}
+{% macro documentBlock file document %}
+ {% set rootType %}{% call typeBlock document.metadata %}{% endset %}
+ {% if document.metadata.type == "Array" %}
+ {{accessModifier}} static let items: {{rootType}} = objectFromJSON(at: "{% call transformPath file.path %}")
+ {% elif document.metadata.type == "Dictionary" %}
+ private static let _document = JSONDocument(path: "{% call transformPath file.path %}")
+
+ {% for key,value in document.metadata.properties %}
+ {{accessModifier}} {% call propertyBlock key value %}
+ {% endfor %}
+ {% else %}
+ {{accessModifier}} static let value: {{rootType}} = objectFromJSON(at: "{% call transformPath file.path %}")
+ {% endif %}
+{% endmacro %}
+{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "Array" %}
+ [{% call typeBlock metadata.element %}]
+ {% elif metadata.type == "Dictionary" %}
+ [String: Any]
+ {% elif metadata.type == "Optional" %}
+ Any?
+ {% else %}
+ {{metadata.type}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro propertyBlock key metadata %}{% filter removeNewlines:"leading" %}
+ {% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set propertyType %}{% call typeBlock metadata %}{% endset %}
+ static let {{propertyName}}: {{propertyType}} = _document["{{key}}"]
+{% endfilter %}{% endmacro %}
+{% macro transformPath path %}{% filter removeNewlines %}
+ {% if param.preservePath %}
+ {{path}}
+ {% else %}
+ {{path|basename}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+
+// swiftlint:disable identifier_name line_length type_body_length
+{{accessModifier}} enum {{param.enumName|default:"JSONFiles"}} {
+ {% if files.count > 1 or param.forceFileNameEnum %}
+ {% for file in files %}
+ {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call fileBlock files.first %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length type_body_length
+
+// MARK: - Implementation Details
+
+private func objectFromJSON(at path: String) -> T {
+ {% if param.lookupFunction %}
+ guard let url = {{param.lookupFunction}}(path),
+ {% else %}
+ guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
+ {% endif %}
+ let json = try? JSONSerialization.jsonObject(with: Data(contentsOf: url), options: []),
+ let result = json as? T else {
+ fatalError("Unable to load JSON at path: \(path)")
+ }
+ return result
+}
+
+private struct JSONDocument {
+ let data: [String: Any]
+
+ init(path: String) {
+ self.data = objectFromJSON(at: path)
+ }
+
+ subscript(key: String) -> T {
+ guard let result = data[key] as? T else {
+ fatalError("Property '\(key)' is not of type \(T.self)")
+ }
+ return result
+ }
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/runtime-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/runtime-swift5.stencil
new file mode 100644
index 0000000..c2466c7
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/json/runtime-swift5.stencil
@@ -0,0 +1,112 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if files %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - JSON Files
+{% macro fileBlock file %}
+ {% call documentBlock file file.document %}
+{% endmacro %}
+{% macro documentBlock file document %}
+ {% set rootType %}{% call typeBlock document.metadata %}{% endset %}
+ {% if document.metadata.type == "Array" %}
+ {{accessModifier}} static let items: {{rootType}} = objectFromJSON(at: "{% call transformPath file.path %}")
+ {% elif document.metadata.type == "Dictionary" %}
+ private static let _document = JSONDocument(path: "{% call transformPath file.path %}")
+
+ {% for key,value in document.metadata.properties %}
+ {{accessModifier}} {% call propertyBlock key value %}
+ {% endfor %}
+ {% else %}
+ {{accessModifier}} static let value: {{rootType}} = objectFromJSON(at: "{% call transformPath file.path %}")
+ {% endif %}
+{% endmacro %}
+{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "Array" %}
+ [{% call typeBlock metadata.element %}]
+ {% elif metadata.type == "Dictionary" %}
+ [String: Any]
+ {% elif metadata.type == "Optional" %}
+ Any?
+ {% else %}
+ {{metadata.type}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro propertyBlock key metadata %}{% filter removeNewlines:"leading" %}
+ {% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set propertyType %}{% call typeBlock metadata %}{% endset %}
+ static let {{propertyName}}: {{propertyType}} = _document["{{key}}"]
+{% endfilter %}{% endmacro %}
+{% macro transformPath path %}{% filter removeNewlines %}
+ {% if param.preservePath %}
+ {{path}}
+ {% else %}
+ {{path|basename}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+
+// swiftlint:disable identifier_name line_length type_body_length
+{{accessModifier}} enum {{param.enumName|default:"JSONFiles"}} {
+ {% if files.count > 1 or param.forceFileNameEnum %}
+ {% for file in files %}
+ {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call fileBlock files.first %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length type_body_length
+
+// MARK: - Implementation Details
+
+private func objectFromJSON(at path: String) -> T {
+ {% if param.lookupFunction %}
+ guard let url = {{param.lookupFunction}}(path),
+ {% else %}
+ guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
+ {% endif %}
+ let json = try? JSONSerialization.jsonObject(with: Data(contentsOf: url), options: []),
+ let result = json as? T else {
+ fatalError("Unable to load JSON at path: \(path)")
+ }
+ return result
+}
+
+private struct JSONDocument {
+ let data: [String: Any]
+
+ init(path: String) {
+ self.data = objectFromJSON(at: path)
+ }
+
+ subscript(key: String) -> T {
+ guard let result = data[key] as? T else {
+ fatalError("Property '\(key)' is not of type \(T.self)")
+ }
+ return result
+ }
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/inline-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/inline-swift4.stencil
new file mode 100644
index 0000000..c8e8831
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/inline-swift4.stencil
@@ -0,0 +1,82 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if files %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - Plist Files
+{% macro fileBlock file %}
+ {% call documentBlock file file.document %}
+{% endmacro %}
+{% macro documentBlock file document %}
+ {% set rootType %}{% call typeBlock document.metadata %}{% endset %}
+ {% if document.metadata.type == "Array" %}
+ {{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% elif document.metadata.type == "Dictionary" %}
+ {% for key,value in document.metadata.properties %}
+ {{accessModifier}} {% call propertyBlock key value document.data %}
+ {% endfor %}
+ {% else %}
+ {{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% endif %}
+{% endmacro %}
+{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "Array" %}
+ [{% call typeBlock metadata.element %}]
+ {% elif metadata.type == "Dictionary" %}
+ [String: Any]
+ {% else %}
+ {{metadata.type}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
+ {% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set propertyType %}{% call typeBlock metadata %}{% endset %}
+ static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
+{% endfilter %}{% endmacro %}
+{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "String" %}
+ "{{ value }}"
+ {% elif metadata.type == "Date" %}
+ Date(timeIntervalSinceReferenceDate: {{ value.timeIntervalSinceReferenceDate }})
+ {% elif metadata.type == "Optional" %}
+ nil
+ {% elif metadata.type == "Array" and value %}
+ [{% for value in value %}
+ {% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}]
+ {% elif metadata.type == "Dictionary" %}
+ [{% for key,value in value %}
+ "{{key}}": {% call valueBlock value metadata.properties[key] %}
+ {{ ", " if not forloop.last }}
+ {% empty %}
+ :
+ {% endfor %}]
+ {% elif metadata.type == "Bool" %}
+ {% if value %}true{% else %}false{% endif %}
+ {% else %}
+ {{ value }}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+
+// swiftlint:disable identifier_name line_length number_separator type_body_length
+{{accessModifier}} enum {{param.enumName|default:"PlistFiles"}} {
+ {% if files.count > 1 or param.forceFileNameEnum %}
+ {% for file in files %}
+ {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call fileBlock files.first %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length number_separator type_body_length
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/inline-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/inline-swift5.stencil
new file mode 100644
index 0000000..c8e8831
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/inline-swift5.stencil
@@ -0,0 +1,82 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if files %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - Plist Files
+{% macro fileBlock file %}
+ {% call documentBlock file file.document %}
+{% endmacro %}
+{% macro documentBlock file document %}
+ {% set rootType %}{% call typeBlock document.metadata %}{% endset %}
+ {% if document.metadata.type == "Array" %}
+ {{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% elif document.metadata.type == "Dictionary" %}
+ {% for key,value in document.metadata.properties %}
+ {{accessModifier}} {% call propertyBlock key value document.data %}
+ {% endfor %}
+ {% else %}
+ {{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% endif %}
+{% endmacro %}
+{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "Array" %}
+ [{% call typeBlock metadata.element %}]
+ {% elif metadata.type == "Dictionary" %}
+ [String: Any]
+ {% else %}
+ {{metadata.type}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
+ {% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set propertyType %}{% call typeBlock metadata %}{% endset %}
+ static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
+{% endfilter %}{% endmacro %}
+{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "String" %}
+ "{{ value }}"
+ {% elif metadata.type == "Date" %}
+ Date(timeIntervalSinceReferenceDate: {{ value.timeIntervalSinceReferenceDate }})
+ {% elif metadata.type == "Optional" %}
+ nil
+ {% elif metadata.type == "Array" and value %}
+ [{% for value in value %}
+ {% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}]
+ {% elif metadata.type == "Dictionary" %}
+ [{% for key,value in value %}
+ "{{key}}": {% call valueBlock value metadata.properties[key] %}
+ {{ ", " if not forloop.last }}
+ {% empty %}
+ :
+ {% endfor %}]
+ {% elif metadata.type == "Bool" %}
+ {% if value %}true{% else %}false{% endif %}
+ {% else %}
+ {{ value }}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+
+// swiftlint:disable identifier_name line_length number_separator type_body_length
+{{accessModifier}} enum {{param.enumName|default:"PlistFiles"}} {
+ {% if files.count > 1 or param.forceFileNameEnum %}
+ {% for file in files %}
+ {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call fileBlock files.first %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length number_separator type_body_length
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/runtime-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/runtime-swift4.stencil
new file mode 100644
index 0000000..a498a8f
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/runtime-swift4.stencil
@@ -0,0 +1,117 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if files %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - Plist Files
+{% macro fileBlock file %}
+ {% call documentBlock file file.document %}
+{% endmacro %}
+{% macro documentBlock file document %}
+ {% set rootType %}{% call typeBlock document.metadata %}{% endset %}
+ {% if document.metadata.type == "Array" %}
+ {{accessModifier}} static let items: {{rootType}} = arrayFromPlist(at: "{% call transformPath file.path %}")
+ {% elif document.metadata.type == "Dictionary" %}
+ private static let _document = PlistDocument(path: "{% call transformPath file.path %}")
+
+ {% for key,value in document.metadata.properties %}
+ {{accessModifier}} {% call propertyBlock key value %}
+ {% endfor %}
+ {% else %}
+ // Unsupported root type `{{rootType}}`
+ {% endif %}
+{% endmacro %}
+{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "Array" %}
+ [{% call typeBlock metadata.element %}]
+ {% elif metadata.type == "Dictionary" %}
+ [String: Any]
+ {% else %}
+ {{metadata.type}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro propertyBlock key metadata %}{% filter removeNewlines:"leading" %}
+ {% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set propertyType %}{% call typeBlock metadata %}{% endset %}
+ static let {{propertyName}}: {{propertyType}} = _document["{{key}}"]
+{% endfilter %}{% endmacro %}
+{% macro transformPath path %}{% filter removeNewlines %}
+ {% if param.preservePath %}
+ {{path}}
+ {% else %}
+ {{path|basename}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+
+// swiftlint:disable identifier_name line_length type_body_length
+{{accessModifier}} enum {{param.enumName|default:"PlistFiles"}} {
+ {% if files.count > 1 or param.forceFileNameEnum %}
+ {% for file in files %}
+ {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call fileBlock files.first %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length type_body_length
+
+// MARK: - Implementation Details
+
+private func arrayFromPlist(at path: String) -> [T] {
+ {% if param.lookupFunction %}
+ guard let url = {{param.lookupFunction}}(path),
+ {% else %}
+ guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
+ {% endif %}
+ let data = NSArray(contentsOf: url) as? [T] else {
+ fatalError("Unable to load PLIST at path: \(path)")
+ }
+ return data
+}
+
+private struct PlistDocument {
+ let data: [String: Any]
+
+ init(path: String) {
+ {% if param.lookupFunction %}
+ guard let url = {{param.lookupFunction}}(path),
+ {% else %}
+ guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
+ {% endif %}
+ let data = NSDictionary(contentsOf: url) as? [String: Any] else {
+ fatalError("Unable to load PLIST at path: \(path)")
+ }
+ self.data = data
+ }
+
+ subscript(key: String) -> T {
+ guard let result = data[key] as? T else {
+ fatalError("Property '\(key)' is not of type \(T.self)")
+ }
+ return result
+ }
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/runtime-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/runtime-swift5.stencil
new file mode 100644
index 0000000..a498a8f
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/plist/runtime-swift5.stencil
@@ -0,0 +1,117 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if files %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - Plist Files
+{% macro fileBlock file %}
+ {% call documentBlock file file.document %}
+{% endmacro %}
+{% macro documentBlock file document %}
+ {% set rootType %}{% call typeBlock document.metadata %}{% endset %}
+ {% if document.metadata.type == "Array" %}
+ {{accessModifier}} static let items: {{rootType}} = arrayFromPlist(at: "{% call transformPath file.path %}")
+ {% elif document.metadata.type == "Dictionary" %}
+ private static let _document = PlistDocument(path: "{% call transformPath file.path %}")
+
+ {% for key,value in document.metadata.properties %}
+ {{accessModifier}} {% call propertyBlock key value %}
+ {% endfor %}
+ {% else %}
+ // Unsupported root type `{{rootType}}`
+ {% endif %}
+{% endmacro %}
+{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "Array" %}
+ [{% call typeBlock metadata.element %}]
+ {% elif metadata.type == "Dictionary" %}
+ [String: Any]
+ {% else %}
+ {{metadata.type}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro propertyBlock key metadata %}{% filter removeNewlines:"leading" %}
+ {% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set propertyType %}{% call typeBlock metadata %}{% endset %}
+ static let {{propertyName}}: {{propertyType}} = _document["{{key}}"]
+{% endfilter %}{% endmacro %}
+{% macro transformPath path %}{% filter removeNewlines %}
+ {% if param.preservePath %}
+ {{path}}
+ {% else %}
+ {{path|basename}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+
+// swiftlint:disable identifier_name line_length type_body_length
+{{accessModifier}} enum {{param.enumName|default:"PlistFiles"}} {
+ {% if files.count > 1 or param.forceFileNameEnum %}
+ {% for file in files %}
+ {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call fileBlock files.first %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length type_body_length
+
+// MARK: - Implementation Details
+
+private func arrayFromPlist(at path: String) -> [T] {
+ {% if param.lookupFunction %}
+ guard let url = {{param.lookupFunction}}(path),
+ {% else %}
+ guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
+ {% endif %}
+ let data = NSArray(contentsOf: url) as? [T] else {
+ fatalError("Unable to load PLIST at path: \(path)")
+ }
+ return data
+}
+
+private struct PlistDocument {
+ let data: [String: Any]
+
+ init(path: String) {
+ {% if param.lookupFunction %}
+ guard let url = {{param.lookupFunction}}(path),
+ {% else %}
+ guard let url = {{param.bundle|default:"BundleToken.bundle"}}.url(forResource: path, withExtension: nil),
+ {% endif %}
+ let data = NSDictionary(contentsOf: url) as? [String: Any] else {
+ fatalError("Unable to load PLIST at path: \(path)")
+ }
+ self.data = data
+ }
+
+ subscript(key: String) -> T {
+ guard let result = data[key] as? T else {
+ fatalError("Property '\(key)' is not of type \(T.self)")
+ }
+ return result
+ }
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/flat-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/flat-swift4.stencil
new file mode 100644
index 0000000..5bb4a12
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/flat-swift4.stencil
@@ -0,0 +1,99 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if tables.count > 0 %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command file_length implicit_return
+
+// MARK: - Strings
+
+{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ {% if type == "String" %}
+ _ p{{forloop.counter}}: Any
+ {% else %}
+ _ p{{forloop.counter}}: {{type}}
+ {% endif %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ {% if type == "String" %}
+ String(describing: p{{forloop.counter}})
+ {% elif type == "UnsafeRawPointer" %}
+ Int(bitPattern: p{{forloop.counter}})
+ {% else %}
+ p{{forloop.counter}}
+ {% endif %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro recursiveBlock table item %}
+ {% for string in item.strings %}
+ {% if not param.noComments %}
+ {% for line in string.translation|split:"\n" %}
+ /// {{line}}
+ {% endfor %}
+ {% endif %}
+ {% if string.types %}
+ {{accessModifier}} static func {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
+ return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
+ }
+ {% elif param.lookupFunction %}
+ {# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #}
+ {{accessModifier}} static var {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") }
+ {% else %}
+ {{accessModifier}} static let {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}")
+ {% endif %}
+ {% endfor %}
+ {% for child in item.children %}
+ {% call recursiveBlock table child %}
+ {% endfor %}
+{% endmacro %}
+// swiftlint:disable function_parameter_count identifier_name line_length type_body_length
+{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
+{{accessModifier}} enum {{enumName}} {
+ {% if tables.count > 1 or param.forceFileNameEnum %}
+ {% for table in tables %}
+ {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call recursiveBlock tables.first.name tables.first.levels %}
+ {% endif %}
+}
+// swiftlint:enable function_parameter_count identifier_name line_length type_body_length
+
+// MARK: - Implementation Details
+
+extension {{enumName}} {
+ private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
+ {% if param.lookupFunction %}
+ let format = {{ param.lookupFunction }}(key, table)
+ {% else %}
+ let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table)
+ {% endif %}
+ return String(format: format, locale: Locale.current, arguments: args)
+ }
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No string found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/flat-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/flat-swift5.stencil
new file mode 100644
index 0000000..5bb4a12
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/flat-swift5.stencil
@@ -0,0 +1,99 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if tables.count > 0 %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command file_length implicit_return
+
+// MARK: - Strings
+
+{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ {% if type == "String" %}
+ _ p{{forloop.counter}}: Any
+ {% else %}
+ _ p{{forloop.counter}}: {{type}}
+ {% endif %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ {% if type == "String" %}
+ String(describing: p{{forloop.counter}})
+ {% elif type == "UnsafeRawPointer" %}
+ Int(bitPattern: p{{forloop.counter}})
+ {% else %}
+ p{{forloop.counter}}
+ {% endif %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro recursiveBlock table item %}
+ {% for string in item.strings %}
+ {% if not param.noComments %}
+ {% for line in string.translation|split:"\n" %}
+ /// {{line}}
+ {% endfor %}
+ {% endif %}
+ {% if string.types %}
+ {{accessModifier}} static func {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
+ return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
+ }
+ {% elif param.lookupFunction %}
+ {# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #}
+ {{accessModifier}} static var {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") }
+ {% else %}
+ {{accessModifier}} static let {{string.key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}")
+ {% endif %}
+ {% endfor %}
+ {% for child in item.children %}
+ {% call recursiveBlock table child %}
+ {% endfor %}
+{% endmacro %}
+// swiftlint:disable function_parameter_count identifier_name line_length type_body_length
+{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
+{{accessModifier}} enum {{enumName}} {
+ {% if tables.count > 1 or param.forceFileNameEnum %}
+ {% for table in tables %}
+ {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call recursiveBlock tables.first.name tables.first.levels %}
+ {% endif %}
+}
+// swiftlint:enable function_parameter_count identifier_name line_length type_body_length
+
+// MARK: - Implementation Details
+
+extension {{enumName}} {
+ private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
+ {% if param.lookupFunction %}
+ let format = {{ param.lookupFunction }}(key, table)
+ {% else %}
+ let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table)
+ {% endif %}
+ return String(format: format, locale: Locale.current, arguments: args)
+ }
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No string found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/objc-h.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/objc-h.stencil
new file mode 100644
index 0000000..7c50291
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/objc-h.stencil
@@ -0,0 +1,68 @@
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if tables.count > 0 %}
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ ({% call paramTranslate type %})p{{ forloop.counter }}{{ " :" if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ p{{forloop.counter}}{{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro paramTranslate swiftType %}
+ {% if swiftType == "Any" %}
+ id
+ {% elif swiftType == "CChar" %}
+ char
+ {% elif swiftType == "Float" %}
+ float
+ {% elif swiftType == "Int" %}
+ NSInteger
+ {% elif swiftType == "String" %}
+ id
+ {% elif swiftType == "UnsafePointer" %}
+ char*
+ {% elif swiftType == "UnsafeRawPointer" %}
+ void*
+ {% else %}
+ objc-h.stencil is missing '{{swiftType}}'
+ {% endif %}
+{% endmacro %}
+{% macro emitOneMethod table item %}
+{% for string in item.strings %}
+{% if not param.noComments %}
+{% for line in string.translation|split:"\n" %}
+/// {{line}}
+{% endfor %}
+{% endif %}
+{% if string.types %}
+ {% if string.types.count == 1 %}
++ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}}WithValue:{% call parametersBlock string.types %};
+ {% else %}
++ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}}WithValues:{% call parametersBlock string.types %};
+ {% endif %}
+{% else %}
++ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}};
+{% endif %}
+{% endfor %}
+{% for child in item.children %}
+{% call emitOneMethod table child %}
+{% endfor %}
+{% endmacro %}
+{% for table in tables %}
+@interface {{ table.name }} : NSObject
+ {% call emitOneMethod table.name table.levels %}
+@end
+
+{% endfor %}
+
+NS_ASSUME_NONNULL_END
+{% else %}
+// No strings found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/objc-m.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/objc-m.stencil
new file mode 100644
index 0000000..1f154b5
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/objc-m.stencil
@@ -0,0 +1,90 @@
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if tables.count > 0 %}
+#import "{{ param.headerName|default:"Localizable.h" }}"
+{% if not param.bundle %}
+
+@interface BundleToken : NSObject
+@end
+
+@implementation BundleToken
+@end
+{% endif %}
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wformat-security"
+
+static NSString* tr(NSString *tableName, NSString *key, ...) {
+ NSBundle *bundle = {{param.bundle|default:"[NSBundle bundleForClass:BundleToken.class]"}};
+ NSString *format = [bundle localizedStringForKey:key value:nil table:tableName];
+ NSLocale *locale = [NSLocale currentLocale];
+
+ va_list args;
+ va_start(args, key);
+ NSString *result = [[NSString alloc] initWithFormat:format locale:locale arguments:args];
+ va_end(args);
+
+ return result;
+};
+#pragma clang diagnostic pop
+
+{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ ({% call paramTranslate type %})p{{ forloop.counter }}{{ " :" if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ p{{forloop.counter}}{{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro paramTranslate swiftType %}
+ {% if swiftType == "Any" %}
+ id
+ {% elif swiftType == "CChar" %}
+ char
+ {% elif swiftType == "Float" %}
+ float
+ {% elif swiftType == "Int" %}
+ NSInteger
+ {% elif swiftType == "String" %}
+ id
+ {% elif swiftType == "UnsafePointer" %}
+ char*
+ {% elif swiftType == "UnsafeRawPointer" %}
+ void*
+ {% else %}
+ objc-m.stencil is missing '{{swiftType}}'
+ {% endif %}
+{% endmacro %}
+{% macro tableContents table item %}
+ {% for string in item.strings %}
+ {% if string.types %}
+ {% if string.types.count == 1 %}
++ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}}WithValue:{% call parametersBlock string.types %}
+ {% else %}
++ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}}WithValues:{% call parametersBlock string.types %}
+ {% endif %}
+{
+ return tr(@"{{table}}", @"{{string.key}}", {% call argumentsBlock string.types %});
+}
+{% else %}
++ (NSString*){{string.key|swiftIdentifier:"pretty"|lowerFirstWord}} {
+ return tr(@"{{table}}", @"{{string.key}}");
+}
+ {% endif %}
+ {% endfor %}
+ {% for child in item.children %}
+ {% call tableContents table child %}
+ {% endfor %}
+{% endmacro %}
+{% for table in tables %}
+ {% set tableName %}{{table.name|default:"Localized"}}{% endset %}
+@implementation {{ tableName }} : NSObject
+ {% call tableContents table.name table.levels %}
+@end
+
+{% endfor %}
+{% else %}
+// No strings found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/structured-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/structured-swift4.stencil
new file mode 100644
index 0000000..f809bc2
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/structured-swift4.stencil
@@ -0,0 +1,104 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if tables.count > 0 %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command file_length implicit_return
+
+// MARK: - Strings
+
+{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ {% if type == "String" %}
+ _ p{{forloop.counter}}: Any
+ {% else %}
+ _ p{{forloop.counter}}: {{type}}
+ {% endif %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ {% if type == "String" %}
+ String(describing: p{{forloop.counter}})
+ {% elif type == "UnsafeRawPointer" %}
+ Int(bitPattern: p{{forloop.counter}})
+ {% else %}
+ p{{forloop.counter}}
+ {% endif %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro recursiveBlock table item %}
+ {% for string in item.strings %}
+ {% if not param.noComments %}
+ {% for line in string.translation|split:"\n" %}
+ /// {{line}}
+ {% endfor %}
+ {% endif %}
+ {% if string.types %}
+ {{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
+ return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
+ }
+ {% elif param.lookupFunction %}
+ {# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #}
+ {{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") }
+ {% else %}
+ {{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}")
+ {% endif %}
+ {% endfor %}
+ {% for child in item.children %}
+
+ {{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %}
+ }
+ {% endfor %}
+{% endmacro %}
+// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
+// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
+{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
+{{accessModifier}} enum {{enumName}} {
+ {% if tables.count > 1 or param.forceFileNameEnum %}
+ {% for table in tables %}
+ {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call recursiveBlock tables.first.name tables.first.levels %}
+ {% endif %}
+}
+// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
+// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
+
+// MARK: - Implementation Details
+
+extension {{enumName}} {
+ private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
+ {% if param.lookupFunction %}
+ let format = {{ param.lookupFunction }}(key, table)
+ {% else %}
+ let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table)
+ {% endif %}
+ return String(format: format, locale: Locale.current, arguments: args)
+ }
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No string found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/structured-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/structured-swift5.stencil
new file mode 100644
index 0000000..f809bc2
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/strings/structured-swift5.stencil
@@ -0,0 +1,104 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if tables.count > 0 %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command file_length implicit_return
+
+// MARK: - Strings
+
+{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ {% if type == "String" %}
+ _ p{{forloop.counter}}: Any
+ {% else %}
+ _ p{{forloop.counter}}: {{type}}
+ {% endif %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ {% if type == "String" %}
+ String(describing: p{{forloop.counter}})
+ {% elif type == "UnsafeRawPointer" %}
+ Int(bitPattern: p{{forloop.counter}})
+ {% else %}
+ p{{forloop.counter}}
+ {% endif %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro recursiveBlock table item %}
+ {% for string in item.strings %}
+ {% if not param.noComments %}
+ {% for line in string.translation|split:"\n" %}
+ /// {{line}}
+ {% endfor %}
+ {% endif %}
+ {% if string.types %}
+ {{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
+ return {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
+ }
+ {% elif param.lookupFunction %}
+ {# custom localization function is mostly used for in-app lang selection, so we want the loc to be recomputed at each call for those (hence the computed var) #}
+ {{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { return {{enumName}}.tr("{{table}}", "{{string.key}}") }
+ {% else %}
+ {{accessModifier}} static let {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{enumName}}.tr("{{table}}", "{{string.key}}")
+ {% endif %}
+ {% endfor %}
+ {% for child in item.children %}
+
+ {{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %}
+ }
+ {% endfor %}
+{% endmacro %}
+// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
+// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
+{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
+{{accessModifier}} enum {{enumName}} {
+ {% if tables.count > 1 or param.forceFileNameEnum %}
+ {% for table in tables %}
+ {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call recursiveBlock tables.first.name tables.first.levels %}
+ {% endif %}
+}
+// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
+// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
+
+// MARK: - Implementation Details
+
+extension {{enumName}} {
+ private static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
+ {% if param.lookupFunction %}
+ let format = {{ param.lookupFunction }}(key, table)
+ {% else %}
+ let format = {{param.bundle|default:"BundleToken.bundle"}}.localizedString(forKey: key, value: nil, table: table)
+ {% endif %}
+ return String(format: format, locale: Locale.current, arguments: args)
+ }
+}
+{% if not param.bundle and not param.lookupFunction %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No string found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/xcassets/swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/xcassets/swift4.stencil
new file mode 100644
index 0000000..c856593
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/xcassets/swift4.stencil
@@ -0,0 +1,329 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if catalogs %}
+{% set enumName %}{{param.enumName|default:"Asset"}}{% endset %}
+{% set arResourceGroupType %}{{param.arResourceGroupTypeName|default:"ARResourceGroupAsset"}}{% endset %}
+{% set colorType %}{{param.colorTypeName|default:"ColorAsset"}}{% endset %}
+{% set dataType %}{{param.dataTypeName|default:"DataAsset"}}{% endset %}
+{% set imageType %}{{param.imageTypeName|default:"ImageAsset"}}{% endset %}
+{% set symbolType %}{{param.symbolTypeName|default:"SymbolAsset"}}{% endset %}
+{% set forceNamespaces %}{{param.forceProvidesNamespaces|default:"false"}}{% endset %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+#if os(macOS)
+ import AppKit
+#elseif os(iOS)
+{% if resourceCount.arresourcegroup > 0 %}
+ import ARKit
+{% endif %}
+ import UIKit
+#elseif os(tvOS) || os(watchOS)
+ import UIKit
+#endif
+
+// Deprecated typealiases
+{% if resourceCount.color > 0 %}
+@available(*, deprecated, renamed: "{{colorType}}.Color", message: "This typealias will be removed in SwiftGen 7.0")
+{{accessModifier}} typealias {{param.colorAliasName|default:"AssetColorTypeAlias"}} = {{colorType}}.Color
+{% endif %}
+{% if resourceCount.image > 0 %}
+@available(*, deprecated, renamed: "{{imageType}}.Image", message: "This typealias will be removed in SwiftGen 7.0")
+{{accessModifier}} typealias {{param.imageAliasName|default:"AssetImageTypeAlias"}} = {{imageType}}.Image
+{% endif %}
+
+// swiftlint:disable superfluous_disable_command file_length implicit_return
+
+// MARK: - Asset Catalogs
+
+{% macro enumBlock assets %}
+ {% call casesBlock assets %}
+ {% if param.allValues %}
+
+ // swiftlint:disable trailing_comma
+ {% if resourceCount.arresourcegroup > 0 %}
+ {{accessModifier}} static let allResourceGroups: [{{arResourceGroupType}}] = [
+ {% filter indent:2 %}{% call allValuesBlock assets "arresourcegroup" "" %}{% endfilter %}
+ ]
+ {% endif %}
+ {% if resourceCount.color > 0 %}
+ {{accessModifier}} static let allColors: [{{colorType}}] = [
+ {% filter indent:2 %}{% call allValuesBlock assets "color" "" %}{% endfilter %}
+ ]
+ {% endif %}
+ {% if resourceCount.data > 0 %}
+ {{accessModifier}} static let allDataAssets: [{{dataType}}] = [
+ {% filter indent:2 %}{% call allValuesBlock assets "data" "" %}{% endfilter %}
+ ]
+ {% endif %}
+ {% if resourceCount.image > 0 %}
+ {{accessModifier}} static let allImages: [{{imageType}}] = [
+ {% filter indent:2 %}{% call allValuesBlock assets "image" "" %}{% endfilter %}
+ ]
+ {% endif %}
+ {% if resourceCount.symbol > 0 %}
+ {{accessModifier}} static let allSymbols: [{{symbolType}}] = [
+ {% filter indent:2 %}{% call allValuesBlock assets "symbol" "" %}{% endfilter %}
+ ]
+ {% endif %}
+ // swiftlint:enable trailing_comma
+ {% endif %}
+{% endmacro %}
+{% macro casesBlock assets %}
+ {% for asset in assets %}
+ {% if asset.type == "arresourcegroup" %}
+ {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{arResourceGroupType}}(name: "{{asset.value}}")
+ {% elif asset.type == "color" %}
+ {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{colorType}}(name: "{{asset.value}}")
+ {% elif asset.type == "data" %}
+ {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{dataType}}(name: "{{asset.value}}")
+ {% elif asset.type == "image" %}
+ {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{imageType}}(name: "{{asset.value}}")
+ {% elif asset.type == "symbol" %}
+ {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{symbolType}}(name: "{{asset.value}}")
+ {% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
+ {{accessModifier}} enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %}
+ }
+ {% elif asset.items %}
+ {% call casesBlock asset.items %}
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+{% macro allValuesBlock assets filter prefix %}
+ {% for asset in assets %}
+ {% if asset.type == filter %}
+ {{prefix}}{{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}},
+ {% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
+ {% set prefix2 %}{{prefix}}{{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.{% endset %}
+ {% call allValuesBlock asset.items filter prefix2 %}
+ {% elif asset.items %}
+ {% call allValuesBlock asset.items filter prefix %}
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+// swiftlint:disable identifier_name line_length nesting type_body_length type_name
+{{accessModifier}} enum {{enumName}} {
+ {% if catalogs.count > 1 or param.forceFileNameEnum %}
+ {% for catalog in catalogs %}
+ {{accessModifier}} enum {{catalog.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call enumBlock catalog.assets %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call enumBlock catalogs.first.assets %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length nesting type_body_length type_name
+
+// MARK: - Implementation Details
+{% if resourceCount.arresourcegroup > 0 %}
+
+{{accessModifier}} struct {{arResourceGroupType}} {
+ {{accessModifier}} fileprivate(set) var name: String
+
+ #if os(iOS)
+ @available(iOS 11.3, *)
+ {{accessModifier}} var referenceImages: Set {
+ return ARReferenceImage.referenceImages(in: self)
+ }
+
+ @available(iOS 12.0, *)
+ {{accessModifier}} var referenceObjects: Set {
+ return ARReferenceObject.referenceObjects(in: self)
+ }
+ #endif
+}
+
+#if os(iOS)
+@available(iOS 11.3, *)
+{{accessModifier}} extension ARReferenceImage {
+ static func referenceImages(in asset: {{arResourceGroupType}}) -> Set {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ return referenceImages(inGroupNamed: asset.name, bundle: bundle) ?? Set()
+ }
+}
+
+@available(iOS 12.0, *)
+{{accessModifier}} extension ARReferenceObject {
+ static func referenceObjects(in asset: {{arResourceGroupType}}) -> Set {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ return referenceObjects(inGroupNamed: asset.name, bundle: bundle) ?? Set()
+ }
+}
+#endif
+{% endif %}
+{% if resourceCount.color > 0 %}
+
+{{accessModifier}} final class {{colorType}} {
+ {{accessModifier}} fileprivate(set) var name: String
+
+ #if os(macOS)
+ {{accessModifier}} typealias Color = NSColor
+ #elseif os(iOS) || os(tvOS) || os(watchOS)
+ {{accessModifier}} typealias Color = UIColor
+ #endif
+
+ @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
+ {{accessModifier}} private(set) lazy var color: Color = Color(asset: self)
+
+ #if os(iOS) || os(tvOS)
+ @available(iOS 11.0, tvOS 11.0, *)
+ {{accessModifier}} func color(compatibleWith traitCollection: UITraitCollection) -> Color {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else {
+ fatalError("Unable to load color asset named \(name).")
+ }
+ return color
+ }
+ #endif
+
+ fileprivate init(name: String) {
+ self.name = name
+ }
+}
+
+{{accessModifier}} extension {{colorType}}.Color {
+ @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
+ convenience init!(asset: {{colorType}}) {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ #if os(iOS) || os(tvOS)
+ self.init(named: asset.name, in: bundle, compatibleWith: nil)
+ #elseif os(macOS)
+ self.init(named: NSColor.Name(asset.name), bundle: bundle)
+ #elseif os(watchOS)
+ self.init(named: asset.name)
+ #endif
+ }
+}
+{% endif %}
+{% if resourceCount.data > 0 %}
+
+{{accessModifier}} struct {{dataType}} {
+ {{accessModifier}} fileprivate(set) var name: String
+
+ @available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *)
+ {{accessModifier}} var data: NSDataAsset {
+ return NSDataAsset(asset: self)
+ }
+}
+
+@available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *)
+{{accessModifier}} extension NSDataAsset {
+ convenience init!(asset: {{dataType}}) {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ #if os(iOS) || os(tvOS) || os(watchOS)
+ self.init(name: asset.name, bundle: bundle)
+ #elseif os(macOS)
+ self.init(name: NSDataAsset.Name(asset.name), bundle: bundle)
+ #endif
+ }
+}
+{% endif %}
+{% if resourceCount.image > 0 %}
+
+{{accessModifier}} struct {{imageType}} {
+ {{accessModifier}} fileprivate(set) var name: String
+
+ #if os(macOS)
+ {{accessModifier}} typealias Image = NSImage
+ #elseif os(iOS) || os(tvOS) || os(watchOS)
+ {{accessModifier}} typealias Image = UIImage
+ #endif
+
+ @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
+ {{accessModifier}} var image: Image {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ #if os(iOS) || os(tvOS)
+ let image = Image(named: name, in: bundle, compatibleWith: nil)
+ #elseif os(macOS)
+ let name = NSImage.Name(self.name)
+ let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
+ #elseif os(watchOS)
+ let image = Image(named: name)
+ #endif
+ guard let result = image else {
+ fatalError("Unable to load image asset named \(name).")
+ }
+ return result
+ }
+
+ #if os(iOS) || os(tvOS)
+ @available(iOS 8.0, tvOS 9.0, *)
+ {{accessModifier}} func image(compatibleWith traitCollection: UITraitCollection) -> Image {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
+ fatalError("Unable to load image asset named \(name).")
+ }
+ return result
+ }
+ #endif
+}
+
+{{accessModifier}} extension {{imageType}}.Image {
+ @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
+ @available(macOS, deprecated,
+ message: "This initializer is unsafe on macOS, please use the {{imageType}}.image property")
+ convenience init!(asset: {{imageType}}) {
+ #if os(iOS) || os(tvOS)
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ self.init(named: asset.name, in: bundle, compatibleWith: nil)
+ #elseif os(macOS)
+ self.init(named: NSImage.Name(asset.name))
+ #elseif os(watchOS)
+ self.init(named: asset.name)
+ #endif
+ }
+}
+{% endif %}
+{% if resourceCount.symbol > 0 %}
+
+{{accessModifier}} struct {{symbolType}} {
+ {{accessModifier}} fileprivate(set) var name: String
+
+ #if os(iOS) || os(tvOS) || os(watchOS)
+ @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
+ {{accessModifier}} typealias Configuration = UIImage.SymbolConfiguration
+ {{accessModifier}} typealias Image = UIImage
+
+ @available(iOS 12.0, tvOS 12.0, watchOS 5.0, *)
+ {{accessModifier}} var image: Image {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ #if os(iOS) || os(tvOS)
+ let image = Image(named: name, in: bundle, compatibleWith: nil)
+ #elseif os(watchOS)
+ let image = Image(named: name)
+ #endif
+ guard let result = image else {
+ fatalError("Unable to load symbol asset named \(name).")
+ }
+ return result
+ }
+
+ @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
+ {{accessModifier}} func image(with configuration: Configuration) -> Image {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ guard let result = Image(named: name, in: bundle, with: configuration) else {
+ fatalError("Unable to load symbol asset named \(name).")
+ }
+ return result
+ }
+ #endif
+}
+{% endif %}
+{% if not param.bundle %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No assets found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/xcassets/swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/xcassets/swift5.stencil
new file mode 100644
index 0000000..42df7be
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/xcassets/swift5.stencil
@@ -0,0 +1,337 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if catalogs %}
+{% set enumName %}{{param.enumName|default:"Asset"}}{% endset %}
+{% set arResourceGroupType %}{{param.arResourceGroupTypeName|default:"ARResourceGroupAsset"}}{% endset %}
+{% set colorType %}{{param.colorTypeName|default:"ColorAsset"}}{% endset %}
+{% set dataType %}{{param.dataTypeName|default:"DataAsset"}}{% endset %}
+{% set imageType %}{{param.imageTypeName|default:"ImageAsset"}}{% endset %}
+{% set symbolType %}{{param.symbolTypeName|default:"SymbolAsset"}}{% endset %}
+{% set forceNamespaces %}{{param.forceProvidesNamespaces|default:"false"}}{% endset %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+#if os(macOS)
+ import AppKit
+#elseif os(iOS)
+{% if resourceCount.arresourcegroup > 0 %}
+ import ARKit
+{% endif %}
+ import UIKit
+#elseif os(tvOS) || os(watchOS)
+ import UIKit
+#endif
+
+// Deprecated typealiases
+{% if resourceCount.color > 0 %}
+@available(*, deprecated, renamed: "{{colorType}}.Color", message: "This typealias will be removed in SwiftGen 7.0")
+{{accessModifier}} typealias {{param.colorAliasName|default:"AssetColorTypeAlias"}} = {{colorType}}.Color
+{% endif %}
+{% if resourceCount.image > 0 %}
+@available(*, deprecated, renamed: "{{imageType}}.Image", message: "This typealias will be removed in SwiftGen 7.0")
+{{accessModifier}} typealias {{param.imageAliasName|default:"AssetImageTypeAlias"}} = {{imageType}}.Image
+{% endif %}
+
+// swiftlint:disable superfluous_disable_command file_length implicit_return
+
+// MARK: - Asset Catalogs
+
+{% macro enumBlock assets %}
+ {% call casesBlock assets %}
+ {% if param.allValues %}
+
+ // swiftlint:disable trailing_comma
+ {% if resourceCount.arresourcegroup > 0 %}
+ {{accessModifier}} static let allResourceGroups: [{{arResourceGroupType}}] = [
+ {% filter indent:2 %}{% call allValuesBlock assets "arresourcegroup" "" %}{% endfilter %}
+ ]
+ {% endif %}
+ {% if resourceCount.color > 0 %}
+ {{accessModifier}} static let allColors: [{{colorType}}] = [
+ {% filter indent:2 %}{% call allValuesBlock assets "color" "" %}{% endfilter %}
+ ]
+ {% endif %}
+ {% if resourceCount.data > 0 %}
+ {{accessModifier}} static let allDataAssets: [{{dataType}}] = [
+ {% filter indent:2 %}{% call allValuesBlock assets "data" "" %}{% endfilter %}
+ ]
+ {% endif %}
+ {% if resourceCount.image > 0 %}
+ {{accessModifier}} static let allImages: [{{imageType}}] = [
+ {% filter indent:2 %}{% call allValuesBlock assets "image" "" %}{% endfilter %}
+ ]
+ {% endif %}
+ {% if resourceCount.symbol > 0 %}
+ {{accessModifier}} static let allSymbols: [{{symbolType}}] = [
+ {% filter indent:2 %}{% call allValuesBlock assets "symbol" "" %}{% endfilter %}
+ ]
+ {% endif %}
+ // swiftlint:enable trailing_comma
+ {% endif %}
+{% endmacro %}
+{% macro casesBlock assets %}
+ {% for asset in assets %}
+ {% if asset.type == "arresourcegroup" %}
+ {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{arResourceGroupType}}(name: "{{asset.value}}")
+ {% elif asset.type == "color" %}
+ {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{colorType}}(name: "{{asset.value}}")
+ {% elif asset.type == "data" %}
+ {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{dataType}}(name: "{{asset.value}}")
+ {% elif asset.type == "image" %}
+ {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{imageType}}(name: "{{asset.value}}")
+ {% elif asset.type == "symbol" %}
+ {{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{symbolType}}(name: "{{asset.value}}")
+ {% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
+ {{accessModifier}} enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %}
+ }
+ {% elif asset.items %}
+ {% call casesBlock asset.items %}
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+{% macro allValuesBlock assets filter prefix %}
+ {% for asset in assets %}
+ {% if asset.type == filter %}
+ {{prefix}}{{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}},
+ {% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
+ {% set prefix2 %}{{prefix}}{{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.{% endset %}
+ {% call allValuesBlock asset.items filter prefix2 %}
+ {% elif asset.items %}
+ {% call allValuesBlock asset.items filter prefix %}
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+// swiftlint:disable identifier_name line_length nesting type_body_length type_name
+{{accessModifier}} enum {{enumName}} {
+ {% if catalogs.count > 1 or param.forceFileNameEnum %}
+ {% for catalog in catalogs %}
+ {{accessModifier}} enum {{catalog.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call enumBlock catalog.assets %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call enumBlock catalogs.first.assets %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length nesting type_body_length type_name
+
+// MARK: - Implementation Details
+{% if resourceCount.arresourcegroup > 0 %}
+
+{{accessModifier}} struct {{arResourceGroupType}} {
+ {{accessModifier}} fileprivate(set) var name: String
+
+ #if os(iOS)
+ @available(iOS 11.3, *)
+ {{accessModifier}} var referenceImages: Set {
+ return ARReferenceImage.referenceImages(in: self)
+ }
+
+ @available(iOS 12.0, *)
+ {{accessModifier}} var referenceObjects: Set {
+ return ARReferenceObject.referenceObjects(in: self)
+ }
+ #endif
+}
+
+#if os(iOS)
+@available(iOS 11.3, *)
+{{accessModifier}} extension ARReferenceImage {
+ static func referenceImages(in asset: {{arResourceGroupType}}) -> Set {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ return referenceImages(inGroupNamed: asset.name, bundle: bundle) ?? Set()
+ }
+}
+
+@available(iOS 12.0, *)
+{{accessModifier}} extension ARReferenceObject {
+ static func referenceObjects(in asset: {{arResourceGroupType}}) -> Set {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ return referenceObjects(inGroupNamed: asset.name, bundle: bundle) ?? Set()
+ }
+}
+#endif
+{% endif %}
+{% if resourceCount.color > 0 %}
+
+{{accessModifier}} final class {{colorType}} {
+ {{accessModifier}} fileprivate(set) var name: String
+
+ #if os(macOS)
+ {{accessModifier}} typealias Color = NSColor
+ #elseif os(iOS) || os(tvOS) || os(watchOS)
+ {{accessModifier}} typealias Color = UIColor
+ #endif
+
+ @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
+ {{accessModifier}} private(set) lazy var color: Color = {
+ guard let color = Color(asset: self) else {
+ fatalError("Unable to load color asset named \(name).")
+ }
+ return color
+ }()
+
+ #if os(iOS) || os(tvOS)
+ @available(iOS 11.0, tvOS 11.0, *)
+ {{accessModifier}} func color(compatibleWith traitCollection: UITraitCollection) -> Color {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ guard let color = Color(named: name, in: bundle, compatibleWith: traitCollection) else {
+ fatalError("Unable to load color asset named \(name).")
+ }
+ return color
+ }
+ #endif
+
+ fileprivate init(name: String) {
+ self.name = name
+ }
+}
+
+{{accessModifier}} extension {{colorType}}.Color {
+ @available(iOS 11.0, tvOS 11.0, watchOS 4.0, macOS 10.13, *)
+ convenience init?(asset: {{colorType}}) {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ #if os(iOS) || os(tvOS)
+ self.init(named: asset.name, in: bundle, compatibleWith: nil)
+ #elseif os(macOS)
+ self.init(named: NSColor.Name(asset.name), bundle: bundle)
+ #elseif os(watchOS)
+ self.init(named: asset.name)
+ #endif
+ }
+}
+{% endif %}
+{% if resourceCount.data > 0 %}
+
+{{accessModifier}} struct {{dataType}} {
+ {{accessModifier}} fileprivate(set) var name: String
+
+ @available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *)
+ {{accessModifier}} var data: NSDataAsset {
+ guard let data = NSDataAsset(asset: self) else {
+ fatalError("Unable to load data asset named \(name).")
+ }
+ return data
+ }
+}
+
+@available(iOS 9.0, tvOS 9.0, watchOS 6.0, macOS 10.11, *)
+{{accessModifier}} extension NSDataAsset {
+ convenience init?(asset: {{dataType}}) {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ #if os(iOS) || os(tvOS) || os(watchOS)
+ self.init(name: asset.name, bundle: bundle)
+ #elseif os(macOS)
+ self.init(name: NSDataAsset.Name(asset.name), bundle: bundle)
+ #endif
+ }
+}
+{% endif %}
+{% if resourceCount.image > 0 %}
+
+{{accessModifier}} struct {{imageType}} {
+ {{accessModifier}} fileprivate(set) var name: String
+
+ #if os(macOS)
+ {{accessModifier}} typealias Image = NSImage
+ #elseif os(iOS) || os(tvOS) || os(watchOS)
+ {{accessModifier}} typealias Image = UIImage
+ #endif
+
+ @available(iOS 8.0, tvOS 9.0, watchOS 2.0, macOS 10.7, *)
+ {{accessModifier}} var image: Image {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ #if os(iOS) || os(tvOS)
+ let image = Image(named: name, in: bundle, compatibleWith: nil)
+ #elseif os(macOS)
+ let name = NSImage.Name(self.name)
+ let image = (bundle == .main) ? NSImage(named: name) : bundle.image(forResource: name)
+ #elseif os(watchOS)
+ let image = Image(named: name)
+ #endif
+ guard let result = image else {
+ fatalError("Unable to load image asset named \(name).")
+ }
+ return result
+ }
+
+ #if os(iOS) || os(tvOS)
+ @available(iOS 8.0, tvOS 9.0, *)
+ {{accessModifier}} func image(compatibleWith traitCollection: UITraitCollection) -> Image {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ guard let result = Image(named: name, in: bundle, compatibleWith: traitCollection) else {
+ fatalError("Unable to load image asset named \(name).")
+ }
+ return result
+ }
+ #endif
+}
+
+{{accessModifier}} extension {{imageType}}.Image {
+ @available(iOS 8.0, tvOS 9.0, watchOS 2.0, *)
+ @available(macOS, deprecated,
+ message: "This initializer is unsafe on macOS, please use the {{imageType}}.image property")
+ convenience init?(asset: {{imageType}}) {
+ #if os(iOS) || os(tvOS)
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ self.init(named: asset.name, in: bundle, compatibleWith: nil)
+ #elseif os(macOS)
+ self.init(named: NSImage.Name(asset.name))
+ #elseif os(watchOS)
+ self.init(named: asset.name)
+ #endif
+ }
+}
+{% endif %}
+{% if resourceCount.symbol > 0 %}
+
+{{accessModifier}} struct {{symbolType}} {
+ {{accessModifier}} fileprivate(set) var name: String
+
+ #if os(iOS) || os(tvOS) || os(watchOS)
+ @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
+ {{accessModifier}} typealias Configuration = UIImage.SymbolConfiguration
+ {{accessModifier}} typealias Image = UIImage
+
+ @available(iOS 12.0, tvOS 12.0, watchOS 5.0, *)
+ {{accessModifier}} var image: Image {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ #if os(iOS) || os(tvOS)
+ let image = Image(named: name, in: bundle, compatibleWith: nil)
+ #elseif os(watchOS)
+ let image = Image(named: name)
+ #endif
+ guard let result = image else {
+ fatalError("Unable to load symbol asset named \(name).")
+ }
+ return result
+ }
+
+ @available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)
+ {{accessModifier}} func image(with configuration: Configuration) -> Image {
+ let bundle = {{param.bundle|default:"BundleToken.bundle"}}
+ guard let result = Image(named: name, in: bundle, with: configuration) else {
+ fatalError("Unable to load symbol asset named \(name).")
+ }
+ return result
+ }
+ #endif
+}
+{% endif %}
+{% if not param.bundle %}
+
+// swiftlint:disable convenience_type
+private final class BundleToken {
+ static let bundle: Bundle = {
+ #if SWIFT_PACKAGE
+ return Bundle.module
+ #else
+ return Bundle(for: BundleToken.self)
+ #endif
+ }()
+}
+// swiftlint:enable convenience_type
+{% endif %}
+{% else %}
+// No assets found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/yaml/inline-swift4.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/yaml/inline-swift4.stencil
new file mode 100644
index 0000000..9cc2aa3
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/yaml/inline-swift4.stencil
@@ -0,0 +1,92 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if files %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set documentPrefix %}{{param.documentName|default:"Document"}}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - YAML Files
+{% macro fileBlock file %}
+ {% if file.documents.count > 1 %}
+ {% for document in file.documents %}
+ {% set documentName %}{{documentPrefix}}{{forloop.counter}}{% endset %}
+ {{accessModifier}} enum {{documentName|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call documentBlock file document %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call documentBlock file file.documents.first %}
+ {% endif %}
+{% endmacro %}
+{% macro documentBlock file document %}
+ {% set rootType %}{% call typeBlock document.metadata %}{% endset %}
+ {% if document.metadata.type == "Array" %}
+ {{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% elif document.metadata.type == "Dictionary" %}
+ {% for key,value in document.metadata.properties %}
+ {{accessModifier}} {% call propertyBlock key value document.data %}
+ {% endfor %}
+ {% else %}
+ {{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% endif %}
+{% endmacro %}
+{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "Array" %}
+ [{% call typeBlock metadata.element %}]
+ {% elif metadata.type == "Dictionary" %}
+ [String: Any]
+ {% elif metadata.type == "Optional" %}
+ Any?
+ {% else %}
+ {{metadata.type}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
+ {% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set propertyType %}{% call typeBlock metadata %}{% endset %}
+ static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
+{% endfilter %}{% endmacro %}
+{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "String" %}
+ "{{ value }}"
+ {% elif metadata.type == "Optional" %}
+ nil
+ {% elif metadata.type == "Array" and value %}
+ [{% for value in value %}
+ {% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}]
+ {% elif metadata.type == "Dictionary" %}
+ [{% for key,value in value %}
+ "{{key}}": {% call valueBlock value metadata.properties[key] %}
+ {{ ", " if not forloop.last }}
+ {% empty %}
+ :
+ {% endfor %}]
+ {% elif metadata.type == "Bool" %}
+ {% if value %}true{% else %}false{% endif %}
+ {% else %}
+ {{ value }}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+
+// swiftlint:disable identifier_name line_length number_separator type_body_length
+{{accessModifier}} enum {{param.enumName|default:"YAMLFiles"}} {
+ {% if files.count > 1 or param.forceFileNameEnum %}
+ {% for file in files %}
+ {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call fileBlock files.first %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length number_separator type_body_length
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/yaml/inline-swift5.stencil b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/yaml/inline-swift5.stencil
new file mode 100644
index 0000000..9cc2aa3
--- /dev/null
+++ b/.swiftgen/bin/SwiftGen_SwiftGenCLI.bundle/Contents/Resources/templates/yaml/inline-swift5.stencil
@@ -0,0 +1,92 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if files %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+{% set documentPrefix %}{{param.documentName|default:"Document"}}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command
+// swiftlint:disable file_length
+
+// MARK: - YAML Files
+{% macro fileBlock file %}
+ {% if file.documents.count > 1 %}
+ {% for document in file.documents %}
+ {% set documentName %}{{documentPrefix}}{{forloop.counter}}{% endset %}
+ {{accessModifier}} enum {{documentName|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call documentBlock file document %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call documentBlock file file.documents.first %}
+ {% endif %}
+{% endmacro %}
+{% macro documentBlock file document %}
+ {% set rootType %}{% call typeBlock document.metadata %}{% endset %}
+ {% if document.metadata.type == "Array" %}
+ {{accessModifier}} static let items: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% elif document.metadata.type == "Dictionary" %}
+ {% for key,value in document.metadata.properties %}
+ {{accessModifier}} {% call propertyBlock key value document.data %}
+ {% endfor %}
+ {% else %}
+ {{accessModifier}} static let value: {{rootType}} = {% call valueBlock document.data document.metadata %}
+ {% endif %}
+{% endmacro %}
+{% macro typeBlock metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "Array" %}
+ [{% call typeBlock metadata.element %}]
+ {% elif metadata.type == "Dictionary" %}
+ [String: Any]
+ {% elif metadata.type == "Optional" %}
+ Any?
+ {% else %}
+ {{metadata.type}}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+{% macro propertyBlock key metadata data %}{% filter removeNewlines:"leading" %}
+ {% set propertyName %}{{key|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+ {% set propertyType %}{% call typeBlock metadata %}{% endset %}
+ static let {{propertyName}}: {{propertyType}} = {% call valueBlock data[key] metadata %}
+{% endfilter %}{% endmacro %}
+{% macro valueBlock value metadata %}{% filter removeNewlines:"leading" %}
+ {% if metadata.type == "String" %}
+ "{{ value }}"
+ {% elif metadata.type == "Optional" %}
+ nil
+ {% elif metadata.type == "Array" and value %}
+ [{% for value in value %}
+ {% call valueBlock value metadata.element.items[forloop.counter0]|default:metadata.element %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}]
+ {% elif metadata.type == "Dictionary" %}
+ [{% for key,value in value %}
+ "{{key}}": {% call valueBlock value metadata.properties[key] %}
+ {{ ", " if not forloop.last }}
+ {% empty %}
+ :
+ {% endfor %}]
+ {% elif metadata.type == "Bool" %}
+ {% if value %}true{% else %}false{% endif %}
+ {% else %}
+ {{ value }}
+ {% endif %}
+{% endfilter %}{% endmacro %}
+
+// swiftlint:disable identifier_name line_length number_separator type_body_length
+{{accessModifier}} enum {{param.enumName|default:"YAMLFiles"}} {
+ {% if files.count > 1 or param.forceFileNameEnum %}
+ {% for file in files %}
+ {{accessModifier}} enum {{file.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call fileBlock file %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call fileBlock files.first %}
+ {% endif %}
+}
+// swiftlint:enable identifier_name line_length number_separator type_body_length
+{% else %}
+// No files found
+{% endif %}
diff --git a/.swiftgen/bin/swiftgen b/.swiftgen/bin/swiftgen
new file mode 100755
index 0000000..1bc2b87
Binary files /dev/null and b/.swiftgen/bin/swiftgen differ
diff --git a/.swiftgen/templates/fonts.stencil b/.swiftgen/templates/fonts.stencil
new file mode 100644
index 0000000..c6148bf
--- /dev/null
+++ b/.swiftgen/templates/fonts.stencil
@@ -0,0 +1,29 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if families %}
+import SwiftUI
+{% for family in families %}
+{% set identifierName %}{{family.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}{% endset %}
+{% set styleTypeName %}{{family.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}Style{% endset %}
+
+extension Font {
+ public static func {{identifierName}}(_ style: {{styleTypeName}}, fixedSize: CGFloat) -> Font {
+ return Font.custom(style.rawValue, fixedSize: fixedSize)
+ }
+
+ public static func {{identifierName}}(_ style: {{styleTypeName}}, size: CGFloat, relativeTo textStyle: TextStyle = .body) -> Font {
+ return Font.custom(style.rawValue, size: size, relativeTo: textStyle)
+ }
+
+ public enum {{styleTypeName}}: String {
+ {% for font in family.fonts %}
+ case {{font.style|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = "{{font.name}}"
+ {% endfor %}
+ }
+}
+{% endfor %}
+{% else %}
+// No fonts found
+{% endif %}
+// swiftlint:enable all
diff --git a/.swiftgen/templates/strings.stencil b/.swiftgen/templates/strings.stencil
new file mode 100644
index 0000000..daeeea4
--- /dev/null
+++ b/.swiftgen/templates/strings.stencil
@@ -0,0 +1,85 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if tables.count > 0 %}
+{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
+import Foundation
+
+// swiftlint:disable superfluous_disable_command file_length implicit_return
+
+// MARK: - Strings
+
+{% macro parametersBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ {% if type == "String" %}
+ _ p{{forloop.counter}}: Any
+ {% else %}
+ _ p{{forloop.counter}}: {{type}}
+ {% endif %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro argumentsBlock types %}{% filter removeNewlines:"leading" %}
+ {% for type in types %}
+ {% if type == "String" %}
+ String(describing: p{{forloop.counter}})
+ {% elif type == "UnsafeRawPointer" %}
+ Int(bitPattern: p{{forloop.counter}})
+ {% else %}
+ p{{forloop.counter}}
+ {% endif %}
+ {{ ", " if not forloop.last }}
+ {% endfor %}
+{% endfilter %}{% endmacro %}
+{% macro recursiveBlock table item %}
+ {% for string in item.strings %}
+ {% if not param.noComments %}
+ {% for line in string.translation|split:"\n" %}
+ /// {{line}}
+ {% endfor %}
+ {% endif %}
+ {% if string.types %}
+ {{accessModifier}} static func {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}({% call parametersBlock string.types %}) -> String {
+ {{enumName}}.tr("{{table}}", "{{string.key}}", {% call argumentsBlock string.types %})
+ }
+ {% else %}
+ {{accessModifier}} static var {{string.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}}: String { {{enumName}}.tr("{{table}}", "{{string.key}}") }
+ {% endif %}
+ {% endfor %}
+ {% for child in item.children %}
+
+ {{accessModifier}} enum {{child.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call recursiveBlock table child %}{% endfilter %}
+ }
+ {% endfor %}
+{% endmacro %}
+// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
+// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
+{% set enumName %}{{param.enumName|default:"L10n"}}{% endset %}
+{{accessModifier}} enum {{enumName}} {
+ {% if tables.count > 1 or param.forceFileNameEnum %}
+ {% for table in tables %}
+ {{accessModifier}} enum {{table.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call recursiveBlock table.name table.levels %}{% endfilter %}
+ }
+ {% endfor %}
+ {% else %}
+ {% call recursiveBlock tables.first.name tables.first.levels %}
+ {% endif %}
+}
+// swiftlint:enable explicit_type_interface function_parameter_count identifier_name line_length
+// swiftlint:enable nesting type_body_length type_name vertical_whitespace_opening_braces
+
+// MARK: - Implementation Details
+import Localize_Swift
+extension {{enumName}} {
+ static func tr(_ table: String, _ key: String, _ args: CVarArg...) -> String {
+ let selectedLanguage = Localize.currentLanguage()
+ guard let path = Bundle.main.path(forResource: selectedLanguage, ofType: "lproj"),
+ let bundle = Bundle(path: path) else { return "Setup language error" }
+ return NSLocalizedString(key, tableName: table, bundle: bundle, comment: "")
+ }
+}
+
+{% endif %}
+// swiftlint: enable all
diff --git a/.swiftgen/templates/xcassets.stencil b/.swiftgen/templates/xcassets.stencil
new file mode 100644
index 0000000..b6283e7
--- /dev/null
+++ b/.swiftgen/templates/xcassets.stencil
@@ -0,0 +1,48 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if catalogs %}
+import SwiftUI
+{% macro casesBlock assets %}
+ {% for asset in assets %}
+ {% if asset.items and asset.isNamespaced == "true" %}
+ public enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %}
+ }
+ {% elif asset.items %}
+ {% call casesBlock asset.items %}
+ {% elif asset.type == "color" %}
+ public static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = Color("{{asset.value}}")
+ {% elif asset.type == "image" %}
+ public static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = Image("{{asset.value}}")
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+{% for catalog in catalogs %}
+{% if catalog.name == "Colors" %}
+
+extension Color {
+ {% for catalog in catalogs %}
+ {% if catalog.name == "Colors" %}
+ {% call casesBlock catalog.assets %}
+ {% endif %}
+ {% endfor %}
+}
+{% endif %}
+{% endfor %}
+{% for catalog in catalogs %}
+{% if catalog.name == "Images" %}
+
+extension Image {
+ {% for catalog in catalogs %}
+ {% if catalog.name == "Images" %}
+ {% call casesBlock catalog.assets %}
+ {% endif %}
+ {% endfor %}
+}
+{% endif %}
+{% endfor %}
+{% else %}
+// No assets found
+{% endif %}
+// swiftlint: enable all
diff --git a/.swiftgen/templates/xcassets_strings.stencil b/.swiftgen/templates/xcassets_strings.stencil
new file mode 100644
index 0000000..a5540bc
--- /dev/null
+++ b/.swiftgen/templates/xcassets_strings.stencil
@@ -0,0 +1,36 @@
+// swiftlint:disable all
+// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
+
+{% if catalogs %}
+import Foundation
+
+typealias AssetStrings = String
+{% macro casesBlock assets %}
+ {% for asset in assets %}
+ {% if asset.items and asset.isNamespaced == "true" %}
+ public enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
+ {% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %}
+ }
+ {% elif asset.items %}
+ {% call casesBlock asset.items %}
+ {% elif asset.type == "image" %}
+ public static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = String("{{asset.value}}")
+ {% endif %}
+ {% endfor %}
+{% endmacro %}
+{% for catalog in catalogs %}
+{% if catalog.name == "Images" %}
+
+extension String {
+ {% for catalog in catalogs %}
+ {% if catalog.name == "Images" %}
+ {% call casesBlock catalog.assets %}
+ {% endif %}
+ {% endfor %}
+}
+{% endif %}
+{% endfor %}
+{% else %}
+// No assets found
+{% endif %}
+// swiftlint: enable all
diff --git a/.swiftlint.yml b/.swiftlint.yml
new file mode 100644
index 0000000..5eafdf8
--- /dev/null
+++ b/.swiftlint.yml
@@ -0,0 +1,115 @@
+---
+colon:
+ severity: error
+
+line_length:
+ ignores_comments: true
+ warning: 260
+ error: 300
+
+type_body_length:
+ warning: 300
+ error: 500
+
+file_length:
+ warning: 800
+ error: 1000
+
+function_parameter_count:
+ warning: 20
+ error: 30
+
+function_body_length:
+ warning: 120
+ error: 150
+
+cyclomatic_complexity:
+ warning: 40
+ error: 50
+
+nesting:
+ type_level:
+ warning: 3
+ error: 6
+ function_level:
+ warning: 500
+ error: 10
+
+vertical_parameter_alignment:
+ severity: warning
+
+implicitly_unwrapped_optional:
+ severity: warning
+
+force_unwrapping:
+ severity: error
+
+vertical_whitespace:
+ severity: error
+
+force_try:
+ severity: error
+
+trailing_semicolon:
+ severity: error
+
+type_name:
+ min_length: 3
+ severity: warning
+
+identifier_name:
+ min_length: 3
+ max_length: 60
+ # validates_start_with_lowercase: true
+ allowed_symbols: "_"
+ excluded:
+ - iv
+ - id
+ - ip
+ - on
+ - ui
+ - x
+ - y
+ - tz
+ - to
+ - db
+
+# Disable rules from the default enabled set.
+disabled_rules:
+ - trailing_whitespace
+ - implicit_getter
+ - redundant_string_enum_value
+ - switch_case_alignment
+
+# Enable rules not from the default set.
+opt_in_rules:
+ # - function_default_parameter_at_end
+ - empty_count
+ - indentation_width
+ # - index_at_zero
+ - legacy_constant
+ # - implicitly_unwrapped_optional
+ - force_unwrapping
+ # - no header
+ - file_header
+ # - for force unwrapping
+ - implicitly_unwrapped_optional
+ - vertical_parameter_alignment_on_call
+ - vertical_whitespace_between_cases
+ - vertical_whitespace_closing_braces
+ - vertical_whitespace_opening_braces
+
+# Acts as a whitelist, only the rules specified in this list will be enabled. Can not be specified alongside disabled_rules or opt_in_rules.
+only_rules:
+
+# This is an entirely separate list of rules that are only run by the analyze command. All analyzer rules are opt-in, so this is the only configurable rule list (there is no disabled/whitelist equivalent).
+analyzer_rules:
+ - unused_import
+ - unused_declaration
+
+unused_declaration:
+ include_public_and_open: true
+
+# paths to ignore during linting. Takes precedence over `included`.
+excluded:
+ - SomePathHere
diff --git a/ConversationsClassic/AppCore/Actions/AccountsActions.swift b/ConversationsClassic/AppCore/Actions/AccountsActions.swift
new file mode 100644
index 0000000..d7dc21a
--- /dev/null
+++ b/ConversationsClassic/AppCore/Actions/AccountsActions.swift
@@ -0,0 +1,10 @@
+enum AccountsAction: Codable {
+ case accountsListUpdated(accounts: [Account])
+
+ case goTo(AccountNavigationState)
+
+ case tryAddAccountWithCredentials(login: String, password: String)
+ case addAccountError(jid: String, reason: String?)
+
+ case makeAccountPermanent(account: Account)
+}
diff --git a/ConversationsClassic/AppCore/Actions/AppActions.swift b/ConversationsClassic/AppCore/Actions/AppActions.swift
new file mode 100644
index 0000000..e97afdd
--- /dev/null
+++ b/ConversationsClassic/AppCore/Actions/AppActions.swift
@@ -0,0 +1,12 @@
+enum AppAction: Codable {
+ case empty
+ case flushState
+ case changeFlow(_ flow: AppFlow)
+
+ case startAction(_ action: StartAction)
+ case databaseAction(_ action: DatabaseAction)
+ case accountsAction(_ action: AccountsAction)
+ case xmppAction(_ action: XMPPAction)
+ case rostersAction(_ action: RostersAction)
+ case chatsAction(_ action: ChatsAction)
+}
diff --git a/ConversationsClassic/AppCore/Actions/ChatsActions.swift b/ConversationsClassic/AppCore/Actions/ChatsActions.swift
new file mode 100644
index 0000000..e4a720b
--- /dev/null
+++ b/ConversationsClassic/AppCore/Actions/ChatsActions.swift
@@ -0,0 +1,3 @@
+enum ChatsAction: Codable {
+ case chatsListUpdated(chats: [Chat])
+}
diff --git a/ConversationsClassic/AppCore/Actions/DatabaseActions.swift b/ConversationsClassic/AppCore/Actions/DatabaseActions.swift
new file mode 100644
index 0000000..5381970
--- /dev/null
+++ b/ConversationsClassic/AppCore/Actions/DatabaseActions.swift
@@ -0,0 +1,8 @@
+enum DatabaseAction: Codable {
+ case storedAccountsLoaded(accounts: [Account])
+ case loadingStoredAccountsFailed
+ case updateAccountFailed
+
+ case storedRostersLoaded(rosters: [Roster])
+ case storedChatsLoaded(chats: [Chat])
+}
diff --git a/ConversationsClassic/AppCore/Actions/RostersActions.swift b/ConversationsClassic/AppCore/Actions/RostersActions.swift
new file mode 100644
index 0000000..b4ba796
--- /dev/null
+++ b/ConversationsClassic/AppCore/Actions/RostersActions.swift
@@ -0,0 +1,12 @@
+enum RostersAction: Codable {
+ case addRoster(ownerJID: String, contactJID: String, name: String?, groups: [String])
+ case addRosterDone(jid: String)
+ case addRosterError(reason: String)
+
+ case rostersListUpdated([Roster])
+
+ case markRosterAsLocallyDeleted(ownerJID: String, contactJID: String)
+ case unmarkRosterAsLocallyDeleted(ownerJID: String, contactJID: String)
+ case deleteRoster(ownerJID: String, contactJID: String)
+ case rosterDeletingFailed(reason: String)
+}
diff --git a/ConversationsClassic/AppCore/Actions/StartActions.swift b/ConversationsClassic/AppCore/Actions/StartActions.swift
new file mode 100644
index 0000000..7d41c7c
--- /dev/null
+++ b/ConversationsClassic/AppCore/Actions/StartActions.swift
@@ -0,0 +1,5 @@
+enum StartAction: Codable {
+ case loadStoredAccounts
+
+ case goTo(StartNavigationState)
+}
diff --git a/ConversationsClassic/AppCore/Actions/XMPPActions.swift b/ConversationsClassic/AppCore/Actions/XMPPActions.swift
new file mode 100644
index 0000000..3c2a3a8
--- /dev/null
+++ b/ConversationsClassic/AppCore/Actions/XMPPActions.swift
@@ -0,0 +1,3 @@
+enum XMPPAction: Codable {
+ case clientConnectionChanged(jid: String, state: ConnectionStatus)
+}
diff --git a/ConversationsClassic/AppCore/AppStore.swift b/ConversationsClassic/AppCore/AppStore.swift
new file mode 100644
index 0000000..443a049
--- /dev/null
+++ b/ConversationsClassic/AppCore/AppStore.swift
@@ -0,0 +1,95 @@
+/*
+ In 99,99% of time YOU DON'T NEEDED TO CHANGE ANYTHING in this file!
+
+ This file declare global state object for whole app
+ and reducers/actions/middleware types. Core of app.
+ */
+import Combine
+import Foundation
+
+typealias Stateable = Codable & Equatable
+typealias AppStore = Store
+typealias Reducer = (inout State, Action) -> Void
+typealias Middleware = (State, Action) -> AnyPublisher?
+
+final class Store: ObservableObject {
+ // Fake variable for be able to trigger SwiftUI redraw after app state completely changed
+ // this hack is needed because @Published wrapper sends signals on "willSet:"
+ @Published private var dumbVar: UUID = .init()
+
+ // State is read-only (changes only over reducers)
+ private(set) var state: State {
+ didSet {
+ DispatchQueue.main.async { [weak self] in
+ self?.dumbVar = UUID()
+ }
+ } // signal to SwiftUI only when new state did set
+ }
+
+ // Serial queue for performing any actions sequentially
+ private let serialQueue = DispatchQueue(label: "im.narayana.conversations.classic.serial.queue", qos: .userInteractive)
+
+ private let reducer: Reducer
+ private let middlewares: [Middleware]
+ private var middlewareCancellables: Set = []
+
+ // Init
+ init(
+ initialState: State,
+ reducer: @escaping Reducer,
+ middlewares: [Middleware] = []
+ ) {
+ state = initialState
+ self.reducer = reducer
+ self.middlewares = middlewares
+ }
+
+ // Run reducers/middlewares
+ func dispatch(_ action: Action) {
+ serialQueue.sync { [weak self] in
+ guard let wSelf = self else { return }
+ let newState = wSelf.dispatch(wSelf.state, action)
+ wSelf.state = newState
+ }
+ }
+
+ private func dispatch(_ currentState: State, _ action: Action) -> State {
+ let startTime = CFAbsoluteTimeGetCurrent()
+
+ // Do reducing
+ var newState = currentState
+ reducer(&newState, action)
+
+ // Dispatch all middleware functions
+ for middleware in middlewares {
+ guard let middleware = middleware(newState, action) else {
+ break
+ }
+ middleware
+ .receive(on: DispatchQueue.main)
+ .sink(receiveValue: dispatch)
+ .store(in: &middlewareCancellables)
+ }
+
+ // Check performance
+ let timeElapsed = CFAbsoluteTimeGetCurrent() - startTime
+ if timeElapsed > 0.05 {
+ #if DEBUG
+ print(
+ """
+ --
+ (Ignore this warning ONLY in case, when execution is paused by your breakpoint)
+ 🕐Execution time: \(timeElapsed)
+ ❌WARNING! Some reducers/middlewares work too long! It will lead to issues in production build!
+ Because of execution each action is synchronous the any stuck will reduce performance dramatically.
+ Probably you need check which part of reducer/middleware should be async (wrapped with Futures, as example)
+ --
+ """
+ )
+ #else
+ #endif
+ }
+
+ return newState
+ }
+}
diff --git a/ConversationsClassic/AppCore/Database/Database+Martin.swift b/ConversationsClassic/AppCore/Database/Database+Martin.swift
new file mode 100644
index 0000000..38cc515
--- /dev/null
+++ b/ConversationsClassic/AppCore/Database/Database+Martin.swift
@@ -0,0 +1,222 @@
+import Foundation
+import GRDB
+import Martin
+
+extension Database: MartinsManager {
+ // MARK: - Martin's roster manager
+ func clear(for context: Martin.Context) {
+ print("Clearing roster for context: \(context)")
+ do {
+ try _db.write { db in
+ try Roster
+ .filter(Column("bareJid") == context.userBareJid.stringValue)
+ .deleteAll(db)
+
+ try RosterVersion
+ .filter(Column("bareJid") == context.userBareJid.stringValue)
+ .deleteAll(db)
+ }
+ } catch {
+ logIt(.error, "Error clearing roster: \(error.localizedDescription)")
+ }
+ }
+
+ func items(for context: Martin.Context) -> [any Martin.RosterItemProtocol] {
+ do {
+ let rosters: [Roster] = try _db.read { db in
+ try Roster.filter(Column("bareJid") == context.userBareJid.stringValue).fetchAll(db)
+ }
+ return rosters.map { roster in
+ RosterItemBase(
+ jid: JID(roster.bareJid),
+ name: roster.name,
+ subscription: RosterItemSubscription(rawValue: roster.subscription) ?? .none,
+ groups: roster.data.groups,
+ ask: roster.ask,
+ annotations: roster.data.annotations
+ )
+ }
+ } catch {
+ logIt(.error, "Error fetching roster items: \(error.localizedDescription)")
+ return []
+ }
+ }
+
+ func item(for context: Martin.Context, jid: Martin.JID) -> (any Martin.RosterItemProtocol)? {
+ do {
+ let roster: Roster? = try _db.read { db in
+ try Roster
+ .filter(Column("bareJid") == context.userBareJid.stringValue)
+ .filter(Column("contactBareJid") == jid.stringValue)
+ .fetchOne(db)
+ }
+ if let roster {
+ return RosterItemBase(
+ jid: JID(roster.bareJid),
+ name: roster.name,
+ subscription: RosterItemSubscription(rawValue: roster.subscription) ?? .none,
+ groups: roster.data.groups,
+ ask: roster.ask,
+ annotations: roster.data.annotations
+ )
+ } else {
+ return nil
+ }
+ } catch {
+ logIt(.error, "Error fetching roster item: \(error.localizedDescription)")
+ return nil
+ }
+ }
+
+ func updateItem(for context: Martin.Context, jid: Martin.JID, name: String?, subscription: Martin.RosterItemSubscription, groups: [String], ask: Bool, annotations: [Martin.RosterItemAnnotation]) -> (any Martin.RosterItemProtocol)? {
+ do {
+ let roster = Roster(
+ bareJid: context.userBareJid.stringValue,
+ contactBareJid: jid.stringValue,
+ name: name,
+ subscription: subscription.rawValue,
+ ask: ask,
+ data: DBRosterData(
+ groups: groups,
+ annotations: annotations
+ )
+ )
+ try _db.write { db in
+ try roster.save(db)
+ }
+ return RosterItemBase(jid: jid, name: name, subscription: subscription, groups: groups, ask: ask, annotations: annotations)
+ } catch {
+ logIt(.error, "Error updating roster item: \(error.localizedDescription)")
+ return nil
+ }
+ }
+
+ func deleteItem(for context: Martin.Context, jid: Martin.JID) -> (any Martin.RosterItemProtocol)? {
+ do {
+ let roster: Roster? = try _db.read { db in
+ try Roster
+ .filter(Column("bareJid") == context.userBareJid.stringValue)
+ .filter(Column("contactBareJid") == jid.stringValue)
+ .fetchOne(db)
+ }
+ if let roster {
+ _ = try _db.write { db in
+ try roster.delete(db)
+ }
+ return RosterItemBase(
+ jid: JID(roster.bareJid),
+ name: roster.name,
+ subscription: RosterItemSubscription(rawValue: roster.subscription) ?? .none,
+ groups: roster.data.groups,
+ ask: roster.ask,
+ annotations: roster.data.annotations
+ )
+ } else {
+ return nil
+ }
+ } catch {
+ logIt(.error, "Error fetching roster version: \(error.localizedDescription)")
+ return nil
+ }
+ }
+
+ func version(for context: Martin.Context) -> String? {
+ do {
+ let version: RosterVersion? = try _db.read { db in
+ try RosterVersion
+ .filter(Column("bareJid") == context.userBareJid.stringValue)
+ .fetchOne(db)
+ }
+ return version?.version
+ } catch {
+ logIt(.error, "Error fetching roster version: \(error.localizedDescription)")
+ return nil
+ }
+ }
+
+ func set(version: String?, for context: Martin.Context) {
+ guard let version else { return }
+ do {
+ try _db.write { db in
+ let rosterVersion = RosterVersion(
+ bareJid: context.userBareJid.stringValue,
+ version: version
+ )
+ try rosterVersion.save(db)
+ }
+ } catch {
+ logIt(.error, "Error setting roster version: \(error.localizedDescription)")
+ }
+ }
+
+ func initialize(context _: Martin.Context) {}
+ func deinitialize(context _: Martin.Context) {}
+
+ // MARK: - Martin's chats manager
+ func chats(for context: Martin.Context) -> [any Martin.ChatProtocol] {
+ do {
+ let chats: [Chat] = try _db.read { db in
+ try Chat.filter(Column("account") == context.userBareJid.stringValue).fetchAll(db)
+ }
+ return chats.map { chat in
+ Martin.ChatBase(context: context, jid: BareJID(chat.participant))
+ }
+ } catch {
+ logIt(.error, "Error fetching chats: \(error.localizedDescription)")
+ return []
+ }
+ }
+
+ func chat(for context: Martin.Context, with: Martin.BareJID) -> (any Martin.ChatProtocol)? {
+ do {
+ let chat: Chat? = try _db.read { db in
+ try Chat
+ .filter(Column("account") == context.userBareJid.stringValue)
+ .filter(Column("participant") == with.stringValue)
+ .fetchOne(db)
+ }
+ if chat != nil {
+ return Martin.ChatBase(context: context, jid: with)
+ } else {
+ return nil
+ }
+ } catch {
+ logIt(.error, "Error fetching chat: \(error.localizedDescription)")
+ return nil
+ }
+ }
+
+ func createChat(for context: Martin.Context, with: Martin.BareJID) -> (any Martin.ChatProtocol)? {
+ do {
+ let chat: Chat? = try _db.read { db in
+ try Chat
+ .filter(Column("account") == context.userBareJid.stringValue)
+ .filter(Column("participant") == with.stringValue)
+ .fetchOne(db)
+ }
+ if chat != nil {
+ return Martin.ChatBase(context: context, jid: with)
+ } else {
+ let chat = Chat(
+ id: UUID().uuidString,
+ account: context.userBareJid.stringValue,
+ participant: with.stringValue,
+ type: .chat
+ )
+ try _db.write { db in
+ try chat.save(db)
+ }
+ return Martin.ChatBase(context: context, jid: with)
+ }
+ } catch {
+ logIt(.error, "Error fetching chat: \(error.localizedDescription)")
+ return nil
+ }
+ }
+
+ func close(chat: any Martin.ChatProtocol) -> Bool {
+ // not used in Martin library for now
+ print("Closing chat: \(chat)")
+ return false
+ }
+}
diff --git a/ConversationsClassic/AppCore/Database/Database+Migrations.swift b/ConversationsClassic/AppCore/Database/Database+Migrations.swift
new file mode 100644
index 0000000..522dc64
--- /dev/null
+++ b/ConversationsClassic/AppCore/Database/Database+Migrations.swift
@@ -0,0 +1,65 @@
+import Foundation
+import GRDB
+
+extension Database {
+ static var migrator: DatabaseMigrator = {
+ var migrator = DatabaseMigrator()
+
+ // flush db on schema change (only in DEV mode)
+ #if DEBUG
+ migrator.eraseDatabaseOnSchemaChange = true
+ #endif
+
+ // 1st migration - basic tables
+ migrator.registerMigration("Add basic tables") { db in
+ // accounts
+ try db.create(table: "accounts", options: [.ifNotExists]) { table in
+ table.column("bareJid", .text).notNull().primaryKey().unique(onConflict: .replace)
+ table.column("pass", .text).notNull()
+ table.column("isActive", .boolean).notNull().defaults(to: true)
+ table.column("isTemp", .boolean).notNull().defaults(to: false)
+ }
+
+ // rosters
+ try db.create(table: "rosterVersions", options: [.ifNotExists]) { table in
+ table.column("bareJid", .text).notNull().primaryKey().unique(onConflict: .replace)
+ table.column("version", .text).notNull()
+ }
+ try db.create(table: "rosters", options: [.ifNotExists]) { table in
+ table.column("bareJid", .text).notNull()
+ table.column("contactBareJid", .text).notNull()
+ table.column("name", .text)
+ table.column("subscription", .text).notNull()
+ table.column("ask", .boolean).notNull().defaults(to: false)
+ table.column("data", .text).notNull()
+ table.primaryKey(["bareJid", "contactBareJid"], onConflict: .replace)
+ table.column("locallyDeleted", .boolean).notNull().defaults(to: false)
+ }
+
+ // chats
+ try db.create(table: "chats", options: [.ifNotExists]) { table in
+ table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace)
+ table.column("account", .text).notNull()
+ table.column("participant", .text).notNull()
+ table.column("type", .integer).notNull()
+ }
+
+ // messages
+ try db.create(table: "messages", options: [.ifNotExists]) { table in
+ table.column("id", .text).notNull().primaryKey().unique(onConflict: .replace)
+ table.column("chatId", .text).notNull().references("chats", onDelete: .cascade)
+ table.column("fromJid", .text).notNull()
+ table.column("toJid", .text).notNull()
+ table.column("timestamp", .datetime).notNull()
+ table.column("body", .text)
+ // table.column("isReaded", .boolean).notNull().defaults(to: false)
+ // table.column("subject", .text)
+ // table.column("threadId", .text)
+ // table.column("errorType", .text)
+ }
+ }
+
+ // return migrator
+ return migrator
+ }()
+}
diff --git a/ConversationsClassic/AppCore/Database/Database.swift b/ConversationsClassic/AppCore/Database/Database.swift
new file mode 100644
index 0000000..d334bb0
--- /dev/null
+++ b/ConversationsClassic/AppCore/Database/Database.swift
@@ -0,0 +1,55 @@
+import Combine
+import Foundation
+import GRDB
+import SwiftUI
+
+// MARK: - Models protocol
+typealias DBStorable = Codable & FetchableRecord & Identifiable & PersistableRecord & TableRecord
+
+// MARK: - Database init
+final class Database {
+ static let shared = Database()
+ let _db: DatabaseQueue
+
+ private init() {
+ do {
+ // Create db folder if not exists
+ let fileManager = FileManager.default
+ let appSupportURL = try fileManager.url(
+ for: .applicationSupportDirectory, in: .userDomainMask,
+ appropriateFor: nil, create: true
+ )
+ let directoryURL = appSupportURL.appendingPathComponent("ConversationsClassic", isDirectory: true)
+ try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
+
+ // Open or create the database
+ let databaseURL = directoryURL.appendingPathComponent("db.sqlite")
+ _db = try DatabaseQueue(path: databaseURL.path, configuration: Database.config)
+
+ // Some debug info
+ #if DEBUG
+ print("Database path: \(databaseURL.path)")
+ #endif
+
+ // Apply migrations
+ try Database.migrator.migrate(_db)
+ } catch {
+ fatalError("Database initialization failed: \(error)")
+ }
+ }
+}
+
+// MARK: - Config
+private extension Database {
+ static let config: Configuration = {
+ var config = Configuration()
+ #if DEBUG
+ // verbose and debugging in DEBUG builds only.
+ config.publicStatementArguments = true
+ config.prepareDatabase { db in
+ db.trace { print("SQL> \($0)") }
+ }
+ #endif
+ return config
+ }()
+}
diff --git a/ConversationsClassic/AppCore/Middlewares/AccountsMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/AccountsMiddleware.swift
new file mode 100644
index 0000000..b341cf3
--- /dev/null
+++ b/ConversationsClassic/AppCore/Middlewares/AccountsMiddleware.swift
@@ -0,0 +1,44 @@
+import Combine
+
+final class AccountsMiddleware {
+ static let shared = AccountsMiddleware()
+
+ func middleware(state: AppState, action: AppAction) -> AnyPublisher {
+ switch action {
+ case .databaseAction(.storedAccountsLoaded(let accounts)):
+ return Just(.accountsAction(.accountsListUpdated(accounts: accounts)))
+ .eraseToAnyPublisher()
+
+ case .xmppAction(.clientConnectionChanged(let jid, let connectionStatus)):
+ return Future { promise in
+ guard let account = state.accountsState.accounts.first(where: { $0.bareJid == jid }) else {
+ promise(.success(.empty))
+ return
+ }
+ if account.isTemp {
+ switch connectionStatus {
+ case .connected:
+ promise(.success(.accountsAction(.makeAccountPermanent(account: account))))
+
+ case .disconnected(let reason):
+ if reason != "No error!" {
+ promise(.success(.accountsAction(.addAccountError(jid: jid, reason: reason))))
+ } else {
+ promise(.success(.empty))
+ }
+
+ default:
+ promise(.success(.empty))
+ }
+
+ } else {
+ promise(.success(.empty))
+ }
+ }
+ .eraseToAnyPublisher()
+
+ default:
+ return Empty().eraseToAnyPublisher()
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Middlewares/ChatsMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/ChatsMiddleware.swift
new file mode 100644
index 0000000..ade60c2
--- /dev/null
+++ b/ConversationsClassic/AppCore/Middlewares/ChatsMiddleware.swift
@@ -0,0 +1,16 @@
+import Combine
+
+final class ChatsMiddleware {
+ static let shared = ChatsMiddleware()
+
+ func middleware(state _: AppState, action: AppAction) -> AnyPublisher {
+ switch action {
+ case .databaseAction(.storedChatsLoaded(let chats)):
+ return Just(.chatsAction(.chatsListUpdated(chats: chats)))
+ .eraseToAnyPublisher()
+
+ default:
+ return Empty().eraseToAnyPublisher()
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift
new file mode 100644
index 0000000..abef39f
--- /dev/null
+++ b/ConversationsClassic/AppCore/Middlewares/DatabaseMiddleware.swift
@@ -0,0 +1,132 @@
+import Combine
+import Foundation
+import GRDB
+
+final class DatabaseMiddleware {
+ static let shared = DatabaseMiddleware()
+ private let database = Database.shared
+ private var cancellables: Set = []
+
+ private init() {
+ // Database changes
+ ValueObservation
+ .tracking(Roster.fetchAll)
+ .publisher(in: database._db, scheduling: .immediate)
+ .sink { _ in
+ // Handle completion
+ } receiveValue: { rosters in
+ DispatchQueue.main.async {
+ store.dispatch(.databaseAction(.storedRostersLoaded(rosters: rosters)))
+ }
+ }
+ .store(in: &cancellables)
+ ValueObservation
+ .tracking(Chat.fetchAll)
+ .publisher(in: database._db, scheduling: .immediate)
+ .sink { _ in
+ // Handle completion
+ } receiveValue: { chats in
+ DispatchQueue.main.async {
+ store.dispatch(.databaseAction(.storedChatsLoaded(chats: chats)))
+ }
+ }
+ .store(in: &cancellables)
+ }
+
+ func middleware(state _: AppState, action: AppAction) -> AnyPublisher {
+ switch action {
+ case .startAction(.loadStoredAccounts):
+ return Future { promise in
+ Task(priority: .background) { [weak self] in
+ guard let database = self?.database else {
+ promise(.success(.databaseAction(.loadingStoredAccountsFailed)))
+ return
+ }
+ do {
+ try database._db.read { db in
+ let accounts = try Account.fetchAll(db)
+ promise(.success(.databaseAction(.storedAccountsLoaded(accounts: accounts))))
+ }
+ } catch {
+ promise(.success(.databaseAction(.loadingStoredAccountsFailed)))
+ }
+ }
+ }
+ .eraseToAnyPublisher()
+
+ case .accountsAction(.makeAccountPermanent(let account)):
+ return Future { promise in
+ Task(priority: .background) { [weak self] in
+ guard let database = self?.database else {
+ promise(.success(.databaseAction(.updateAccountFailed)))
+ return
+ }
+ do {
+ try database._db.write { db in
+ // make permanent and store to database
+ var acc = account
+ acc.isTemp = false
+ try acc.insert(db)
+
+ // Re-Fetch all accounts
+ let accounts = try Account.fetchAll(db)
+
+ // Use the accounts
+ promise(.success(.databaseAction(.storedAccountsLoaded(accounts: accounts))))
+ }
+ } catch {
+ promise(.success(.databaseAction(.updateAccountFailed)))
+ }
+ }
+ }
+ .eraseToAnyPublisher()
+
+ case .rostersAction(.markRosterAsLocallyDeleted(let ownerJID, let contactJID)):
+ return Future { promise in
+ Task(priority: .background) { [weak self] in
+ guard let database = self?.database else {
+ promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError))))
+ return
+ }
+ do {
+ _ = try database._db.write { db in
+ try Roster
+ .filter(Column("bareJid") == ownerJID)
+ .filter(Column("contactBareJid") == contactJID)
+ .updateAll(db, Column("locallyDeleted").set(to: true))
+ }
+ promise(.success(.empty))
+ } catch {
+ promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError))))
+ }
+ }
+ }
+ .eraseToAnyPublisher()
+
+ case .rostersAction(.unmarkRosterAsLocallyDeleted(let ownerJID, let contactJID)):
+ return Future { promise in
+ Task(priority: .background) { [weak self] in
+ guard let database = self?.database else {
+ promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError))))
+ return
+ }
+ do {
+ _ = try database._db.write { db in
+ try Roster
+ .filter(Column("bareJid") == ownerJID)
+ .filter(Column("contactBareJid") == contactJID)
+ .updateAll(db, Column("locallyDeleted").set(to: false))
+ }
+ promise(.success(.empty))
+ } catch {
+ promise(.success(.rostersAction(.rosterDeletingFailed(reason: L10n.Global.Error.genericDbError))))
+ }
+ }
+ }
+ .eraseToAnyPublisher()
+
+ default:
+ return Empty().eraseToAnyPublisher()
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Middlewares/LoggerMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/LoggerMiddleware.swift
new file mode 100644
index 0000000..272f319
--- /dev/null
+++ b/ConversationsClassic/AppCore/Middlewares/LoggerMiddleware.swift
@@ -0,0 +1,55 @@
+import Combine
+import Foundation
+
+let isConsoleLoggingEnabled = false
+
+#if DEBUG
+ let prefixLength = 2000
+ func loggerMiddleware() -> Middleware {
+ { state, action in
+ let timeStr = dateFormatter.string(from: Date())
+ var actionStr = "\(action)"
+ actionStr = String(actionStr.prefix(prefixLength)) + " ..."
+ var stateStr = "\(state)"
+ stateStr = String(stateStr.prefix(prefixLength)) + " ..."
+
+ let str = "\(timeStr) ➡️ \(actionStr)\n\(timeStr) ✅ \(stateStr)\n"
+ print(str)
+ if isConsoleLoggingEnabled {
+ NSLog(str)
+ }
+ return Empty().eraseToAnyPublisher()
+ }
+ }
+#else
+ func loggerMiddleware() -> Middleware {
+ { _, _ in
+ Empty().eraseToAnyPublisher()
+ }
+ }
+#endif
+
+enum LogLevels: String {
+ case info = "ℹ️"
+ case warning = "⚠️"
+ case error = "❌"
+}
+
+// For database errors logging
+func logIt(_ level: LogLevels, _ message: String) {
+ #if DEBUG
+ let timeStr = dateFormatter.string(from: Date())
+ let str = "\(timeStr) \(level.rawValue) \(message)"
+ print(str)
+ if isConsoleLoggingEnabled {
+ NSLog(str)
+ }
+ #endif
+}
+
+private var dateFormatter: DateFormatter {
+ let formatter = DateFormatter()
+ formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX") as Locale
+ formatter.dateFormat = "MM-dd HH:mm:ss.SSS"
+ return formatter
+}
diff --git a/ConversationsClassic/AppCore/Middlewares/RostersMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/RostersMiddleware.swift
new file mode 100644
index 0000000..e253589
--- /dev/null
+++ b/ConversationsClassic/AppCore/Middlewares/RostersMiddleware.swift
@@ -0,0 +1,16 @@
+import Combine
+
+final class RostersMiddleware {
+ static let shared = RostersMiddleware()
+
+ func middleware(state _: AppState, action: AppAction) -> AnyPublisher {
+ switch action {
+ case .databaseAction(.storedRostersLoaded(let rosters)):
+ return Just(.rostersAction(.rostersListUpdated(rosters)))
+ .eraseToAnyPublisher()
+
+ default:
+ return Empty().eraseToAnyPublisher()
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Middlewares/StartMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/StartMiddleware.swift
new file mode 100644
index 0000000..1a084b6
--- /dev/null
+++ b/ConversationsClassic/AppCore/Middlewares/StartMiddleware.swift
@@ -0,0 +1,32 @@
+import Combine
+
+final class StartMiddleware {
+ static let shared = StartMiddleware()
+
+ func middleware(state: AppState, action: AppAction) -> AnyPublisher {
+ switch action {
+ case .accountsAction(.accountsListUpdated(let accounts)):
+ if accounts.isEmpty {
+ if state.currentFlow == .start {
+ return Just(.startAction(.goTo(.welcomeScreen)))
+ .eraseToAnyPublisher()
+ } else {
+ return Empty().eraseToAnyPublisher()
+ }
+ } else {
+ if state.currentFlow == .accounts, state.accountsState.navigation == .addAccount {
+ return Just(.changeFlow(.chats))
+ .eraseToAnyPublisher()
+ } else if state.currentFlow == .start {
+ return Just(.changeFlow(.chats))
+ .eraseToAnyPublisher()
+ } else {
+ return Empty().eraseToAnyPublisher()
+ }
+ }
+
+ default:
+ return Empty().eraseToAnyPublisher()
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift
new file mode 100644
index 0000000..0b48440
--- /dev/null
+++ b/ConversationsClassic/AppCore/Middlewares/XMPPMiddleware.swift
@@ -0,0 +1,85 @@
+import Combine
+import Foundation
+import Martin
+
+final class XMPPMiddleware {
+ static let shared = XMPPMiddleware()
+ private let service = XMPPService(manager: Database.shared)
+ private var cancellables: Set = []
+
+ private init() {
+ service.clientState.sink { client, state in
+ let jid = client.userBareJid.stringValue
+ let status = ConnectionStatus.from(state)
+ let action = AppAction.xmppAction(.clientConnectionChanged(jid: jid, state: status))
+ DispatchQueue.main.async {
+ store.dispatch(action)
+ }
+ }
+ .store(in: &cancellables)
+ }
+
+ func middleware(state: AppState, action: AppAction) -> AnyPublisher {
+ switch action {
+ case .accountsAction(.tryAddAccountWithCredentials):
+ return Future { [weak self] promise in
+ self?.service.updateClients(for: state.accountsState.accounts)
+ promise(.success(.empty))
+ }
+ .eraseToAnyPublisher()
+
+ case .accountsAction(.addAccountError):
+ return Future { [weak self] promise in
+ self?.service.updateClients(for: state.accountsState.accounts)
+ promise(.success(.empty))
+ }
+ .eraseToAnyPublisher()
+
+ case .databaseAction(.storedAccountsLoaded(let accounts)):
+ return Future { [weak self] promise in
+ self?.service.updateClients(for: accounts.filter { $0.isActive && !$0.isTemp })
+ promise(.success(.empty))
+ }
+ .eraseToAnyPublisher()
+
+ case .rostersAction(.addRoster(let ownerJID, let contactJID, let name, let groups)):
+ return Future { [weak self] promise in
+ guard let service = self?.service, let client = service.clients.first(where: { $0.connectionConfiguration.userJid.stringValue == ownerJID }) else {
+ return promise(.success(.rostersAction(.addRosterError(reason: XMPPError.item_not_found.localizedDescription))))
+ }
+ let module = client.modulesManager.module(RosterModule.self)
+ module.addItem(jid: JID(contactJID), name: name, groups: groups, completionHandler: { result in
+ switch result {
+ case .success:
+ promise(.success(.rostersAction(.addRosterDone(jid: contactJID))))
+
+ case .failure(let error):
+ promise(.success(.rostersAction(.addRosterError(reason: error.localizedDescription))))
+ }
+ })
+ }
+ .eraseToAnyPublisher()
+
+ case .rostersAction(.deleteRoster(let ownerJID, let contactJID)):
+ return Future { [weak self] promise in
+ guard let service = self?.service, let client = service.clients.first(where: { $0.connectionConfiguration.userJid.stringValue == ownerJID }) else {
+ return promise(.success(.rostersAction(.rosterDeletingFailed(reason: XMPPError.item_not_found.localizedDescription))))
+ }
+ let module = client.modulesManager.module(RosterModule.self)
+ module.removeItem(jid: JID(contactJID), completionHandler: { result in
+ switch result {
+ case .success:
+ promise(.success(.empty))
+
+ case .failure(let error):
+ promise(.success(.rostersAction(.rosterDeletingFailed(reason: error.localizedDescription))))
+ }
+ })
+ }
+ .eraseToAnyPublisher()
+
+ default:
+ return Empty().eraseToAnyPublisher()
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Models/Account.swift b/ConversationsClassic/AppCore/Models/Account.swift
new file mode 100644
index 0000000..264d195
--- /dev/null
+++ b/ConversationsClassic/AppCore/Models/Account.swift
@@ -0,0 +1,22 @@
+import Foundation
+import GRDB
+import Martin
+import SwiftUI
+
+// MARK: - Account
+struct Account: DBStorable {
+ var bareJid: String
+ var pass: String
+ var isActive: Bool
+ var isTemp: Bool // account which is added by user, but not yet logged in
+ var id: String { bareJid }
+}
+
+extension Account: UniversalInputSelectionElement {
+ var text: String? { bareJid }
+ var icon: Image? { nil }
+}
+
+extension Account {
+ static let databaseTableName = "accounts"
+}
diff --git a/ConversationsClassic/AppCore/Models/Chat.swift b/ConversationsClassic/AppCore/Models/Chat.swift
new file mode 100644
index 0000000..58fc273
--- /dev/null
+++ b/ConversationsClassic/AppCore/Models/Chat.swift
@@ -0,0 +1,19 @@
+import Foundation
+import GRDB
+
+enum ConversationType: Int, Codable, DatabaseValueConvertible {
+ case chat = 0
+ case room = 1
+ case channel = 2
+}
+
+struct Chat: DBStorable {
+ static let databaseTableName = "chats"
+
+ var id: String
+ var account: String
+ var participant: String
+ var type: ConversationType
+}
+
+extension Chat: Equatable {}
diff --git a/ConversationsClassic/AppCore/Models/ConnectionStatus.swift b/ConversationsClassic/AppCore/Models/ConnectionStatus.swift
new file mode 100644
index 0000000..fab36aa
--- /dev/null
+++ b/ConversationsClassic/AppCore/Models/ConnectionStatus.swift
@@ -0,0 +1,27 @@
+// This struct is simpliest variant of Martin's Client State.
+// Just for more comfortable using in App State
+import Foundation
+import Martin
+
+enum ConnectionStatus: Stateable {
+ case connecting
+ case connected(resumed: Bool = false)
+ case disconnecting
+ case disconnected(reason: String)
+
+ static func from(_ state: XMPPClient.State) -> ConnectionStatus {
+ switch state {
+ case .connecting:
+ return .connecting
+
+ case .connected(let resumed):
+ return .connected(resumed: resumed)
+
+ case .disconnecting:
+ return .disconnecting
+
+ case .disconnected(let reason):
+ return .disconnected(reason: reason.localizedDescription)
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Models/Message.swift b/ConversationsClassic/AppCore/Models/Message.swift
new file mode 100644
index 0000000..cd06541
--- /dev/null
+++ b/ConversationsClassic/AppCore/Models/Message.swift
@@ -0,0 +1,30 @@
+import Foundation
+import GRDB
+
+enum MessageType: String, Codable, DatabaseValueConvertible {
+ case text
+ case image
+ case video
+ case audio
+ case file
+ case location
+}
+
+struct Message: DBStorable, Equatable {
+ static let databaseTableName = "messages"
+
+ let id: String
+ let chatId: String
+ let fromJid: String
+ let toJid: String
+ let timestamp: Date
+ let body: String?
+ // var isReaded: Bool
+ // let subject: String?
+ // let threadId: String?
+ // let errorType: String?
+
+ var type: MessageType {
+ .text
+ }
+}
diff --git a/ConversationsClassic/AppCore/Models/Roster.swift b/ConversationsClassic/AppCore/Models/Roster.swift
new file mode 100644
index 0000000..b4653cb
--- /dev/null
+++ b/ConversationsClassic/AppCore/Models/Roster.swift
@@ -0,0 +1,62 @@
+import Foundation
+import GRDB
+import Martin
+
+struct RosterVersion: DBStorable {
+ static let databaseTableName = "rosterVersions"
+
+ var bareJid: String
+ var version: String
+ var id: String { bareJid }
+}
+
+struct Roster: DBStorable {
+ static let databaseTableName = "rosters"
+
+ var bareJid: String = ""
+ var contactBareJid: String
+ var name: String?
+ var subscription: String
+ var ask: Bool
+ var data: DBRosterData
+ var locallyDeleted: Bool = false
+
+ var id: String { "\(bareJid)-\(contactBareJid)" }
+}
+
+struct DBRosterData: Codable, DatabaseValueConvertible {
+ let groups: [String]
+ let annotations: [RosterItemAnnotation]
+
+ public var databaseValue: DatabaseValue {
+ let encoder = JSONEncoder()
+ // swiftlint:disable:next force_try
+ let data = try! encoder.encode(self)
+ return data.databaseValue
+ }
+
+ public static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self? {
+ guard let data = Data.fromDatabaseValue(dbValue) else {
+ return nil
+ }
+ let decoder = JSONDecoder()
+ // swiftlint:disable:next force_try
+ return try! decoder.decode(Self.self, from: data)
+ }
+
+ static func == (lhs: DBRosterData, rhs: DBRosterData) -> Bool {
+ lhs.groups == rhs.groups && lhs.annotations == rhs.annotations
+ }
+}
+
+extension RosterItemAnnotation: Equatable {
+ public static func == (lhs: RosterItemAnnotation, rhs: RosterItemAnnotation) -> Bool {
+ lhs.type == rhs.type && lhs.values == rhs.values
+ }
+}
+
+extension Roster: Equatable {
+ static func == (lhs: Roster, rhs: Roster) -> Bool {
+ lhs.bareJid == rhs.bareJid && lhs.contactBareJid == rhs.contactBareJid
+ }
+}
diff --git a/ConversationsClassic/AppCore/Reducers/AccountsReducer.swift b/ConversationsClassic/AppCore/Reducers/AccountsReducer.swift
new file mode 100644
index 0000000..ba01721
--- /dev/null
+++ b/ConversationsClassic/AppCore/Reducers/AccountsReducer.swift
@@ -0,0 +1,22 @@
+extension AccountsState {
+ static func reducer(state: inout AccountsState, action: AccountsAction) {
+ switch action {
+ case .accountsListUpdated(let accounts):
+ state.accounts = accounts
+
+ case .goTo(let navigation):
+ state.navigation = navigation
+
+ case .tryAddAccountWithCredentials(let login, let password):
+ let account = Account(bareJid: login, pass: password, isActive: true, isTemp: true)
+ state.accounts.append(account)
+
+ case .addAccountError(let jid, let reason):
+ state.accounts = state.accounts.filter { $0.bareJid != jid }
+ state.addAccountError = reason
+
+ default:
+ break
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Reducers/AppReducer.swift b/ConversationsClassic/AppCore/Reducers/AppReducer.swift
new file mode 100644
index 0000000..ac17fe1
--- /dev/null
+++ b/ConversationsClassic/AppCore/Reducers/AppReducer.swift
@@ -0,0 +1,29 @@
+import Foundation
+
+extension AppState {
+ static func reducer(state: inout AppState, action: AppAction) {
+ switch action {
+ case .flushState:
+ state = AppState()
+
+ case .changeFlow(let flow):
+ state.previousFlow = state.currentFlow
+ state.currentFlow = flow
+
+ case .startAction(let action):
+ StartState.reducer(state: &state.startState, action: action)
+
+ case .databaseAction, .xmppAction, .empty:
+ break // database and xmpp actions are processed by other middlewares
+
+ case .accountsAction(let action):
+ AccountsState.reducer(state: &state.accountsState, action: action)
+
+ case .rostersAction(let action):
+ RostersState.reducer(state: &state.rostersState, action: action)
+
+ case .chatsAction(let action):
+ ChatsState.reducer(state: &state.chatsState, action: action)
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Reducers/ChatsReducer.swift b/ConversationsClassic/AppCore/Reducers/ChatsReducer.swift
new file mode 100644
index 0000000..9fadaab
--- /dev/null
+++ b/ConversationsClassic/AppCore/Reducers/ChatsReducer.swift
@@ -0,0 +1,11 @@
+extension ChatsState {
+ static func reducer(state: inout ChatsState, action: ChatsAction) {
+ switch action {
+ case .chatsListUpdated(let chats):
+ state.chats = chats
+
+ default:
+ break
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Reducers/RostersReducer.swift b/ConversationsClassic/AppCore/Reducers/RostersReducer.swift
new file mode 100644
index 0000000..076559e
--- /dev/null
+++ b/ConversationsClassic/AppCore/Reducers/RostersReducer.swift
@@ -0,0 +1,25 @@
+extension RostersState {
+ static func reducer(state: inout RostersState, action: RostersAction) {
+ switch action {
+ case .addRosterDone(let jid):
+ state.newAddedRosterJid = jid
+ state.newAddedRosterError = nil
+
+ case .addRosterError(let reason):
+ state.newAddedRosterJid = nil
+ state.newAddedRosterError = reason
+
+ case .rostersListUpdated(let rosters):
+ state.rosters = rosters
+
+ case .markRosterAsLocallyDeleted, .deleteRoster:
+ state.deleteRosterError = nil
+
+ case .rosterDeletingFailed(let reson):
+ state.deleteRosterError = reson
+
+ default:
+ break
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/Reducers/StartReducer.swift b/ConversationsClassic/AppCore/Reducers/StartReducer.swift
new file mode 100644
index 0000000..76539f6
--- /dev/null
+++ b/ConversationsClassic/AppCore/Reducers/StartReducer.swift
@@ -0,0 +1,11 @@
+extension StartState {
+ static func reducer(state: inout StartState, action: StartAction) {
+ switch action {
+ case .loadStoredAccounts:
+ break
+
+ case .goTo(let navigation):
+ state.navigation = navigation
+ }
+ }
+}
diff --git a/ConversationsClassic/AppCore/State/AccountsState.swift b/ConversationsClassic/AppCore/State/AccountsState.swift
new file mode 100644
index 0000000..c8ba0e9
--- /dev/null
+++ b/ConversationsClassic/AppCore/State/AccountsState.swift
@@ -0,0 +1,18 @@
+enum AccountNavigationState: Stateable {
+ case addAccount
+}
+
+struct AccountsState: Stateable {
+ var navigation: AccountNavigationState
+ var accounts: [Account]
+
+ var addAccountError: String?
+}
+
+// MARK: Init
+extension AccountsState {
+ init() {
+ navigation = .addAccount
+ accounts = []
+ }
+}
diff --git a/ConversationsClassic/AppCore/State/AppState.swift b/ConversationsClassic/AppCore/State/AppState.swift
new file mode 100644
index 0000000..1dbb251
--- /dev/null
+++ b/ConversationsClassic/AppCore/State/AppState.swift
@@ -0,0 +1,34 @@
+import Foundation
+
+enum AppFlow: Codable {
+ case start
+ case accounts
+ case chats
+ case contacts
+ case settings
+}
+
+struct AppState: Stateable {
+ var appVersion: String
+ var previousFlow: AppFlow
+ var currentFlow: AppFlow
+
+ var startState: StartState
+ var accountsState: AccountsState
+ var rostersState: RostersState
+ var chatsState: ChatsState
+}
+
+// MARK: Init
+extension AppState {
+ init() {
+ appVersion = Const.appVersion
+ previousFlow = .start
+ currentFlow = .start
+
+ startState = StartState()
+ accountsState = AccountsState()
+ rostersState = RostersState()
+ chatsState = ChatsState()
+ }
+}
diff --git a/ConversationsClassic/AppCore/State/ChatsState.swift b/ConversationsClassic/AppCore/State/ChatsState.swift
new file mode 100644
index 0000000..d83fd39
--- /dev/null
+++ b/ConversationsClassic/AppCore/State/ChatsState.swift
@@ -0,0 +1,10 @@
+struct ChatsState: Stateable {
+ var chats: [Chat]
+}
+
+// MARK: Init
+extension ChatsState {
+ init() {
+ chats = []
+ }
+}
diff --git a/ConversationsClassic/AppCore/State/RostersState.swift b/ConversationsClassic/AppCore/State/RostersState.swift
new file mode 100644
index 0000000..bc42a70
--- /dev/null
+++ b/ConversationsClassic/AppCore/State/RostersState.swift
@@ -0,0 +1,15 @@
+struct RostersState: Stateable {
+ var rosters: [Roster]
+
+ var newAddedRosterJid: String?
+ var newAddedRosterError: String?
+
+ var deleteRosterError: String?
+}
+
+// MARK: Init
+extension RostersState {
+ init() {
+ rosters = []
+ }
+}
diff --git a/ConversationsClassic/AppCore/State/StartState.swift b/ConversationsClassic/AppCore/State/StartState.swift
new file mode 100644
index 0000000..15bb709
--- /dev/null
+++ b/ConversationsClassic/AppCore/State/StartState.swift
@@ -0,0 +1,15 @@
+enum StartNavigationState: Stateable {
+ case startScreen
+ case welcomeScreen
+}
+
+struct StartState: Stateable {
+ var navigation: StartNavigationState
+}
+
+// MARK: Init
+extension StartState {
+ init() {
+ navigation = .startScreen
+ }
+}
diff --git a/ConversationsClassic/AppCore/XMPP/XMPPService.swift b/ConversationsClassic/AppCore/XMPP/XMPPService.swift
new file mode 100644
index 0000000..c6b6ddd
--- /dev/null
+++ b/ConversationsClassic/AppCore/XMPP/XMPPService.swift
@@ -0,0 +1,90 @@
+import Combine
+import Foundation
+import GRDB
+import Martin
+
+protocol MartinsManager: Martin.RosterManager & Martin.ChatManager {}
+
+final class XMPPService: ObservableObject {
+ private let manager: MartinsManager
+ private let clientStatePublisher = PassthroughSubject<(XMPPClient, XMPPClient.State), Never>()
+ private var clientStateCancellables: [AnyCancellable] = []
+
+ @Published private(set) var clients: [XMPPClient] = []
+ var clientState: AnyPublisher<(XMPPClient, XMPPClient.State), Never> {
+ clientStatePublisher.eraseToAnyPublisher()
+ }
+
+ init(manager: MartinsManager) {
+ self.manager = manager
+ }
+
+ func updateClients(for accounts: [Account]) {
+ // get simple diff
+ let forAdd = accounts
+ .filter { !self.clients.map { $0.connectionConfiguration.userJid.stringValue }.contains($0.bareJid) }
+ let forRemove = clients
+ .map { $0.connectionConfiguration.userJid.stringValue }
+ .filter { !accounts.map { $0.bareJid }.contains($0) }
+
+ // init and add clients
+ for account in forAdd {
+ let client = makeClient(for: account, with: manager)
+ clients.append(client)
+ let cancellable = client.$state
+ .sink { [weak self] state in
+ self?.clientStatePublisher.send((client, state))
+ }
+
+ clientStateCancellables.append(cancellable)
+ client.login()
+ }
+
+ // remove clients
+ for jid in forRemove {
+ deinitClient(jid: jid)
+ }
+ }
+
+ private func makeClient(for account: Account, with manager: MartinsManager) -> XMPPClient {
+ let client = XMPPClient()
+
+ // register modules
+ // core modules RFC 6120
+ client.modulesManager.register(StreamFeaturesModule())
+ client.modulesManager.register(SaslModule())
+ client.modulesManager.register(AuthModule())
+ client.modulesManager.register(SessionEstablishmentModule())
+ client.modulesManager.register(ResourceBinderModule())
+ client.modulesManager.register(DiscoveryModule(identity: .init(category: "client", type: "iOS", name: Const.appName)))
+
+ // messaging modules RFC 6121
+ client.modulesManager.register(RosterModule(rosterManager: manager))
+ client.modulesManager.register(PresenceModule())
+
+ client.modulesManager.register(PubSubModule())
+ client.modulesManager.register(MessageModule(chatManager: manager))
+ client.modulesManager.register(MessageCarbonsModule())
+ client.modulesManager.register(MessageArchiveManagementModule())
+
+ // extensions
+ client.modulesManager.register(SoftwareVersionModule())
+ client.modulesManager.register(PingModule())
+ client.connectionConfiguration.userJid = .init(account.bareJid)
+ client.connectionConfiguration.credentials = .password(password: account.pass)
+
+ // add client to clients
+ return client
+ }
+
+ func deinitClient(jid: String) {
+ if let index = clients.firstIndex(where: { $0.connectionConfiguration.userJid.stringValue == jid }) {
+ let client = clients.remove(at: index)
+ _ = client.disconnect()
+ }
+ }
+
+ func getClient(for jid: String) -> XMPPClient? {
+ clients.first { $0.connectionConfiguration.userJid.stringValue == jid }
+ }
+}
diff --git a/ConversationsClassic/ConversationsClassicApp.swift b/ConversationsClassic/ConversationsClassicApp.swift
new file mode 100644
index 0000000..76c2663
--- /dev/null
+++ b/ConversationsClassic/ConversationsClassicApp.swift
@@ -0,0 +1,27 @@
+import Combine
+import SwiftUI
+
+let appState = AppState()
+let store = AppStore(
+ initialState: appState,
+ reducer: AppState.reducer,
+ middlewares: [
+ loggerMiddleware(),
+ StartMiddleware.shared.middleware,
+ DatabaseMiddleware.shared.middleware,
+ AccountsMiddleware.shared.middleware,
+ XMPPMiddleware.shared.middleware,
+ RostersMiddleware.shared.middleware,
+ ChatsMiddleware.shared.middleware
+ ]
+)
+
+@main
+struct ConversationsClassic: App {
+ var body: some Scene {
+ WindowGroup {
+ BaseNavigationView()
+ .environmentObject(store)
+ }
+ }
+}
diff --git a/ConversationsClassic/Generated/.gitignore b/ConversationsClassic/Generated/.gitignore
new file mode 100644
index 0000000..c96a04f
--- /dev/null
+++ b/ConversationsClassic/Generated/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
\ No newline at end of file
diff --git a/ConversationsClassic/Helpers/Bool+Extensions.swift b/ConversationsClassic/Helpers/Bool+Extensions.swift
new file mode 100644
index 0000000..b26674c
--- /dev/null
+++ b/ConversationsClassic/Helpers/Bool+Extensions.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+extension Bool {
+ var intValue: Int {
+ self ? 1 : 0
+ }
+}
diff --git a/ConversationsClassic/Helpers/Const.swift b/ConversationsClassic/Helpers/Const.swift
new file mode 100644
index 0000000..1e6e4e7
--- /dev/null
+++ b/ConversationsClassic/Helpers/Const.swift
@@ -0,0 +1,30 @@
+import Foundation
+
+enum Const {
+ // Network
+ #if DEBUG
+ static let baseUrl = "staging.some.com/api"
+ #else
+ static let baseUrl = "prod.some.com/api"
+ #endif
+ static let requestTimeout = 15.0
+ static let networkLogging = true
+
+ // App
+ static var appVersion: String {
+ let info = Bundle.main.infoDictionary
+ let appVersion = info?["CFBundleShortVersionString"] as? String ?? "Unknown"
+ let appBuild = info?[kCFBundleVersionKey as String] as? String ?? "Unknown"
+ return "v \(appVersion)(\(appBuild))"
+ }
+
+ static var appName: String {
+ Bundle.main.bundleIdentifier ?? "Conversations Classic iOS"
+ }
+
+ // Trusted servers
+ enum TrustedServers: String {
+ case narayana = "narayana.im"
+ case conversations = "conversations.im"
+ }
+}
diff --git a/ConversationsClassic/Helpers/String+Extensions.swift b/ConversationsClassic/Helpers/String+Extensions.swift
new file mode 100644
index 0000000..8fc4cfb
--- /dev/null
+++ b/ConversationsClassic/Helpers/String+Extensions.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+extension String {
+ var firstLetter: String {
+ String(prefix(1)).uppercased()
+ }
+}
diff --git a/ConversationsClassic/Helpers/UserDefaultsWrapper.swift b/ConversationsClassic/Helpers/UserDefaultsWrapper.swift
new file mode 100644
index 0000000..535e9e9
--- /dev/null
+++ b/ConversationsClassic/Helpers/UserDefaultsWrapper.swift
@@ -0,0 +1,32 @@
+import Foundation
+
+// Wrapper
+@propertyWrapper
+struct Storage {
+ private let key: String
+ private let defaultValue: T
+
+ init(key: String, defaultValue: T) {
+ self.key = key
+ self.defaultValue = defaultValue
+ }
+
+ var wrappedValue: T {
+ get {
+ // Read value from UserDefaults
+ UserDefaults.standard.object(forKey: key) as? T ?? defaultValue
+ }
+ set {
+ // Set value to UserDefaults
+ UserDefaults.standard.set(newValue, forKey: key)
+ }
+ }
+}
+
+// Storage
+private let keyLocalizationSelected = "conversations.classic.user.defaults.localizationSelected"
+
+enum UserSettings {
+ @Storage(key: keyLocalizationSelected, defaultValue: false)
+ static var localizationSelectedByUser: Bool
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/main/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/Contents.json
new file mode 100644
index 0000000..6e96565
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/Contents.json
@@ -0,0 +1,9 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "provides-namespace" : true
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/main/backgroundDark.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/backgroundDark.colorset/Contents.json
new file mode 100644
index 0000000..997edf5
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/backgroundDark.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "228",
+ "green" : "228",
+ "red" : "228"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/main/backgroundLight.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/backgroundLight.colorset/Contents.json
new file mode 100644
index 0000000..b8c6d9e
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/backgroundLight.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "235",
+ "green" : "235",
+ "red" : "235"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/main/black.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/black.colorset/Contents.json
new file mode 100644
index 0000000..4f827dd
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/black.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.180",
+ "green" : "0.180",
+ "red" : "0.180"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/main/gray.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/gray.colorset/Contents.json
new file mode 100644
index 0000000..e0fb4d4
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/gray.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.471",
+ "green" : "0.471",
+ "red" : "0.471"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/main/separator.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/separator.colorset/Contents.json
new file mode 100644
index 0000000..3d66dc2
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/separator.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "189",
+ "green" : "189",
+ "red" : "189"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/main/white.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/white.colorset/Contents.json
new file mode 100644
index 0000000..97650a1
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/main/white.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "1.000",
+ "green" : "1.000",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/Contents.json
new file mode 100644
index 0000000..6e96565
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/Contents.json
@@ -0,0 +1,9 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "provides-namespace" : true
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue200.colorset/Contents.json
new file mode 100644
index 0000000..12b3b32
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.976",
+ "green" : "0.792",
+ "red" : "0.565"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue300.colorset/Contents.json
new file mode 100644
index 0000000..0b975d5
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.965",
+ "green" : "0.710",
+ "red" : "0.392"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue500.colorset/Contents.json
new file mode 100644
index 0000000..3c05e0a
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.953",
+ "green" : "0.588",
+ "red" : "0.129"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue800.colorset/Contents.json
new file mode 100644
index 0000000..865adb5
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blue800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.824",
+ "green" : "0.463",
+ "red" : "0.098"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark200.colorset/Contents.json
new file mode 100644
index 0000000..e29b366
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.855",
+ "green" : "0.659",
+ "red" : "0.624"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark300.colorset/Contents.json
new file mode 100644
index 0000000..096e5f1
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.796",
+ "green" : "0.525",
+ "red" : "0.475"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark500.colorset/Contents.json
new file mode 100644
index 0000000..22f8cb4
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.710",
+ "green" : "0.318",
+ "red" : "0.247"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark800.colorset/Contents.json
new file mode 100644
index 0000000..bf615f8
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueDark800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.624",
+ "green" : "0.247",
+ "red" : "0.188"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight200.colorset/Contents.json
new file mode 100644
index 0000000..1ded918
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.980",
+ "green" : "0.831",
+ "red" : "0.506"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight300.colorset/Contents.json
new file mode 100644
index 0000000..f17585c
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.969",
+ "green" : "0.765",
+ "red" : "0.310"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight500.colorset/Contents.json
new file mode 100644
index 0000000..9453b24
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.957",
+ "green" : "0.663",
+ "red" : "0.012"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight800.colorset/Contents.json
new file mode 100644
index 0000000..2b85944
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/blueLight800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.820",
+ "green" : "0.533",
+ "red" : "0.008"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown200.colorset/Contents.json
new file mode 100644
index 0000000..ce0b7bb
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.643",
+ "green" : "0.667",
+ "red" : "0.737"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown300.colorset/Contents.json
new file mode 100644
index 0000000..957f0da
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.498",
+ "green" : "0.533",
+ "red" : "0.631"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown500.colorset/Contents.json
new file mode 100644
index 0000000..2926f1b
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.282",
+ "green" : "0.333",
+ "red" : "0.475"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown800.colorset/Contents.json
new file mode 100644
index 0000000..70e2f7c
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/brown800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.216",
+ "green" : "0.251",
+ "red" : "0.365"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark200.colorset/Contents.json
new file mode 100644
index 0000000..e25608b
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.655",
+ "green" : "0.839",
+ "red" : "0.647"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark300.colorset/Contents.json
new file mode 100644
index 0000000..4c19725
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.518",
+ "green" : "0.780",
+ "red" : "0.506"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark500.colorset/Contents.json
new file mode 100644
index 0000000..ddfea69
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.314",
+ "green" : "0.686",
+ "red" : "0.298"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark800.colorset/Contents.json
new file mode 100644
index 0000000..9dbb7a6
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenDark800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.235",
+ "green" : "0.557",
+ "red" : "0.220"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight200.colorset/Contents.json
new file mode 100644
index 0000000..54f6a97
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.647",
+ "green" : "0.882",
+ "red" : "0.773"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight300.colorset/Contents.json
new file mode 100644
index 0000000..2c4fe10
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.506",
+ "green" : "0.835",
+ "red" : "0.682"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight500.colorset/Contents.json
new file mode 100644
index 0000000..89db920
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.290",
+ "green" : "0.765",
+ "red" : "0.545"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight800.colorset/Contents.json
new file mode 100644
index 0000000..1acd954
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/greenLight800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.220",
+ "green" : "0.624",
+ "red" : "0.408"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark200.colorset/Contents.json
new file mode 100644
index 0000000..ef9328c
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.859",
+ "green" : "0.616",
+ "red" : "0.702"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark300.colorset/Contents.json
new file mode 100644
index 0000000..3556135
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.804",
+ "green" : "0.459",
+ "red" : "0.584"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark500.colorset/Contents.json
new file mode 100644
index 0000000..00e5075
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.718",
+ "green" : "0.227",
+ "red" : "0.404"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark800.colorset/Contents.json
new file mode 100644
index 0000000..748a957
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaDark800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.659",
+ "green" : "0.176",
+ "red" : "0.318"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight200.colorset/Contents.json
new file mode 100644
index 0000000..725c54c
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.847",
+ "green" : "0.576",
+ "red" : "0.808"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight300.colorset/Contents.json
new file mode 100644
index 0000000..d9fbdb8
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.784",
+ "green" : "0.408",
+ "red" : "0.729"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight500.colorset/Contents.json
new file mode 100644
index 0000000..99500b7
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.690",
+ "green" : "0.153",
+ "red" : "0.612"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight800.colorset/Contents.json
new file mode 100644
index 0000000..2921caf
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/magentaLight800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.635",
+ "green" : "0.122",
+ "red" : "0.482"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark200.colorset/Contents.json
new file mode 100644
index 0000000..fbe5e38
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.569",
+ "green" : "0.671",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark300.colorset/Contents.json
new file mode 100644
index 0000000..58e2f9f
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.396",
+ "green" : "0.541",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark500.colorset/Contents.json
new file mode 100644
index 0000000..e219ac8
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.133",
+ "green" : "0.341",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark800.colorset/Contents.json
new file mode 100644
index 0000000..a63dd3e
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeDark800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.098",
+ "green" : "0.290",
+ "red" : "0.902"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight200.colorset/Contents.json
new file mode 100644
index 0000000..6e70cd5
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.502",
+ "green" : "0.800",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight300.colorset/Contents.json
new file mode 100644
index 0000000..799505c
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.302",
+ "green" : "0.718",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight500.colorset/Contents.json
new file mode 100644
index 0000000..4f0878b
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.000",
+ "green" : "0.596",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight800.colorset/Contents.json
new file mode 100644
index 0000000..dfc0149
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/orangeLight800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.000",
+ "green" : "0.486",
+ "red" : "0.961"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink200.colorset/Contents.json
new file mode 100644
index 0000000..0becef6
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.694",
+ "green" : "0.561",
+ "red" : "0.957"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink300.colorset/Contents.json
new file mode 100644
index 0000000..9e9e4b7
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.573",
+ "green" : "0.384",
+ "red" : "0.941"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink500.colorset/Contents.json
new file mode 100644
index 0000000..ddc3e1d
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.388",
+ "green" : "0.118",
+ "red" : "0.914"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink800.colorset/Contents.json
new file mode 100644
index 0000000..463aa83
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/pink800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.357",
+ "green" : "0.094",
+ "red" : "0.761"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red200.colorset/Contents.json
new file mode 100644
index 0000000..518a736
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.604",
+ "green" : "0.604",
+ "red" : "0.937"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red300.colorset/Contents.json
new file mode 100644
index 0000000..94100b6
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.451",
+ "green" : "0.451",
+ "red" : "0.898"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red500.colorset/Contents.json
new file mode 100644
index 0000000..7afafeb
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.212",
+ "green" : "0.263",
+ "red" : "0.957"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red800.colorset/Contents.json
new file mode 100644
index 0000000..8972c91
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/red800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.184",
+ "green" : "0.184",
+ "red" : "0.827"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark200.colorset/Contents.json
new file mode 100644
index 0000000..89f27af
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.769",
+ "green" : "0.796",
+ "red" : "0.502"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark300.colorset/Contents.json
new file mode 100644
index 0000000..39d4eca
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.675",
+ "green" : "0.714",
+ "red" : "0.302"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark500.colorset/Contents.json
new file mode 100644
index 0000000..13174d8
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.533",
+ "green" : "0.588",
+ "red" : "0.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark800.colorset/Contents.json
new file mode 100644
index 0000000..24f01a5
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseDark800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.420",
+ "green" : "0.475",
+ "red" : "0.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight200.colorset/Contents.json
new file mode 100644
index 0000000..0f87be6
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.918",
+ "green" : "0.871",
+ "red" : "0.502"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight300.colorset/Contents.json
new file mode 100644
index 0000000..5d7af98
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.882",
+ "green" : "0.816",
+ "red" : "0.302"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight500.colorset/Contents.json
new file mode 100644
index 0000000..20a4a22
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.831",
+ "green" : "0.737",
+ "red" : "0.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight800.colorset/Contents.json
new file mode 100644
index 0000000..68b5f8d
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/tortoiseLight800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.655",
+ "green" : "0.592",
+ "red" : "0.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark200.colorset/Contents.json
new file mode 100644
index 0000000..98fa97f
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.510",
+ "green" : "0.878",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark300.colorset/Contents.json
new file mode 100644
index 0000000..6140117
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.310",
+ "green" : "0.835",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark500.colorset/Contents.json
new file mode 100644
index 0000000..6ef924c
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.027",
+ "green" : "0.757",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark800.colorset/Contents.json
new file mode 100644
index 0000000..93e32b7
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowDark800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.000",
+ "green" : "0.627",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight200.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight200.colorset/Contents.json
new file mode 100644
index 0000000..f1b174f
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight200.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.612",
+ "green" : "0.933",
+ "red" : "0.902"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight300.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight300.colorset/Contents.json
new file mode 100644
index 0000000..74aa8a8
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight300.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.459",
+ "green" : "0.906",
+ "red" : "0.863"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight500.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight500.colorset/Contents.json
new file mode 100644
index 0000000..ac7f58f
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight500.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.224",
+ "green" : "0.863",
+ "red" : "0.804"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight800.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight800.colorset/Contents.json
new file mode 100644
index 0000000..8aa0219
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/material/yellowLight800.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.169",
+ "green" : "0.706",
+ "red" : "0.686"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/status/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/Contents.json
new file mode 100644
index 0000000..6e96565
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/Contents.json
@@ -0,0 +1,9 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "provides-namespace" : true
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/status/away.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/away.colorset/Contents.json
new file mode 100644
index 0000000..084e378
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/away.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.157",
+ "green" : "0.792",
+ "red" : "1.000"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/status/chat.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/chat.colorset/Contents.json
new file mode 100644
index 0000000..426fe2c
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/chat.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.416",
+ "green" : "0.733",
+ "red" : "0.400"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/status/dnd.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/dnd.colorset/Contents.json
new file mode 100644
index 0000000..4157b4e
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/dnd.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.314",
+ "green" : "0.325",
+ "red" : "0.937"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/status/online.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/online.colorset/Contents.json
new file mode 100644
index 0000000..bc69efa
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/online.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.396",
+ "green" : "0.800",
+ "red" : "0.612"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/status/xa.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/xa.colorset/Contents.json
new file mode 100644
index 0000000..4157b4e
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/status/xa.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.314",
+ "green" : "0.325",
+ "red" : "0.937"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/Contents.json
new file mode 100644
index 0000000..6e96565
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/Contents.json
@@ -0,0 +1,9 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ },
+ "properties" : {
+ "provides-namespace" : true
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/blueLight.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/blueLight.colorset/Contents.json
new file mode 100644
index 0000000..0f56fa7
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/blueLight.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.812",
+ "green" : "0.624",
+ "red" : "0.447"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/blueMedium.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/blueMedium.colorset/Contents.json
new file mode 100644
index 0000000..afefbe4
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/blueMedium.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.643",
+ "green" : "0.396",
+ "red" : "0.204"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/brownLight.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/brownLight.colorset/Contents.json
new file mode 100644
index 0000000..8ff9def
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/brownLight.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.243",
+ "green" : "0.686",
+ "red" : "0.988"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/brownMedium.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/brownMedium.colorset/Contents.json
new file mode 100644
index 0000000..65c0923
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/brownMedium.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.067",
+ "green" : "0.490",
+ "red" : "0.757"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/greenLight.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/greenLight.colorset/Contents.json
new file mode 100644
index 0000000..fbbc61f
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/greenLight.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.204",
+ "green" : "0.886",
+ "red" : "0.541"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/greenMedium.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/greenMedium.colorset/Contents.json
new file mode 100644
index 0000000..a0f042d
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/greenMedium.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.086",
+ "green" : "0.824",
+ "red" : "0.451"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/magentaLight.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/magentaLight.colorset/Contents.json
new file mode 100644
index 0000000..c546070
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/magentaLight.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.659",
+ "green" : "0.498",
+ "red" : "0.678"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/magentaMedium.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/magentaMedium.colorset/Contents.json
new file mode 100644
index 0000000..d103d20
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/magentaMedium.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.482",
+ "green" : "0.314",
+ "red" : "0.459"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/orangeLight.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/orangeLight.colorset/Contents.json
new file mode 100644
index 0000000..9401878
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/orangeLight.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.431",
+ "green" : "0.537",
+ "red" : "0.914"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/orangeMedium.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/orangeMedium.colorset/Contents.json
new file mode 100644
index 0000000..08122cf
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/orangeMedium.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.000",
+ "green" : "0.475",
+ "red" : "0.961"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/redLight.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/redLight.colorset/Contents.json
new file mode 100644
index 0000000..d960ecf
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/redLight.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.161",
+ "green" : "0.161",
+ "red" : "0.937"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/redMedium.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/redMedium.colorset/Contents.json
new file mode 100644
index 0000000..fa9d4e7
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/redMedium.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.000",
+ "green" : "0.000",
+ "red" : "0.800"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/yellowLight.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/yellowLight.colorset/Contents.json
new file mode 100644
index 0000000..40f2378
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/yellowLight.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.310",
+ "green" : "0.914",
+ "red" : "0.988"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/yellowMedium.colorset/Contents.json b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/yellowMedium.colorset/Contents.json
new file mode 100644
index 0000000..e5bc5bb
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Colors.xcassets/tango/yellowMedium.colorset/Contents.json
@@ -0,0 +1,20 @@
+{
+ "colors" : [
+ {
+ "color" : {
+ "color-space" : "srgb",
+ "components" : {
+ "alpha" : "1.000",
+ "blue" : "0.000",
+ "green" : "0.831",
+ "red" : "0.929"
+ }
+ },
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Images.xcassets/AppIcon.appiconset/Contents.json b/ConversationsClassic/Resources/Assets/Images.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..f434400
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Images.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,14 @@
+{
+ "images" : [
+ {
+ "filename" : "logo.png",
+ "idiom" : "universal",
+ "platform" : "ios",
+ "size" : "1024x1024"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Images.xcassets/AppIcon.appiconset/logo.png b/ConversationsClassic/Resources/Assets/Images.xcassets/AppIcon.appiconset/logo.png
new file mode 100644
index 0000000..8c8b6de
Binary files /dev/null and b/ConversationsClassic/Resources/Assets/Images.xcassets/AppIcon.appiconset/logo.png differ
diff --git a/ConversationsClassic/Resources/Assets/Images.xcassets/Contents.json b/ConversationsClassic/Resources/Assets/Images.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Images.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Images.xcassets/logo.imageset/Contents.json b/ConversationsClassic/Resources/Assets/Images.xcassets/logo.imageset/Contents.json
new file mode 100644
index 0000000..86cf964
--- /dev/null
+++ b/ConversationsClassic/Resources/Assets/Images.xcassets/logo.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "ConvLogo.png",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Assets/Images.xcassets/logo.imageset/ConvLogo.png b/ConversationsClassic/Resources/Assets/Images.xcassets/logo.imageset/ConvLogo.png
new file mode 100644
index 0000000..e137e0f
Binary files /dev/null and b/ConversationsClassic/Resources/Assets/Images.xcassets/logo.imageset/ConvLogo.png differ
diff --git a/ConversationsClassic/Resources/Preview Content/Preview Assets.xcassets/Contents.json b/ConversationsClassic/Resources/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 0000000..73c0059
--- /dev/null
+++ b/ConversationsClassic/Resources/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/ConversationsClassic/Resources/Strings/Localizable.strings b/ConversationsClassic/Resources/Strings/Localizable.strings
new file mode 100644
index 0000000..8dadfa1
--- /dev/null
+++ b/ConversationsClassic/Resources/Strings/Localizable.strings
@@ -0,0 +1,59 @@
+// MARK: General
+"Global.name" = "Conversartions Classic";
+"Global.ok" = "Ok";
+"Global.back" = "Back";
+"Global.cancel" = "Cancel";
+"Global.save" = "Save";
+"Global.Error.title" = "Error";
+"Global.Error.genericText" = "Something went wrong";
+"Global.Error.genericDbError" = "Database error";
+
+// MARK: Onboar screen
+"Start.subtitle" = "Free and secure messaging and calls between any existed messengers";
+"Start.Btn.login" = "Enter with JID";
+"Start.Btn.register" = "New Account";
+"Login.title" = "Let\'s go!";
+"Login.subtitle" = "Enter your JID, it should looks like email address";
+"Login.Hint.jid" = "user@domain.im";
+"Login.Hint.password" = "password";
+"Login.btn" = "Continue";
+"Login.Error.wrongPassword" = "Wrong password or JID";
+"Login.Error.noServer" = "Server not exists";
+"Login.Error.serverError" = "Server error. Check internet connection";
+
+// MARK: Contacts screen
+"Contacts.title" = "Contacts";
+"Contacts.sendMessage" = "Send message";
+"Contacts.editContact" = "Edit contact";
+"Contacts.selectContact" = "Select contact";
+"Contacts.deleteContact" = "Delete contact";
+"Contacts.Add.title" = "Add Contact";
+"Contacts.Add.explanation" = "Contact or group/channel name are usually JID in format name@domain.ltd (like email)";
+"Contacts.Add.error" = "Contact not added. Server returns error.";
+"Contacts.Delete.title" = "Delete contact";
+"Contacts.Delete.message" = "You can delete contact from this device (contact will be available on other devices), or delete it completely";
+"Contacts.Delete.deleteFromDevice" = "Delete from device";
+"Contacts.Delete.deleteCompletely" = "Delete completely";
+"Contacts.Delete.error" = "Contact not deleted. Server returns error.";
+
+
+// MARK: Chats screen
+"Chats.title" = "Chats";
+
+"Chat.title" = "Chat";
+
+// MARK: Accounts add screen
+"Accounts.Add.or" = "or";
+"Accounts.Add.Exist.title" = "Add existing\naccount";
+"Accounts.Add.Exist.Prompt.jid" = "Enter your XMPP ID";
+"Accounts.Add.Exist.Prompt.password" = "Enter password";
+"Accounts.Add.Exist.Hint.jid" = "user@domain.im";
+"Accounts.Add.Exist.Hint.password" = "password";
+"Accounts.Add.Exist.Btn.link" = "create a new one";
+"Accounts.Add.Exist.Btn.main" = "Continue";
+"Accounts.Add.Exist.loginError" = "Wrong login or password";
+
+// MARK: Server connecting indicator
+"ServerConnectingIndicator.State.connecting" = "Connecting to server";
+"ServerConnectingIndicator.State.connected" = "Connected";
+"ServerConnectingIndicator.State.error" = "Server unreachable. Check internet connection and server name";
diff --git a/ConversationsClassic/Resources/launchscreen.storyboard b/ConversationsClassic/Resources/launchscreen.storyboard
new file mode 100644
index 0000000..ab103e8
--- /dev/null
+++ b/ConversationsClassic/Resources/launchscreen.storyboard
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ConversationsClassic/View/BaseNavigationView.swift b/ConversationsClassic/View/BaseNavigationView.swift
new file mode 100644
index 0000000..3848577
--- /dev/null
+++ b/ConversationsClassic/View/BaseNavigationView.swift
@@ -0,0 +1,36 @@
+import Martin
+import SwiftUI
+
+struct BaseNavigationView: View {
+ @EnvironmentObject var store: AppStore
+
+ public var body: some View {
+ Group {
+ switch store.state.currentFlow {
+ case .start:
+ switch store.state.startState.navigation {
+ case .startScreen:
+ StartScreen()
+
+ case .welcomeScreen:
+ WelcomeScreen()
+ }
+
+ case .accounts:
+ switch store.state.accountsState.navigation {
+ case .addAccount:
+ AddAccountScreen()
+ }
+
+ case .chats:
+ ChatsListScreen()
+
+ case .contacts:
+ ContactsScreen()
+
+ case .settings:
+ SettingsScreen()
+ }
+ }
+ }
+}
diff --git a/ConversationsClassic/View/Components/SharedListRow.swift b/ConversationsClassic/View/Components/SharedListRow.swift
new file mode 100644
index 0000000..bd7fab4
--- /dev/null
+++ b/ConversationsClassic/View/Components/SharedListRow.swift
@@ -0,0 +1,17 @@
+import SwiftUI
+
+extension View {
+ func sharedListRow() -> some View {
+ modifier(SharedListRow())
+ }
+}
+
+struct SharedListRow: ViewModifier {
+ public func body(content: Content) -> some View {
+ content
+ .listRowInsets(.zero)
+ .listRowSeparator(.hidden)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(Color.Main.backgroundLight)
+ }
+}
diff --git a/ConversationsClassic/View/Components/SharedTabBar.swift b/ConversationsClassic/View/Components/SharedTabBar.swift
new file mode 100644
index 0000000..d0c5209
--- /dev/null
+++ b/ConversationsClassic/View/Components/SharedTabBar.swift
@@ -0,0 +1,76 @@
+import SwiftUI
+
+struct SharedTabBar: View {
+ var body: some View {
+ VStack(spacing: 0) {
+ Rectangle()
+ .frame(maxWidth: .infinity)
+ .frame(height: 0.2)
+ .foregroundColor(.Main.separator)
+ HStack(spacing: 0) {
+ SharedTabBarButton(buttonFlow: .contacts)
+ SharedTabBarButton(buttonFlow: .chats)
+ SharedTabBarButton(buttonFlow: .settings)
+ }
+ .background(Color.Main.backgroundDark)
+ }
+ .frame(height: 50)
+ }
+}
+
+private struct SharedTabBarButton: View {
+ @EnvironmentObject var store: AppStore
+
+ let buttonFlow: AppFlow
+
+ var body: some View {
+ ZStack {
+ VStack(spacing: 2) {
+ buttonImg
+ .foregroundColor(buttonFlow == store.state.currentFlow ? .Material.greenDark500 : .Main.gray)
+ .font(.system(size: 24, weight: .light))
+ .symbolRenderingMode(.hierarchical)
+ Text(buttonTitle)
+ .font(.sub1)
+ .foregroundColor(buttonFlow == store.state.currentFlow ? .Main.black : .Main.gray)
+ }
+ Rectangle()
+ .foregroundColor(.white.opacity(0.01))
+ .onTapGesture {
+ store.dispatch(.changeFlow(buttonFlow))
+ }
+ }
+ }
+
+ var buttonImg: Image {
+ switch buttonFlow {
+ case .contacts:
+ return Image(systemName: "person.2.fill")
+
+ case .chats:
+ return Image(systemName: "bubble.left.fill")
+
+ case .settings:
+ return Image(systemName: "gearshape.fill")
+
+ default:
+ return Image(systemName: "questionmark.circle")
+ }
+ }
+
+ var buttonTitle: String {
+ switch buttonFlow {
+ case .contacts:
+ return "Contacts"
+
+ case .chats:
+ return "Chats"
+
+ case .settings:
+ return "Settings"
+
+ default:
+ return "Unknown"
+ }
+ }
+}
diff --git a/ConversationsClassic/View/Components/UniversalInputCollection.swift b/ConversationsClassic/View/Components/UniversalInputCollection.swift
new file mode 100644
index 0000000..3a6418f
--- /dev/null
+++ b/ConversationsClassic/View/Components/UniversalInputCollection.swift
@@ -0,0 +1,203 @@
+import SwiftUI
+
+// MARK: Public
+protocol UniversalInputSelectionElement: Identifiable, Equatable, Hashable {
+ var icon: Image? { get }
+ var text: String? { get }
+}
+
+public enum UniversalInputCollection {
+ struct TextField {
+ let prompt: String
+ @Binding var text: String
+ var focus: FocusState.Binding
+ var fieldType: T
+ let contentType: UITextContentType
+ let keyboardType: UIKeyboardType
+ let submitLabel: SubmitLabel
+ let action: () -> Void
+ }
+
+ struct SecureField {
+ let prompt: String
+ @Binding var text: String
+ var focus: FocusState.Binding
+ var fieldType: T
+ let submitLabel: SubmitLabel
+ let action: () -> Void
+ }
+
+ struct DropDownMenu {
+ let prompt: String
+ let elements: [E]
+ @Binding var selected: E?
+ var focus: FocusState.Binding
+ var fieldType: T
+ }
+}
+
+// MARK: Inputs implementations
+extension UniversalInputCollection.TextField: View {
+ var body: some View {
+ TextField("", text: $text)
+ .padding(.horizontal, 8)
+ .focused(focus, equals: fieldType)
+ .font(.body2)
+ .foregroundColor(.Main.black)
+ .autocorrectionDisabled(true)
+ .autocapitalization(.none)
+ .textContentType(contentType)
+ .keyboardType(keyboardType)
+ .submitLabel(submitLabel)
+ .textSelection(.enabled)
+ .onSubmit {
+ action()
+ }
+ .modifier(UniversalInputModifier(
+ prompt: prompt,
+ focus: focus,
+ fieldType: fieldType,
+ isActive: isFilled
+ ))
+ }
+
+ var isFilled: Bool {
+ !text.isEmpty || focus.wrappedValue == fieldType
+ }
+}
+
+extension UniversalInputCollection.SecureField: View {
+ var body: some View {
+ SecureField("", text: $text)
+ .padding(.horizontal, 8)
+ .focused(focus, equals: fieldType)
+ .font(.body2)
+ .foregroundColor(.Main.black)
+ .autocorrectionDisabled(true)
+ .autocapitalization(.none)
+ .textContentType(.password)
+ .submitLabel(submitLabel)
+ .textSelection(.disabled)
+ .onSubmit {
+ action()
+ }
+ .modifier(UniversalInputModifier(
+ prompt: prompt,
+ focus: focus,
+ fieldType: fieldType,
+ isActive: isFilled
+ ))
+ }
+
+ var isFilled: Bool {
+ !text.isEmpty || focus.wrappedValue == fieldType
+ }
+}
+
+extension UniversalInputCollection.DropDownMenu: View {
+ var body: some View {
+ ZStack {
+ HStack {
+ Text(text)
+ .font(.body2)
+ .foregroundColor(.Main.black)
+ .padding(.leading, 8)
+ Spacer()
+ }
+ .modifier(UniversalInputModifier(
+ prompt: prompt,
+ focus: focus,
+ fieldType: fieldType,
+ isActive: selected != nil
+ ))
+
+ Menu {
+ ForEach(elements, id: \.self.id) { element in
+ Button {
+ selected = element
+ } label: {
+ Text(element.text ?? "")
+ }
+ }
+ } label: {
+ Label("", image: "")
+ .labelStyle(TitleOnlyLabelStyle())
+ .padding(.vertical)
+ .frame(height: 48)
+ .frame(maxWidth: .infinity)
+ }
+ }
+ }
+
+ var text: String {
+ if let text = selected?.text {
+ return text
+ } else {
+ return ""
+ }
+ }
+}
+
+// MARK: Modifiers
+private struct UniversalInputModifier: ViewModifier {
+ let prompt: String
+ var focus: FocusState.Binding
+ var fieldType: T
+ let isActive: Bool
+ var promptBackground: Color?
+ var isCentered: Bool?
+ var customTapAction: (() -> Void)?
+
+ func body(content: Content) -> some View {
+ VStack(spacing: 0) {
+ ZStack {
+ HStack {
+ Text(isActive ? "" : prompt)
+ .font(.body2)
+ .foregroundColor(.Main.gray)
+ .padding(8)
+ Spacer()
+ }
+ content
+ .frame(height: 48)
+ }
+ }
+ .frame(height: 48)
+ .background {
+ ZStack {
+ RoundedRectangle(cornerRadius: 4)
+ .foregroundColor(.Main.white)
+ RoundedRectangle(cornerRadius: 4)
+ .stroke(Color.Main.gray)
+ }
+ }
+ .contentShape(Rectangle())
+ .onTapGesture {
+ if let customTapAction {
+ customTapAction()
+ } else {
+ if focus.wrappedValue != fieldType {
+ focus.wrappedValue = fieldType
+ }
+ }
+ }
+ }
+}
+
+// MARK: Validators
+extension UniversalInputCollection {
+ enum Validators {
+ static func isEmail(_ input: String) -> Bool {
+ if !input.isEmpty {
+ let mailRegex = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}"
+ if !NSPredicate(format: "SELF MATCHES %@", mailRegex).evaluate(with: input) {
+ return false
+ } else {
+ return true
+ }
+ } else {
+ return true
+ }
+ }
+ }
+}
diff --git a/ConversationsClassic/View/Screens/AddAccountScreen.swift b/ConversationsClassic/View/Screens/AddAccountScreen.swift
new file mode 100644
index 0000000..9b67736
--- /dev/null
+++ b/ConversationsClassic/View/Screens/AddAccountScreen.swift
@@ -0,0 +1,120 @@
+import Combine
+import Martin
+import SwiftUI
+
+struct AddAccountScreen: View {
+ @EnvironmentObject var store: AppStore
+
+ enum Field {
+ case userJid
+ case password
+ }
+
+ @FocusState private var focus: Field?
+ @State private var errorMsg: String = ""
+ @State private var isShowingAlert = false
+ @State private var isShowingLoader = false
+
+ #if DEBUG
+ @State private var jidStr: String = "test1@test.anal.company"
+ @State private var pass: String = "12345"
+ #else
+ @State private var jidStr: String = ""
+ @State private var pass: String = ""
+ #endif
+
+ public var body: some View {
+ ZStack {
+ // background
+ Color.Main.backgroundLight
+ .ignoresSafeArea()
+
+ // content
+ VStack(spacing: 32) {
+ // icon
+ Image.logo
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 120, height: 120)
+
+ // texts
+ VStack(spacing: 10) {
+ Text(L10n.Login.title)
+ .font(.head1l)
+ .foregroundColor(.Material.tortoiseDark500)
+ .fixedSize(horizontal: true, vertical: false)
+ Text(L10n.Login.subtitle)
+ .font(.body2)
+ .foregroundColor(.Material.tortoiseDark300)
+ .multilineTextAlignment(.center)
+ .fixedSize(horizontal: false, vertical: true)
+ }
+
+ VStack(spacing: 16) {
+ UniversalInputCollection.TextField(
+ prompt: L10n.Login.Hint.jid,
+ text: $jidStr,
+ focus: $focus,
+ fieldType: .userJid,
+ contentType: .emailAddress,
+ keyboardType: .emailAddress,
+ submitLabel: .next,
+ action: {
+ focus = .password
+ }
+ )
+ UniversalInputCollection.SecureField(
+ prompt: L10n.Login.Hint.password,
+ text: $pass,
+ focus: $focus,
+ fieldType: .password,
+ submitLabel: .go,
+ action: {
+ focus = nil
+ }
+ )
+
+ Button {
+ isShowingLoader = true
+ store.dispatch(.accountsAction(.tryAddAccountWithCredentials(login: jidStr, password: pass)))
+ } label: {
+ Text(L10n.Login.btn)
+ }
+ .buttonStyle(PrimaryButtonStyle())
+ .disabled(!loginInputValid)
+
+ Button {
+ store.dispatch(.startAction(.goTo(.welcomeScreen)))
+ store.dispatch(.changeFlow(.start))
+ } label: {
+ Text("\(Image(systemName: "chevron.left")) \(L10n.Global.back)")
+ .foregroundColor(.Material.tortoiseDark300)
+ .font(.body2)
+ }
+ }
+ }
+ .padding(.horizontal, 32)
+ }
+ .loadingIndicator(isShowingLoader)
+ .alert(isPresented: $isShowingAlert) {
+ Alert(
+ title: Text(L10n.Global.Error.title),
+ message: Text(errorMsg),
+ dismissButton: .default(Text(L10n.Global.ok)) {
+ store.dispatch(.accountsAction(.addAccountError(jid: jidStr, reason: nil)))
+ }
+ )
+ }
+ .onChange(of: store.state.accountsState.addAccountError) { err in
+ if let err {
+ isShowingLoader = false
+ isShowingAlert = true
+ errorMsg = err
+ }
+ }
+ }
+
+ private var loginInputValid: Bool {
+ !jidStr.isEmpty && !pass.isEmpty && UniversalInputCollection.Validators.isEmail(jidStr)
+ }
+}
diff --git a/ConversationsClassic/View/Screens/AddContactOrChannelScreen.swift b/ConversationsClassic/View/Screens/AddContactOrChannelScreen.swift
new file mode 100644
index 0000000..73229c7
--- /dev/null
+++ b/ConversationsClassic/View/Screens/AddContactOrChannelScreen.swift
@@ -0,0 +1,167 @@
+import SwiftUI
+
+struct AddContactOrChannelScreen: View {
+ @EnvironmentObject var store: AppStore
+
+ enum Field {
+ case account
+ case contact
+ }
+
+ @FocusState private var focus: Field?
+
+ @Binding var isPresented: Bool
+ @State private var contactJID: String = ""
+ @State private var ownerAccount: Account?
+
+ @State private var isShowingLoader = false
+ @State private var isShowingAlert = false
+ @State private var errorMsg = ""
+
+ var body: some View {
+ ZStack {
+ // Background color
+ Color.Main.backgroundLight
+ .ignoresSafeArea()
+
+ // Content
+ VStack(spacing: 0) {
+ // Header
+ AddContactsScreenHeader(isPresented: $isPresented)
+
+ VStack(spacing: 16) {
+ // Explanation text
+
+ Text(L10n.Contacts.Add.explanation)
+ .font(.body3)
+ .foregroundColor(.Main.gray)
+ .multilineTextAlignment(.center)
+ .padding(.top, 16)
+
+ // Account selector
+ HStack(spacing: 0) {
+ Text("Use account:")
+ .font(.body2)
+ .foregroundColor(.Main.black)
+ .frame(alignment: .leading)
+ Spacer()
+ }
+ UniversalInputCollection.DropDownMenu(
+ prompt: "Use account",
+ elements: store.state.accountsState.accounts,
+ selected: $ownerAccount,
+ focus: $focus,
+ fieldType: .account
+ )
+
+ // Contact text input
+ HStack(spacing: 0) {
+ Text("Contact JID:")
+ .font(.body2)
+ .foregroundColor(.Main.black)
+ .frame(alignment: .leading)
+ Spacer()
+ }
+ UniversalInputCollection.TextField(
+ prompt: "Contact or channel JID",
+ text: $contactJID,
+ focus: $focus,
+ fieldType: .contact,
+ contentType: .emailAddress,
+ keyboardType: .emailAddress,
+ submitLabel: .done,
+ action: {
+ focus = .account
+ }
+ )
+
+ // Save button
+ Button {
+ save()
+ } label: {
+ Text(L10n.Global.save)
+ }
+ .buttonStyle(PrimaryButtonStyle())
+ .disabled(!inputValid)
+ .padding(.top)
+ Spacer()
+ }
+ .padding(.horizontal, 32)
+ }
+ }
+ .onAppear {
+ if let exists = store.state.accountsState.accounts.first, exists.isActive {
+ ownerAccount = exists
+ }
+ }
+ .loadingIndicator(isShowingLoader)
+ .alert(isPresented: $isShowingAlert) {
+ Alert(
+ title: Text(L10n.Global.Error.title),
+ message: Text(errorMsg),
+ dismissButton: .default(Text(L10n.Global.ok))
+ )
+ }
+ .onChange(of: store.state.rostersState.newAddedRosterJid) { jid in
+ if let _ = jid, isShowingLoader {
+ isShowingLoader = false
+ isPresented = false
+ }
+ }
+ .onChange(of: store.state.rostersState.newAddedRosterError) { error in
+ if let error = error, isShowingLoader {
+ isShowingLoader = false
+ errorMsg = error
+ isShowingAlert = true
+ }
+ }
+ }
+
+ private var inputValid: Bool {
+ ownerAccount != nil && !contactJID.isEmpty && UniversalInputCollection.Validators.isEmail(contactJID)
+ }
+
+ private func save() {
+ guard let ownerAccount else { return }
+ if let exists = store.state.rostersState.rosters.first(where: { $0.bareJid == ownerAccount.bareJid && $0.contactBareJid == contactJID }), exists.locallyDeleted {
+ store.dispatch(.rostersAction(.unmarkRosterAsLocallyDeleted(ownerJID: ownerAccount.bareJid, contactJID: contactJID)))
+ isPresented = false
+ } else {
+ isShowingLoader = true
+ store.dispatch(.rostersAction(.addRoster(ownerJID: ownerAccount.bareJid, contactJID: contactJID, name: nil, groups: [])))
+ }
+ }
+}
+
+private struct AddContactsScreenHeader: View {
+ @Binding var isPresented: Bool
+
+ var body: some View {
+ ZStack {
+ // bg
+ Color.Main.backgroundDark
+ .ignoresSafeArea()
+
+ // title
+ Text(L10n.Contacts.Add.title)
+ .font(.head2)
+ .foregroundColor(Color.Main.black)
+
+ HStack {
+ Image(systemName: "chevron.left")
+ .foregroundColor(Color.Material.greenDark500)
+ .tappablePadding(.symmetric(12)) {
+ isPresented = false
+ }
+ Spacer()
+ Image(systemName: "plus.viewfinder")
+ .foregroundColor(Color.Material.greenDark500)
+ .tappablePadding(.symmetric(12)) {
+ print("Scan QR-code")
+ }
+ }
+ .padding(.horizontal, 16)
+ }
+ .frame(height: 44)
+ }
+}
diff --git a/ConversationsClassic/View/Screens/ChatScreen.swift b/ConversationsClassic/View/Screens/ChatScreen.swift
new file mode 100644
index 0000000..e7c9a8f
--- /dev/null
+++ b/ConversationsClassic/View/Screens/ChatScreen.swift
@@ -0,0 +1,112 @@
+import Combine
+import Foundation
+import Martin
+import SwiftUI
+
+struct ChatScreen: View {
+ // @EnvironmentObject var state: AppState
+
+ var body: some View {
+ VStack(spacing: 0) {
+ // Header
+ ChatScreenHeader()
+
+ // Msg list
+ // if !state.messages.isEmpty {
+ // List {
+ // ForEach(state.messages) { message in
+ // ChatMessageView(message: message)
+ // }
+ // }
+ // } else {
+ // Text("No messages")
+ // Spacer()
+ // }
+ }
+ }
+}
+
+private struct ChatScreenHeader: View {
+ // @EnvironmentObject var state: AppState
+
+ var body: some View {
+ ZStack {
+ // bg
+ Color.Main.backgroundDark
+ .ignoresSafeArea()
+
+ // title
+ // let name = (
+ // state.activeConversation?.participant?.name ??
+ // state.activeConversation.participant?.contactBareJid
+ // ) ?? L10n.Chat.title
+ Text(L10n.Chat.title)
+ .font(.head2)
+ .foregroundColor(Color.Main.black)
+
+ HStack {
+ Image(systemName: "chevron.left")
+ .foregroundColor(Color.Tango.orangeMedium)
+ .tappablePadding(.symmetric(12)) {
+ // state.flow = .chatsList
+ }
+ Spacer()
+ // Image(systemName: "plus.viewfinder")
+ // .foregroundColor(Color.Tango.orangeMedium)
+ // .tappablePadding(.symmetric(12)) {
+ // print("Scan QR-code")
+ // }
+ }
+ .padding(.horizontal, 16)
+ }
+ .frame(height: 44)
+ }
+}
+
+private struct ChatMessageView: View {
+ // @EnvironmentObject var state: AppState
+
+ let message: Message
+
+ var body: some View {
+ HStack {
+ Text(message.body ?? "--NO BODY?--")
+ // .padding(.all, 8)
+ // .background(.black)
+ // .clipShape(RoundedRectangle(cornerRadius: 8))
+ .foregroundColor(Color.Main.black)
+ Spacer()
+ // if isIncoming() {
+ // Image(systemName: "person.fill")
+ // .foregroundColor(Color.Main.black)
+ // .frame(width: 32, height: 32)
+ // .background(Color.Main.backgroundLight)
+ // .clipShape(Circle())
+ // Text(message.body ?? "--NO BODY?--")
+ // .padding(.all, 8)
+ // .background(Color.Main.backgroundLight)
+ // .clipShape(RoundedRectangle(cornerRadius: 8))
+ // .foregroundColor(Color.Main.black)
+ // } else {
+ // Text(message.body ?? "--NO BODY?--")
+ // .padding(.all, 8)
+ // .background(Color.Main.backgroundLight)
+ // .clipShape(RoundedRectangle(cornerRadius: 8))
+ // .foregroundColor(Color.Main.black)
+ // Image(systemName: "person.fill")
+ // .foregroundColor(Color.Main.black)
+ // .frame(width: 32, height: 32)
+ // .background(Color.Main.backgroundLight)
+ // .clipShape(Circle())
+ // }
+ }
+ .padding(.horizontal, 16)
+ .background(Color.red)
+ }
+
+ // private func isIncoming() -> Bool {
+ // message.fromJid != state.currentChat?.account
+ // }
+}
+
+// for test
diff --git a/ConversationsClassic/View/Screens/ChatsListScreen.swift b/ConversationsClassic/View/Screens/ChatsListScreen.swift
new file mode 100644
index 0000000..722dc70
--- /dev/null
+++ b/ConversationsClassic/View/Screens/ChatsListScreen.swift
@@ -0,0 +1,96 @@
+import SwiftUI
+
+struct ChatsListScreen: View {
+ @EnvironmentObject var store: AppStore
+
+ var body: some View {
+ ZStack {
+ // Background color
+ Color.Main.backgroundLight
+ .ignoresSafeArea()
+
+ // Content
+ VStack(spacing: 0) {
+ // Header
+ ChatsScreenHeader()
+
+ // Chats list
+ if !store.state.chatsState.chats.isEmpty {
+ List {
+ ForEach(store.state.chatsState.chats) { chat in
+ ChatsRow(chat: chat)
+ }
+ }
+ .listStyle(.plain)
+ .background(Color.Main.backgroundLight)
+ } else {
+ Spacer()
+ }
+
+ // Tab bar
+ SharedTabBar()
+ }
+ }
+ }
+}
+
+private struct ChatsScreenHeader: View {
+ var body: some View {
+ ZStack {
+ // bg
+ Color.Main.backgroundDark
+ .ignoresSafeArea()
+
+ // title
+ Text(L10n.Chats.title)
+ .font(.head2)
+ .foregroundColor(Color.Main.black)
+
+ HStack {
+ Spacer()
+ Image(systemName: "plus")
+ .foregroundColor(.Material.greenDark500)
+ .tappablePadding(.symmetric(12)) {
+ print("Add contact")
+ }
+ }
+ .padding(.horizontal, 16)
+ }
+ .frame(height: 44)
+ }
+}
+
+private struct ChatsRow: View {
+ @EnvironmentObject var store: AppStore
+
+ var chat: Chat
+
+ var body: some View {
+ VStack(spacing: 0) {
+ HStack(spacing: 8) {
+ ZStack {
+ Circle()
+ .frame(width: 44, height: 44)
+ .foregroundColor(.red)
+ Text(chat.participant.firstLetter)
+ .foregroundColor(.white)
+ .font(.body1)
+ }
+ Text(chat.participant)
+ .foregroundColor(Color.Main.black)
+ .font(.body2)
+ Spacer()
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 4)
+ Rectangle()
+ .frame(maxWidth: .infinity)
+ .frame(height: 1)
+ .foregroundColor(.Main.backgroundDark)
+ }
+ .sharedListRow()
+ .onTapGesture {
+ // state.startChat(chat)
+ }
+ }
+}
diff --git a/ConversationsClassic/View/Screens/ContactsScreen.swift b/ConversationsClassic/View/Screens/ContactsScreen.swift
new file mode 100644
index 0000000..e60809a
--- /dev/null
+++ b/ConversationsClassic/View/Screens/ContactsScreen.swift
@@ -0,0 +1,188 @@
+import SwiftUI
+
+struct ContactsScreen: View {
+ @EnvironmentObject var store: AppStore
+
+ @State private var addPanelPresented = false
+ @State private var isErrorAlertPresented = false
+ @State private var errorAlertMessage = ""
+ @State private var isShowingLoader = false
+
+ var body: some View {
+ ZStack {
+ // Background color
+ Color.Main.backgroundLight
+ .ignoresSafeArea()
+
+ // Content
+ VStack(spacing: 0) {
+ // Header
+ ContactsScreenHeader(addPanelPresented: $addPanelPresented)
+
+ // Contacts list
+ let rosters = store.state.rostersState.rosters.filter { !$0.locallyDeleted }
+ if !rosters.isEmpty {
+ List {
+ ForEach(rosters) { roster in
+ ContactsScreenRow(
+ roster: roster,
+ isErrorAlertPresented: $isErrorAlertPresented,
+ errorAlertMessage: $errorAlertMessage,
+ isShowingLoader: $isShowingLoader
+ )
+ }
+ }
+ .listStyle(.plain)
+ .background(Color.Main.backgroundLight)
+ } else {
+ Spacer()
+ }
+
+ // Tab bar
+ SharedTabBar()
+ }
+ }
+ .loadingIndicator(isShowingLoader)
+ .fullScreenCover(isPresented: $addPanelPresented) {
+ AddContactOrChannelScreen(isPresented: $addPanelPresented)
+ }
+ .alert(isPresented: $isErrorAlertPresented) {
+ Alert(
+ title: Text(L10n.Global.Error.title),
+ message: Text(errorAlertMessage),
+ dismissButton: .default(Text(L10n.Global.ok))
+ )
+ }
+ }
+}
+
+private struct ContactsScreenHeader: View {
+ @Binding var addPanelPresented: Bool
+
+ var body: some View {
+ ZStack {
+ // bg
+ Color.Main.backgroundDark
+ .ignoresSafeArea()
+
+ // title
+ Text(L10n.Contacts.title)
+ .font(.head2)
+ .foregroundColor(Color.Main.black)
+
+ HStack {
+ Spacer()
+ Image(systemName: "plus")
+ .foregroundColor(Color.Material.greenDark500)
+ .tappablePadding(.symmetric(12)) {
+ addPanelPresented = true
+ }
+ }
+ .padding(.horizontal, 16)
+ }
+ .frame(height: 44)
+ }
+}
+
+private struct ContactsScreenRow: View {
+ @EnvironmentObject var store: AppStore
+
+ var roster: Roster
+ @State private var isShowingMenu = false
+ @State private var isDeleteAlertPresented = false
+
+ @Binding var isErrorAlertPresented: Bool
+ @Binding var errorAlertMessage: String
+ @Binding var isShowingLoader: Bool
+
+ var body: some View {
+ VStack(spacing: 0) {
+ HStack(spacing: 8) {
+ ZStack {
+ Circle()
+ .frame(width: 44, height: 44)
+ .foregroundColor(.red)
+ Text(roster.name?.firstLetter ?? roster.contactBareJid.firstLetter)
+ .foregroundColor(.white)
+ .font(.body1)
+ }
+ Text(roster.contactBareJid)
+ .foregroundColor(Color.Main.black)
+ .font(.body2)
+ Spacer()
+ }
+ .padding(.horizontal, 16)
+ .padding(.vertical, 4)
+ Rectangle()
+ .frame(maxWidth: .infinity)
+ .frame(height: 1)
+ .foregroundColor(.Main.backgroundDark)
+ }
+ .sharedListRow()
+ .onTapGesture {
+ // state.startChat(roster)
+ }
+ .onLongPressGesture {
+ isShowingMenu.toggle()
+ }
+ .swipeActions(edge: .trailing, allowsFullSwipe: false) {
+ Button {
+ isDeleteAlertPresented = true
+ } label: {
+ Label(L10n.Contacts.sendMessage, systemImage: "trash")
+ }
+ .tint(Color.red)
+ }
+ .contextMenu {
+ Button(L10n.Contacts.sendMessage, systemImage: "message") {
+ // state.startChat(roster)
+ }
+ Divider()
+
+ Button(L10n.Contacts.editContact) {
+ print("Edit contact")
+ }
+
+ Button(L10n.Contacts.selectContact) {
+ print("Select contact")
+ }
+
+ Divider()
+ Button(L10n.Contacts.deleteContact, systemImage: "trash", role: .destructive) {
+ isDeleteAlertPresented = true
+ }
+ }
+ .actionSheet(isPresented: $isDeleteAlertPresented) {
+ ActionSheet(
+ title: Text(L10n.Contacts.Delete.title),
+ message: Text(L10n.Contacts.Delete.message),
+ buttons: [
+ .destructive(Text(L10n.Contacts.Delete.deleteFromDevice)) {
+ store.dispatch(.rostersAction(.markRosterAsLocallyDeleted(ownerJID: roster.bareJid, contactJID: roster.contactBareJid)))
+ },
+ .destructive(Text(L10n.Contacts.Delete.deleteCompletely)) {
+ isShowingLoader = true
+ store.dispatch(.rostersAction(.deleteRoster(ownerJID: roster.bareJid, contactJID: roster.contactBareJid)))
+ },
+ .cancel(Text(L10n.Global.cancel))
+ ]
+ )
+ }
+ .onChange(of: store.state.rostersState.rosters) { _ in
+ endOfDeleting()
+ }
+ .onChange(of: store.state.rostersState.deleteRosterError) { _ in
+ endOfDeleting()
+ }
+ }
+
+ private func endOfDeleting() {
+ if isShowingLoader {
+ isShowingLoader = false
+ if let error = store.state.rostersState.deleteRosterError {
+ errorAlertMessage = error
+ isErrorAlertPresented = true
+ }
+ }
+ }
+}
diff --git a/ConversationsClassic/View/Screens/RegistrationScreen.swift b/ConversationsClassic/View/Screens/RegistrationScreen.swift
new file mode 100644
index 0000000..40a2ede
--- /dev/null
+++ b/ConversationsClassic/View/Screens/RegistrationScreen.swift
@@ -0,0 +1,20 @@
+import SwiftUI
+
+struct RegistrationScreen: View {
+ // @EnvironmentObject var state: AppState
+
+ public var body: some View {
+ ZStack {
+ Color.Main.backgroundLight
+ Button {
+ // state.flow = .welcome
+ } label: {
+ VStack {
+ Text("Not yet implemented")
+ Text(L10n.Global.back)
+ }
+ }
+ }
+ .ignoresSafeArea()
+ }
+}
diff --git a/ConversationsClassic/View/Screens/SettingsScreen.swift b/ConversationsClassic/View/Screens/SettingsScreen.swift
new file mode 100644
index 0000000..68a9614
--- /dev/null
+++ b/ConversationsClassic/View/Screens/SettingsScreen.swift
@@ -0,0 +1,20 @@
+import SwiftUI
+
+struct SettingsScreen: View {
+ var body: some View {
+ ZStack {
+ // bg
+ Color.Main.backgroundLight
+ .ignoresSafeArea()
+
+ // content
+ Text("under construction...")
+
+ // tab bar
+ VStack {
+ Spacer()
+ SharedTabBar()
+ }
+ }
+ }
+}
diff --git a/ConversationsClassic/View/Screens/StartScreen.swift b/ConversationsClassic/View/Screens/StartScreen.swift
new file mode 100644
index 0000000..79b100c
--- /dev/null
+++ b/ConversationsClassic/View/Screens/StartScreen.swift
@@ -0,0 +1,19 @@
+import SwiftUI
+
+struct StartScreen: View {
+ @EnvironmentObject var store: AppStore
+
+ var body: some View {
+ ZStack {
+ Color.Main.backgroundLight
+ Image.logo
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 200, height: 200)
+ }
+ .ignoresSafeArea()
+ .onAppear {
+ store.dispatch(.startAction(.loadStoredAccounts))
+ }
+ }
+}
diff --git a/ConversationsClassic/View/Screens/WelcomeScreen.swift b/ConversationsClassic/View/Screens/WelcomeScreen.swift
new file mode 100644
index 0000000..f2ef3b8
--- /dev/null
+++ b/ConversationsClassic/View/Screens/WelcomeScreen.swift
@@ -0,0 +1,53 @@
+import SwiftUI
+
+struct WelcomeScreen: View {
+ @EnvironmentObject var store: AppStore
+
+ public var body: some View {
+ ZStack {
+ // background
+ Color.Main.backgroundLight
+ .ignoresSafeArea()
+
+ // content
+ VStack(spacing: 32) {
+ // icon
+ Image.logo
+ .resizable()
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 120, height: 120)
+
+ // texts
+ VStack(spacing: 10) {
+ Text(L10n.Global.name)
+ .font(.head1r)
+ .foregroundColor(.Material.tortoiseDark500)
+ .fixedSize(horizontal: true, vertical: false)
+ Text(L10n.Start.subtitle)
+ .font(.body2)
+ .foregroundColor(.Material.tortoiseDark300)
+ .fixedSize(horizontal: false, vertical: true)
+ .multilineTextAlignment(.center)
+ }
+
+ // buttons
+ VStack(spacing: 16) {
+ Button {
+ store.dispatch(.accountsAction(.goTo(.addAccount)))
+ store.dispatch(.changeFlow(.accounts))
+ } label: {
+ Text(L10n.Start.Btn.login)
+ }
+ .buttonStyle(SecondaryButtonStyle())
+ Button {
+ // state.flow = .registration
+ } label: {
+ Text(L10n.Start.Btn.register)
+ }
+ .buttonStyle(PrimaryButtonStyle())
+ }
+ }
+ .padding(.horizontal, 32)
+ }
+ }
+}
diff --git a/ConversationsClassic/View/UIToolkit/Binding+Extensions.swift b/ConversationsClassic/View/UIToolkit/Binding+Extensions.swift
new file mode 100644
index 0000000..6a2a796
--- /dev/null
+++ b/ConversationsClassic/View/UIToolkit/Binding+Extensions.swift
@@ -0,0 +1,12 @@
+import SwiftUI
+
+extension Binding where Value == String {
+ func max(_ limit: Int) -> Self {
+ if wrappedValue.count > limit {
+ DispatchQueue.main.async {
+ wrappedValue = String(wrappedValue.dropLast())
+ }
+ }
+ return self
+ }
+}
diff --git a/ConversationsClassic/View/UIToolkit/ButtonStyles.swift b/ConversationsClassic/View/UIToolkit/ButtonStyles.swift
new file mode 100644
index 0000000..bd26ac2
--- /dev/null
+++ b/ConversationsClassic/View/UIToolkit/ButtonStyles.swift
@@ -0,0 +1,50 @@
+import SwiftUI
+
+private enum ButtonSizes {
+ static let padding = 16.0
+ static let cornerRadius = 4.0
+ static let scaleEffect: CGFloat = 0.9
+ static let opacity: Double = 0.6
+}
+
+struct PrimaryButtonStyle: ButtonStyle {
+ @Environment(\.isEnabled) private var isEnabled
+
+ func makeBody(configuration: Configuration) -> some View {
+ configuration
+ .label
+ .font(.head2)
+ .padding(ButtonSizes.padding)
+ .frame(maxWidth: .infinity)
+ .foregroundColor(.Main.white)
+ .background {
+ RoundedRectangle(cornerRadius: ButtonSizes.cornerRadius)
+ .foregroundColor(isEnabled ? .Material.greenDark500 : .Main.separator)
+ }
+ .contentShape(Rectangle())
+ .scaleEffect(configuration.isPressed ? ButtonSizes.scaleEffect : 1.0)
+ .opacity(configuration.isPressed ? ButtonSizes.opacity : 1.0)
+ .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
+ }
+}
+
+struct SecondaryButtonStyle: ButtonStyle {
+ @Environment(\.isEnabled) private var isEnabled
+
+ func makeBody(configuration: Configuration) -> some View {
+ configuration
+ .label
+ .font(.head2)
+ .padding(ButtonSizes.padding)
+ .frame(maxWidth: .infinity)
+ .foregroundColor(isEnabled ? .Material.greenDark500 : .Main.separator)
+ .background {
+ RoundedRectangle(cornerRadius: ButtonSizes.cornerRadius)
+ .stroke(isEnabled ? Color.Material.greenDark500 : Color.Main.separator)
+ }
+ .contentShape(Rectangle())
+ .scaleEffect(configuration.isPressed ? ButtonSizes.scaleEffect : 1.0)
+ .opacity(configuration.isPressed ? ButtonSizes.opacity : 1.0)
+ .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
+ }
+}
diff --git a/ConversationsClassic/View/UIToolkit/EdgeInsets+Extensions.swift b/ConversationsClassic/View/UIToolkit/EdgeInsets+Extensions.swift
new file mode 100644
index 0000000..65f5311
--- /dev/null
+++ b/ConversationsClassic/View/UIToolkit/EdgeInsets+Extensions.swift
@@ -0,0 +1,15 @@
+import SwiftUI
+
+extension EdgeInsets {
+ var inverted: EdgeInsets {
+ .init(top: -top, leading: -leading, bottom: -bottom, trailing: -trailing)
+ }
+
+ static var zero: EdgeInsets {
+ .init(top: 0, leading: 0, bottom: 0, trailing: 0)
+ }
+
+ static func symmetric(_ value: CGFloat) -> EdgeInsets {
+ .init(top: value, leading: value, bottom: value, trailing: value)
+ }
+}
diff --git a/ConversationsClassic/View/UIToolkit/KeyboardDisposableModifier.swift b/ConversationsClassic/View/UIToolkit/KeyboardDisposableModifier.swift
new file mode 100644
index 0000000..8b372fd
--- /dev/null
+++ b/ConversationsClassic/View/UIToolkit/KeyboardDisposableModifier.swift
@@ -0,0 +1,22 @@
+import SwiftUI
+
+private struct ContentBlockModifier: ViewModifier {
+ var focus: FocusState.Binding
+
+ func body(content: Content) -> some View {
+ content
+ .background {
+ Rectangle()
+ .foregroundColor(.white.opacity(0.01))
+ .onTapGesture {
+ focus.wrappedValue = nil
+ }
+ }
+ }
+}
+
+extension View {
+ func keyboardUnfocus(_ focus: FocusState.Binding) -> some View {
+ self.modifier(ContentBlockModifier(focus: focus))
+ }
+}
diff --git a/ConversationsClassic/View/UIToolkit/Typography.swift b/ConversationsClassic/View/UIToolkit/Typography.swift
new file mode 100644
index 0000000..17ed3a9
--- /dev/null
+++ b/ConversationsClassic/View/UIToolkit/Typography.swift
@@ -0,0 +1,12 @@
+import Foundation
+import SwiftUI
+
+extension Font {
+ static let head1l = Font.system(size: 34, weight: .light, design: .rounded)
+ static let head1r = Font.system(size: 34, weight: .regular, design: .rounded)
+ static let head2 = Font.system(size: 20, weight: .regular, design: .rounded)
+ static let body1 = Font.system(size: 18, weight: .regular, design: .rounded)
+ static let body2 = Font.system(size: 16, weight: .regular, design: .rounded)
+ static let body3 = Font.system(size: 14, weight: .regular, design: .rounded)
+ static let sub1 = Font.system(size: 10, weight: .regular, design: .rounded)
+}
diff --git a/ConversationsClassic/View/UIToolkit/View+Debug.swift b/ConversationsClassic/View/UIToolkit/View+Debug.swift
new file mode 100644
index 0000000..34f7c65
--- /dev/null
+++ b/ConversationsClassic/View/UIToolkit/View+Debug.swift
@@ -0,0 +1,35 @@
+import SwiftUI
+
+#if DEBUG
+ private let rainbowDebugColors = [
+ Color.purple,
+ Color.blue,
+ Color.green,
+ Color.yellow,
+ Color.orange,
+ Color.red,
+ Color.pink,
+ Color.black.opacity(0.5),
+ Color.teal,
+ Color.gray,
+ Color.mint,
+ Color.cyan
+ ]
+
+ public extension Color {
+ static func random(randomOpacity: Bool = false) -> Color {
+ Color(
+ red: .random(in: 0 ... 1),
+ green: .random(in: 0 ... 1),
+ blue: .random(in: 0 ... 1),
+ opacity: randomOpacity ? .random(in: 0 ... 1) : 1
+ )
+ }
+ }
+
+ extension View {
+ func rainbowDebug() -> some View {
+ background(Color.random())
+ }
+ }
+#endif
diff --git a/ConversationsClassic/View/UIToolkit/View+Loader.swift b/ConversationsClassic/View/UIToolkit/View+Loader.swift
new file mode 100644
index 0000000..7b93478
--- /dev/null
+++ b/ConversationsClassic/View/UIToolkit/View+Loader.swift
@@ -0,0 +1,40 @@
+import Foundation
+import SwiftUI
+
+public extension View {
+ func loadingIndicator(_ isShowing: Bool) -> some View {
+ modifier(LoadingIndicator(isShowing: isShowing))
+ }
+}
+
+struct LoadingIndicator: ViewModifier {
+ var isShowing: Bool
+
+ func body(content: Content) -> some View {
+ ZStack {
+ content
+ if isShowing {
+ loadingView
+ }
+ }
+ }
+
+ private var loadingView: some View {
+ GeometryReader { proxyReader in
+ ZStack {
+ Color.Tango.blueLight.opacity(0.3)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+
+ // loader
+ ProgressView()
+ .progressViewStyle(
+ CircularProgressViewStyle(tint: .Material.greenDark800)
+ )
+ .position(x: proxyReader.size.width / 2, y: proxyReader.size.height / 2)
+ .controlSize(.large)
+ }
+ }
+ .ignoresSafeArea()
+ .transition(AnyTransition.opacity.animation(.easeInOut(duration: 0.1)))
+ }
+}
diff --git a/ConversationsClassic/View/UIToolkit/View+OnLoad.swift b/ConversationsClassic/View/UIToolkit/View+OnLoad.swift
new file mode 100644
index 0000000..4206bff
--- /dev/null
+++ b/ConversationsClassic/View/UIToolkit/View+OnLoad.swift
@@ -0,0 +1,27 @@
+import SwiftUI
+
+// MARK: - On load
+extension View {
+ func onLoad(_ action: @escaping () -> Void) -> some View {
+ modifier(ViewDidLoadModifier(action))
+ }
+}
+
+private struct ViewDidLoadModifier: ViewModifier {
+ private let action: () -> Void
+
+ @State private var didLoad = false
+
+ init(_ action: @escaping () -> Void) {
+ self.action = action
+ }
+
+ func body(content: Content) -> some View {
+ content.onAppear {
+ if !didLoad {
+ didLoad.toggle()
+ action()
+ }
+ }
+ }
+}
diff --git a/ConversationsClassic/View/UIToolkit/View+TappableArea.swift b/ConversationsClassic/View/UIToolkit/View+TappableArea.swift
new file mode 100644
index 0000000..8c659d4
--- /dev/null
+++ b/ConversationsClassic/View/UIToolkit/View+TappableArea.swift
@@ -0,0 +1,27 @@
+import SwiftUI
+
+extension View {
+ func tappablePadding(_ insets: EdgeInsets, onTap: @escaping () -> Void) -> some View {
+ modifier(TappablePadding(insets: insets, onTap: onTap))
+ }
+}
+
+struct TappablePadding: ViewModifier {
+ let insets: EdgeInsets
+ let onTap: () -> Void
+
+ public init(insets: EdgeInsets, onTap: @escaping () -> Void) {
+ self.insets = insets
+ self.onTap = onTap
+ }
+
+ public func body(content: Content) -> some View {
+ content
+ .padding(insets)
+ .contentShape(Rectangle())
+ .onTapGesture {
+ onTap()
+ }
+ .padding(insets.inverted)
+ }
+}
diff --git a/project.yml b/project.yml
new file mode 100644
index 0000000..abf844b
--- /dev/null
+++ b/project.yml
@@ -0,0 +1,85 @@
+---
+name: ConversationsClassic
+
+options:
+ postGenCommand: swiftgen
+
+packages:
+ MartinOMEMO:
+ url: https://github.com/tigase/MartinOMEMO
+ majorVersion: 2.2.3
+ KeychainAccess:
+ url: https://github.com/kishikawakatsumi/KeychainAccess.git
+ majorVersion: 4.2.2
+ GRDB:
+ url: https://github.com/groue/GRDB.swift.git
+ branch: master
+ # GRDBQuery:
+ # url: https://github.com/groue/GRDBQuery.git
+ # branch: main
+
+settings:
+ DEVELOPMENT_TEAM: U6CKGHL5VR
+
+targets:
+ # Notification service here...
+ #
+
+ # Sharing service here...
+ #
+
+ # iOS App
+ ConversationsClassic:
+ type: application
+ platform: iOS
+ deploymentTarget: 16.0
+ scheme: {}
+ info:
+ path: Info.plist
+ properties:
+ UISupportedInterfaceOrientations: [UIInterfaceOrientationPortrait]
+ UILaunchStoryboardName: launchscreen.storyboard
+ NSAppTransportSecurity:
+ NSAllowsArbitraryLoads: true
+ # UIViewControllerBasedStatusBarAppearance: NO
+ # UIStatusBarStyle: UIStatusBarStyleLightContent
+ # NSCameraUsageDescription: Required for document and facial capture
+ # NSFaceIDUsageDescription: Required for accessing to account info
+ # UIUserInterfaceStyle: Light
+ CFBundleDisplayName: Conversations
+ sources:
+ - path: ConversationsClassic
+ excludes:
+ - .nvim
+ settings:
+ TARGETED_DEVICE_FAMILY: 1
+ DEBUG_INFORMATION_FORMAT: dwarf-with-dsym
+ PRODUCT_BUNDLE_IDENTIFIER: imt.narayana.ConversationsClassic.ios
+ DEAD_CODE_STRIPPING: true
+ # CODE_SIGN_ENTITLEMENTS: ConversationsClassic/ConversationsClassic.entitlements
+ # entitlements:
+ # path: ConversationsClassic/ConversationsClassic.entitlements
+ # properties:
+ # com.apple.security.application-groups: group.ConversationsClassic.engine
+ # keychain-access-groups: imt.narayana.ConversationsClassic.ios
+ dependencies:
+ - sdk: Security.framework
+ # - framework: Lib/WebRTC.xcframework
+ # - target: Engine
+ - package: MartinOMEMO
+ link: true
+ - package: KeychainAccess
+ limk: true
+ - package: GRDB
+ link: true
+ # - package: GRDBQuery
+ # link: true
+ preBuildScripts:
+ - script: swiftlint
+ name: Swiftlint
+ basedOnDependencyAnalysis: false
+ - script: ./.swiftgen/bin/swiftgen
+ name: SwiftGen
+ basedOnDependencyAnalysis: false
+
+parallelizeBuild: true
diff --git a/swiftgen.yml b/swiftgen.yml
new file mode 100644
index 0000000..c9f65a1
--- /dev/null
+++ b/swiftgen.yml
@@ -0,0 +1,27 @@
+---
+output_dir: ConversationsClassic/Generated
+input_dir: ConversationsClassic/Resources
+
+xcassets:
+ - inputs: Assets/Images.xcassets
+ outputs:
+ - templatePath: .swiftgen/templates/xcassets.stencil
+ params:
+ forceProvidesNamespaces: true
+ enumName: Image
+ output: Images+Generated.swift
+ - inputs: Assets/Colors.xcassets
+ outputs:
+ - templatePath: .swiftgen/templates/xcassets.stencil
+ params:
+ forceProvidesNamespaces: true
+ enumName: Color
+ output: Colors+Generated.swift
+
+strings:
+ inputs: Strings/Localizable.strings
+ outputs:
+ # - templatePath: .templates/strings.stencil
+ - templateName: structured-swift5
+
+ output: Strings+Generated.swift