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