Compare commits
187 commits
Author | SHA1 | Date | |
---|---|---|---|
fmodf | 3fbc18cbda | ||
fmodf | b1ca812c56 | ||
fmodf | af7664d2d3 | ||
fmodf | 0e2c135bea | ||
fmodf | 3676bd53ae | ||
fmodf | df54199806 | ||
fmodf | 3b82929f38 | ||
fmodf | 53ee8123a5 | ||
fmodf | 2074fb7f8f | ||
fmodf | 17f310dd6a | ||
fmodf | e2002057db | ||
fmodf | e39dbb2e49 | ||
fmodf | 841387aab7 | ||
fmodf | e6fe7c538c | ||
fmodf | 4f90a0d744 | ||
fmodf | 6a608ead2c | ||
fmodf | f353f5e491 | ||
fmodf | 9315a89f65 | ||
fmodf | 78de092b06 | ||
fmodf | d6f08ca691 | ||
fmodf | 9909611674 | ||
fmodf | 5689a9e342 | ||
fmodf | c02b0ce41f | ||
fmodf | 232fd26b04 | ||
fmodf | 633b4fa2b0 | ||
fmodf | 473cf3b30b | ||
fmodf | c153985bf0 | ||
fmodf | bec095e8de | ||
fmodf | acf2807056 | ||
fmodf | c426847700 | ||
fmodf | 0bd2b7d82d | ||
fmodf | 339aab2bb4 | ||
fmodf | bb76ac49a1 | ||
fmodf | 965b4d4f38 | ||
fmodf | 4f379adfb7 | ||
fmodf | 2e9b4f5d19 | ||
fmodf | 50d64cf96b | ||
fmodf | 5fe8762e1f | ||
fmodf | 8fb9ef52d8 | ||
fmodf | 441409e676 | ||
fmodf | c27a935a3f | ||
fmodf | db66442393 | ||
fmodf | d0cbe63eb1 | ||
fmodf | 5f5e20b462 | ||
fmodf | a60fae1a3a | ||
fmodf | 40ebe72e1b | ||
fmodf | edf1b8782c | ||
fmodf | 09d3a6e606 | ||
fmodf | 61ec1b841e | ||
fmodf | bfd9757a37 | ||
fmodf | 4399b81ec8 | ||
fmodf | fad7112d69 | ||
fmodf | 18083e0b19 | ||
fmodf | 0a57e0648f | ||
fmodf | b3b3b3aef7 | ||
fmodf | 44ef6c25ba | ||
fmodf | eed175fe1f | ||
fmodf | a567c210c4 | ||
fmodf | 6a167f6c2c | ||
fmodf | 13b34c90de | ||
fmodf | 0eeb8f7b75 | ||
fmodf | 05b0b01ede | ||
fmodf | 889211683b | ||
fmodf | 794c50fed0 | ||
fmodf | 82a05e9b6d | ||
fmodf | c8fa4a85b9 | ||
fmodf | 0aafcf6362 | ||
fmodf | 3685ee56e2 | ||
fmodf | bf77fb8188 | ||
fmodf | 6006275690 | ||
fmodf | aea4330bca | ||
fmodf | 59f802d385 | ||
fmodf | 77b9aa8d3a | ||
fmodf | 178f22a140 | ||
fmodf | 9155aa5ad8 | ||
fmodf | 3756dfa527 | ||
fmodf | 86745dc87d | ||
fmodf | 822c4fe749 | ||
fmodf | 3fc0be729e | ||
fmodf | 7ada932664 | ||
fmodf | 8568b1afa6 | ||
fmodf | 2dc41c2013 | ||
fmodf | 002e152604 | ||
fmodf | 6ce16b1f3b | ||
fmodf | 7bb48e8719 | ||
fmodf | 19e6455e4d | ||
fmodf | eb064d4e33 | ||
fmodf | 0ca8ec93a7 | ||
fmodf | e448dc6823 | ||
fmodf | 7ca8af3bdc | ||
fmodf | 6974ac5b7d | ||
fmodf | 69de25593a | ||
fmodf | d726e1fbdd | ||
fmodf | 269d56a07b | ||
fmodf | efaaf0a4dc | ||
fmodf | 36d030b696 | ||
fmodf | 7666b71ef9 | ||
fmodf | 1780360fb4 | ||
fmodf | c1ce9b133d | ||
fmodf | cb1f159a7a | ||
fmodf | 73c7aa5563 | ||
fmodf | e21d1a1ce9 | ||
fmodf | bb502ba79a | ||
fmodf | a02ca5b04f | ||
fmodf | 9c5c54e09e | ||
fmodf | 14a83ca1d8 | ||
fmodf | 18ea45257b | ||
fmodf | 318d792928 | ||
fmodf | 36bfdf9dcb | ||
fmodf | c5a631d546 | ||
fmodf | ae7a13e92b | ||
fmodf | d2b536509a | ||
fmodf | 37936b9903 | ||
fmodf | f7eee58347 | ||
fmodf | e21610d425 | ||
fmodf | 50fba234b0 | ||
fmodf | 0ede68e39a | ||
fmodf | a28d60e128 | ||
fmodf | 07993baddc | ||
fmodf | f89831f82d | ||
fmodf | 32086a28e8 | ||
fmodf | 5fc5457bf0 | ||
fmodf | ad8f4be1c7 | ||
fmodf | ec7b075b35 | ||
fmodf | e564ae5747 | ||
fmodf | 485071162c | ||
fmodf | e2363f9f9f | ||
fmodf | eb06abaebf | ||
fmodf | 08f3b548a6 | ||
fmodf | b910b2ac4b | ||
fmodf | 944f38d301 | ||
fmodf | 8da928e237 | ||
fmodf | 3361b828ef | ||
fmodf | 103dd130ca | ||
fmodf | b3518ea71b | ||
fmodf | ce3bc42fbb | ||
fmodf | ac850bfe4a | ||
fmodf | c9021b964a | ||
fmodf | 170c0daa5b | ||
fmodf | dfa048e918 | ||
fmodf | c3679c9a2a | ||
fmodf | 528e474d91 | ||
fmodf | d4e4c18762 | ||
fmodf | f679c7d357 | ||
fmodf | b309574c78 | ||
fmodf | 385b3e6a74 | ||
fmodf | c2fb21f932 | ||
fmodf | 8aa1ed6b75 | ||
fmodf | 3aca0a69c1 | ||
fmodf | 6818182f66 | ||
fmodf | 29f3507986 | ||
fmodf | c674abe0c6 | ||
fmodf | d93cb63e0a | ||
fmodf | 7368042d81 | ||
fmodf | f60c14cc74 | ||
fmodf | ce85b7dff9 | ||
fmodf | cdccfb9e3e | ||
fmodf | 024c9d85c8 | ||
fmodf | 778130ac65 | ||
fmodf | 21ca70d747 | ||
fmodf | 01f45af8f3 | ||
fmodf | 21b4e772d8 | ||
fmodf | 57ba06a94e | ||
fmodf | a4ccd9af0d | ||
fmodf | 9329cd3ab5 | ||
fmodf | 58b69e7aa2 | ||
fmodf | c0ae9c8c4c | ||
fmodf | 502f6f1cde | ||
fmodf | c3bd783769 | ||
fmodf | 31592a0e17 | ||
fmodf | ac58f634a0 | ||
fmodf | 73017d8d80 | ||
fmodf | b0ef155be8 | ||
fmodf | 1e27b8643c | ||
fmodf | 995d627fde | ||
fmodf | 20c89c65e9 | ||
fmodf | e23a312538 | ||
fmodf | fc88c50ae8 | ||
fmodf | 2b3e50eb77 | ||
fmodf | 5df60bd867 | ||
fmodf | 9b4323ccd3 | ||
fmodf | 8ce21712b7 | ||
fmodf | 9f91741354 | ||
fmodf | 47943f15ff | ||
fmodf | 0c8df19d55 | ||
fmodf | f8a38b0fdd | ||
fmodf | a182726352 |
7
.gitignore
vendored
7
.gitignore
vendored
|
@ -112,12 +112,13 @@ xcuserdata
|
||||||
|
|
||||||
*.moved-aside
|
*.moved-aside
|
||||||
/.idea
|
/.idea
|
||||||
/ConversationsClassic/.idea
|
/AnotherIM/.idea
|
||||||
/ConversationsClassic.xcodeproj
|
/AnotherIM.xcodeproj
|
||||||
/Info.plist
|
/Info.plist
|
||||||
/ConversationsClassic/ConversationsClassic.entitlements
|
/AnotherIM/AnotherIM.entitlements
|
||||||
/XMPPSwift/Client/VoIP/rickroll.mp4
|
/XMPPSwift/Client/VoIP/rickroll.mp4
|
||||||
/.nvim
|
/.nvim
|
||||||
/buildServer.json
|
/buildServer.json
|
||||||
TODO.txt
|
TODO.txt
|
||||||
PASSWD.txt
|
PASSWD.txt
|
||||||
|
sandbox.xml
|
||||||
|
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>BuildMachineOSBuild</key>
|
||||||
|
<string></string>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>SwiftGen_SwiftGenCLI</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>SwiftGen.SwiftGenCLI.resources</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>SwiftGen_SwiftGenCLI</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>BNDL</string>
|
||||||
|
<key>CFBundleSupportedPlatforms</key>
|
||||||
|
<array>
|
||||||
|
<string>MacOSX</string>
|
||||||
|
</array>
|
||||||
|
<key>DTCompiler</key>
|
||||||
|
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||||
|
<key>DTPlatformBuild</key>
|
||||||
|
<string>13A233</string>
|
||||||
|
<key>DTPlatformName</key>
|
||||||
|
<string>macosx</string>
|
||||||
|
<key>DTPlatformVersion</key>
|
||||||
|
<string>11.3</string>
|
||||||
|
<key>DTSDKBuild</key>
|
||||||
|
<string>20E214</string>
|
||||||
|
<key>DTSDKName</key>
|
||||||
|
<string>macosx11.3</string>
|
||||||
|
<key>DTXcode</key>
|
||||||
|
<string>1300</string>
|
||||||
|
<key>DTXcodeBuild</key>
|
||||||
|
<string>13A233</string>
|
||||||
|
<key>LSMinimumSystemVersion</key>
|
||||||
|
<string>10.11</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
||||||
|
/// <span style="display:block;width:3em;height:2em;border:1px solid black;background:#{{color.red}}{{color.green}}{{color.blue}}"></span>
|
||||||
|
/// Alpha: {{color.alpha|hexToInt|int255toFloat|percent}} <br/> (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 %}
|
|
@ -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 %}
|
||||||
|
/// <span style="display:block;width:3em;height:2em;border:1px solid black;background:#{{color.red}}{{color.green}}{{color.blue}}"></span>
|
||||||
|
/// Alpha: {{color.alpha|hexToInt|int255toFloat|percent}} <br/> (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 %}
|
|
@ -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
|
|
@ -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
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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<T{% if not isAppKit %}: UIViewController{% endif %}> {
|
||||||
|
{{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<T{% if not isAppKit %}: UIViewController{% endif %}> {
|
||||||
|
{{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 %}
|
|
@ -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<T{% if not isAppKit %}: UIViewController{% endif %}> {
|
||||||
|
{{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<T{% if not isAppKit %}: UIViewController{% endif %}> {
|
||||||
|
{{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 %}
|
|
@ -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<S: SegueType>(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 %}
|
|
@ -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<S: SegueType>(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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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<T>(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<T>(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 %}
|
|
@ -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<T>(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<T>(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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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<T>(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<T>(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 %}
|
|
@ -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<T>(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<T>(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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1,68 @@
|
||||||
|
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
|
||||||
|
|
||||||
|
{% if tables.count > 0 %}
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
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<CChar>" %}
|
||||||
|
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 %}
|
|
@ -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<CChar>" %}
|
||||||
|
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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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<ARReferenceImage> {
|
||||||
|
return ARReferenceImage.referenceImages(in: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 12.0, *)
|
||||||
|
{{accessModifier}} var referenceObjects: Set<ARReferenceObject> {
|
||||||
|
return ARReferenceObject.referenceObjects(in: self)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
@available(iOS 11.3, *)
|
||||||
|
{{accessModifier}} extension ARReferenceImage {
|
||||||
|
static func referenceImages(in asset: {{arResourceGroupType}}) -> Set<ARReferenceImage> {
|
||||||
|
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<ARReferenceObject> {
|
||||||
|
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 %}
|
|
@ -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<ARReferenceImage> {
|
||||||
|
return ARReferenceImage.referenceImages(in: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(iOS 12.0, *)
|
||||||
|
{{accessModifier}} var referenceObjects: Set<ARReferenceObject> {
|
||||||
|
return ARReferenceObject.referenceObjects(in: self)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(iOS)
|
||||||
|
@available(iOS 11.3, *)
|
||||||
|
{{accessModifier}} extension ARReferenceImage {
|
||||||
|
static func referenceImages(in asset: {{arResourceGroupType}}) -> Set<ARReferenceImage> {
|
||||||
|
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<ARReferenceObject> {
|
||||||
|
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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
BIN
.swiftgen/bin/swiftgen
Executable file
BIN
.swiftgen/bin/swiftgen
Executable file
Binary file not shown.
29
.swiftgen/templates/fonts.stencil
Normal file
29
.swiftgen/templates/fonts.stencil
Normal file
|
@ -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
|
85
.swiftgen/templates/strings.stencil
Normal file
85
.swiftgen/templates/strings.stencil
Normal file
|
@ -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
|
48
.swiftgen/templates/xcassets.stencil
Normal file
48
.swiftgen/templates/xcassets.stencil
Normal file
|
@ -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
|
36
.swiftgen/templates/xcassets_strings.stencil
Normal file
36
.swiftgen/templates/xcassets_strings.stencil
Normal file
|
@ -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
|
121
.swiftlint.yml
Normal file
121
.swiftlint.yml
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
---
|
||||||
|
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:
|
||||||
|
warninig: 3
|
||||||
|
error: 0
|
||||||
|
max_length:
|
||||||
|
warninig: 40
|
||||||
|
error: 80
|
||||||
|
|
||||||
|
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
|
||||||
|
- _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:
|
||||||
|
- .swiftgen
|
||||||
|
- "**/Generated"
|
34
AnotherIM/AnotherIMApp.swift
Normal file
34
AnotherIM/AnotherIMApp.swift
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import Combine
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
@MainActor
|
||||||
|
struct AnotherIMApp: App {
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
|
private let clientsStore = ClientsStore.shared
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// There's a bug on iOS 17 where sheet may not load with large title, even if modifiers are set, which causes some tests to fail
|
||||||
|
// https://stackoverflow.com/questions/77253122/swiftui-navigationstack-title-loads-inline-instead-of-large-when-sheet-is-pres
|
||||||
|
UINavigationBar.appearance().prefersLargeTitles = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
RootView()
|
||||||
|
.environmentObject(clientsStore)
|
||||||
|
.onChange(of: scenePhase) { phase in
|
||||||
|
switch phase {
|
||||||
|
case .active:
|
||||||
|
clientsStore.reconnectOnActiveState()
|
||||||
|
|
||||||
|
case .inactive, .background:
|
||||||
|
fallthrough
|
||||||
|
|
||||||
|
@unknown default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
AnotherIM/AppData/AppError.swift
Normal file
12
AnotherIM/AppData/AppError.swift
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
enum AppError: Error {
|
||||||
|
case clientNotFound
|
||||||
|
case rosterNotFound
|
||||||
|
case imageNotFound
|
||||||
|
case videoNotFound
|
||||||
|
case noData
|
||||||
|
case fileTooBig
|
||||||
|
case invalidContentType
|
||||||
|
case invalidLocalName
|
||||||
|
case featureNotSupported
|
||||||
|
case securityError
|
||||||
|
}
|
48
AnotherIM/AppData/Client/Client+MartinCarbons.swift
Normal file
48
AnotherIM/AppData/Client/Client+MartinCarbons.swift
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
final class ClientMartinCarbonsManager {
|
||||||
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
init(_ xmppConnection: XMPPClient) {
|
||||||
|
// subscribe to carbons
|
||||||
|
let ctx = xmppConnection.module(MessageCarbonsModule.self).context
|
||||||
|
xmppConnection.module(MessageCarbonsModule.self).carbonsPublisher
|
||||||
|
.sink { [weak self] carbon in
|
||||||
|
self?.handleMessage(carbon, ctx)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
// enable carbons if available
|
||||||
|
xmppConnection.module(.messageCarbons).$isAvailable.filter { $0 }
|
||||||
|
.sink(receiveValue: { [weak xmppConnection] _ in
|
||||||
|
xmppConnection?.module(.messageCarbons).enable()
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleMessage(_ received: Martin.MessageCarbonsModule.CarbonReceived, _ context: Context?) {
|
||||||
|
let message = received.message
|
||||||
|
let action = received.action
|
||||||
|
let onJid = received.jid
|
||||||
|
#if DEBUG
|
||||||
|
print("---")
|
||||||
|
print("Carbons message received: \(message)")
|
||||||
|
print("Action: \(action)")
|
||||||
|
print("On JID: \(onJid)")
|
||||||
|
print("---")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if let msg = Message.map(message, context: context) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await msg.save()
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error saving message: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
76
AnotherIM/AppData/Client/Client+MartinChats.swift
Normal file
76
AnotherIM/AppData/Client/Client+MartinChats.swift
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
final class ClientMartinChatsManager: Martin.ChatManager {
|
||||||
|
func chats(for context: Martin.Context) -> [any Martin.ChatProtocol] {
|
||||||
|
do {
|
||||||
|
let chats: [Chat] = try Database.shared.dbQueue.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 Database.shared.dbQueue.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 Database.shared.dbQueue.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,
|
||||||
|
encrypted: UserSettings.secureChatsByDefault
|
||||||
|
)
|
||||||
|
try Database.shared.dbQueue.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
|
||||||
|
}
|
||||||
|
|
||||||
|
func initialize(context _: Martin.Context) {}
|
||||||
|
func deinitialize(context _: Martin.Context) {}
|
||||||
|
}
|
23
AnotherIM/AppData/Client/Client+MartinDisco.swift
Normal file
23
AnotherIM/AppData/Client/Client+MartinDisco.swift
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
final class ClientMartinDiscoManager {
|
||||||
|
private(set) var features: [ServerFeature] = []
|
||||||
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
init(_ xmppConnection: XMPPClient) {
|
||||||
|
// subscribe to client server features
|
||||||
|
xmppConnection.module(DiscoveryModule.self).$serverDiscoResult
|
||||||
|
.sink { [weak self] disco in
|
||||||
|
let allFeatures = ServerFeature.allFeatures
|
||||||
|
let features = disco.features
|
||||||
|
.compactMap { featureId in
|
||||||
|
allFeatures.first(where: { $0.xmppId == featureId })
|
||||||
|
}
|
||||||
|
self?.features = features
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
82
AnotherIM/AppData/Client/Client+MartinMAM.swift
Normal file
82
AnotherIM/AppData/Client/Client+MartinMAM.swift
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
private typealias ArchMsg = Martin.MessageArchiveManagementModule.ArchivedMessageReceived
|
||||||
|
|
||||||
|
final class ClientMartinMAM {
|
||||||
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
private var processor: ArchiveMessageProcessor
|
||||||
|
|
||||||
|
init(_ xmppConnection: XMPPClient) {
|
||||||
|
processor = ArchiveMessageProcessor(xmppConnection.context)
|
||||||
|
// subscribe to archived messages
|
||||||
|
xmppConnection.module(.mam).archivedMessagesPublisher
|
||||||
|
.sink(receiveValue: { [weak self] archived in
|
||||||
|
guard let self = self else { return }
|
||||||
|
Task {
|
||||||
|
await self.processor.append(archived)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private actor ArchiveMessageProcessor {
|
||||||
|
private var accumulator: [ArchMsg] = []
|
||||||
|
private let context: Context?
|
||||||
|
|
||||||
|
init(_ ctx: Context?) {
|
||||||
|
context = ctx
|
||||||
|
Task {
|
||||||
|
while true {
|
||||||
|
try? await Task.sleep(nanoseconds: 700 * NSEC_PER_MSEC)
|
||||||
|
await process()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func append(_ msg: ArchMsg) async {
|
||||||
|
accumulator.append(msg)
|
||||||
|
if accumulator.count >= Const.mamRequestPageSize {
|
||||||
|
await process()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func process() async {
|
||||||
|
if accumulator.isEmpty { return }
|
||||||
|
await handleMessages(accumulator)
|
||||||
|
accumulator.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleMessages(_ received: [ArchMsg]) async {
|
||||||
|
if received.isEmpty { return }
|
||||||
|
try? await Database.shared.dbQueue.write { [weak self] db in
|
||||||
|
for recv in received {
|
||||||
|
let message = recv.message
|
||||||
|
let date = recv.timestamp
|
||||||
|
if let msgId = message.id {
|
||||||
|
if try Message.fetchOne(db, key: msgId) != nil {
|
||||||
|
#if DEBUG
|
||||||
|
print("---")
|
||||||
|
print("Skipping archived message with id \(msgId) (message exists)")
|
||||||
|
print("---")
|
||||||
|
#endif
|
||||||
|
} else {
|
||||||
|
#if DEBUG
|
||||||
|
print("---")
|
||||||
|
print("Archive message received: \(message)")
|
||||||
|
print("Date: \(date)")
|
||||||
|
print("---")
|
||||||
|
#endif
|
||||||
|
if var msg = Message.map(message, context: self?.context) {
|
||||||
|
msg.date = date
|
||||||
|
try msg.insert(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
38
AnotherIM/AppData/Client/Client+MartinMessages.swift
Normal file
38
AnotherIM/AppData/Client/Client+MartinMessages.swift
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
final class ClientMartinMessagesManager {
|
||||||
|
private var cancellables: Set<AnyCancellable> = []
|
||||||
|
|
||||||
|
init(_ xmppConnection: XMPPClient) {
|
||||||
|
xmppConnection.module(MessageModule.self).messagesPublisher
|
||||||
|
.sink { [weak self] message in
|
||||||
|
self?.handleMessage(message)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleMessage(_ received: Martin.MessageModule.MessageReceived) {
|
||||||
|
let message = received.message
|
||||||
|
let chat = received.chat
|
||||||
|
#if DEBUG
|
||||||
|
print("---")
|
||||||
|
print("Message received: \(received)")
|
||||||
|
print("Chat: \(chat)")
|
||||||
|
print("---")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Process image
|
||||||
|
if let msg = Message.map(message, context: chat.context) {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
try await msg.save()
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error saving message: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
366
AnotherIM/AppData/Client/Client+MartinOMEMO.swift
Normal file
366
AnotherIM/AppData/Client/Client+MartinOMEMO.swift
Normal file
|
@ -0,0 +1,366 @@
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
import MartinOMEMO
|
||||||
|
|
||||||
|
final class ClientMartinOMEMO {
|
||||||
|
let credentials: Credentials
|
||||||
|
|
||||||
|
init(_ credentials: Credentials) {
|
||||||
|
self.credentials = credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
var signal: (SignalStorage, SignalContext) {
|
||||||
|
let signalStorage = SignalStorage(sessionStore: self, preKeyStore: self, signedPreKeyStore: self, identityKeyStore: self, senderKeyStore: self)
|
||||||
|
// swiftlint:disable:next force_unwrapping
|
||||||
|
let signalContext = SignalContext(withStorage: signalStorage)!
|
||||||
|
signalStorage.setup(withContext: signalContext)
|
||||||
|
|
||||||
|
_ = regenerateKeys(wipe: false, context: signalContext)
|
||||||
|
|
||||||
|
return (signalStorage, signalContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func regenerateKeys(wipe: Bool = false, context: SignalContext) -> Bool {
|
||||||
|
if wipe {
|
||||||
|
OMEMOSession.wipe(account: credentials.bareJid)
|
||||||
|
OMEMOPreKey.wipe(account: credentials.bareJid)
|
||||||
|
OMEMOSignedPreKey.wipe(account: credentials.bareJid)
|
||||||
|
OMEMOIdentity.wipe(account: credentials.bareJid)
|
||||||
|
UserSettings.set(omemoDeviceId: 0, for: credentials.bareJid)
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasKeyPair = keyPair() != nil
|
||||||
|
let deviceId = UserSettings.get(omemoDeviceIdFor: credentials.bareJid)
|
||||||
|
if wipe || deviceId == 0 || !hasKeyPair {
|
||||||
|
let regId: UInt32 = context.generateRegistrationId()
|
||||||
|
let address = SignalAddress(name: credentials.bareJid, deviceId: Int32(regId))
|
||||||
|
UserSettings.set(omemoDeviceId: regId, for: credentials.bareJid)
|
||||||
|
|
||||||
|
guard let keyPair = SignalIdentityKeyPair.generateKeyPair(context: context), let publicKey = keyPair.publicKey else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let fingerprint = publicKey.map { byte -> String in
|
||||||
|
String(format: "%02x", byte)
|
||||||
|
}.joined()
|
||||||
|
|
||||||
|
return save(address: address, fingerprint: fingerprint, own: true, data: keyPair.serialized())
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func save(address: SignalAddress, fingerprint: String, own: Bool, data: Data) -> Bool {
|
||||||
|
guard !OMEMOIdentity.existsFor(account: credentials.bareJid, name: address.name, fingerprint: fingerprint) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOIdentity(
|
||||||
|
account: credentials.bareJid,
|
||||||
|
name: address.name,
|
||||||
|
deviceId: Int(address.deviceId),
|
||||||
|
fingerprint: fingerprint,
|
||||||
|
key: data,
|
||||||
|
own: own,
|
||||||
|
status: MartinOMEMO.IdentityStatus.trustedActive.rawValue
|
||||||
|
)
|
||||||
|
.insert(db)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error storing identity key: \(error.localizedDescription)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Session
|
||||||
|
extension ClientMartinOMEMO: SignalSessionStoreProtocol {
|
||||||
|
func sessionRecord(forAddress address: MartinOMEMO.SignalAddress) -> Data? {
|
||||||
|
if let key = OMEMOSession.keyFor(account: credentials.bareJid, name: address.name, deviceId: address.deviceId) {
|
||||||
|
return Data(base64Encoded: key)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func allDevices(for name: String, activeAndTrusted: Bool) -> [Int32] {
|
||||||
|
activeAndTrusted ?
|
||||||
|
OMEMOSession.trustedDevicesIdsFor(account: credentials.bareJid, name: name) :
|
||||||
|
OMEMOSession.devicesIdsFor(account: credentials.bareJid, name: name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeSessionRecord(_ data: Data, forAddress: MartinOMEMO.SignalAddress) -> Bool {
|
||||||
|
do {
|
||||||
|
try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOSession(
|
||||||
|
account: credentials.bareJid,
|
||||||
|
name: forAddress.name,
|
||||||
|
deviceId: Int(forAddress.deviceId),
|
||||||
|
key: data.base64EncodedString()
|
||||||
|
)
|
||||||
|
.insert(db)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error storing session info: \(error.localizedDescription)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsSessionRecord(forAddress: MartinOMEMO.SignalAddress) -> Bool {
|
||||||
|
OMEMOSession.keyFor(account: credentials.bareJid, name: forAddress.name, deviceId: forAddress.deviceId) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteSessionRecord(forAddress: MartinOMEMO.SignalAddress) -> Bool {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOSession
|
||||||
|
.filter(Column("account") == credentials.bareJid)
|
||||||
|
.filter(Column("name") == forAddress.name)
|
||||||
|
.filter(Column("deviceId") == forAddress.deviceId)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error deleting session: \(error.localizedDescription)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteAllSessions(for name: String) -> Bool {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOSession
|
||||||
|
.filter(Column("account") == credentials.bareJid)
|
||||||
|
.filter(Column("name") == name)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error deleting all sessions: \(error.localizedDescription)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sessionsWipe() {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOSession
|
||||||
|
.filter(Column("account") == credentials.bareJid)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error wiping sessions: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Identity
|
||||||
|
extension ClientMartinOMEMO: SignalIdentityKeyStoreProtocol {
|
||||||
|
func keyPair() -> (any MartinOMEMO.SignalIdentityKeyPairProtocol)? {
|
||||||
|
let deviceId = UserSettings.get(omemoDeviceIdFor: credentials.bareJid)
|
||||||
|
guard deviceId != 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let record = try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOIdentity
|
||||||
|
.filter(Column("account") == credentials.bareJid)
|
||||||
|
.filter(Column("name") == credentials.bareJid)
|
||||||
|
.filter(Column("deviceId") == deviceId)
|
||||||
|
.fetchOne(db)
|
||||||
|
}
|
||||||
|
guard let key = record?.key else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return SignalIdentityKeyPair(fromKeyPairData: key)
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func localRegistrationId() -> UInt32 {
|
||||||
|
UserSettings.get(omemoDeviceIdFor: credentials.bareJid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(identity: MartinOMEMO.SignalAddress, key: (any MartinOMEMO.SignalIdentityKeyProtocol)?) -> Bool {
|
||||||
|
guard let key = key as SignalIdentityKeyProtocol?, let publicKey = key.publicKey else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let fingerprint = publicKey.map { byte -> String in
|
||||||
|
String(format: "%02x", byte)
|
||||||
|
}.joined()
|
||||||
|
|
||||||
|
defer {
|
||||||
|
_ = self.setStatus(.verifiedActive, forIdentity: identity)
|
||||||
|
}
|
||||||
|
|
||||||
|
return save(address: identity, fingerprint: fingerprint, own: true, data: key.serialized())
|
||||||
|
}
|
||||||
|
|
||||||
|
func save(identity: MartinOMEMO.SignalAddress, publicKeyData: Data?) -> Bool {
|
||||||
|
guard let publicKeyData = publicKeyData else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let fingerprint = publicKeyData.map { byte -> String in
|
||||||
|
String(format: "%02x", byte)
|
||||||
|
}.joined()
|
||||||
|
|
||||||
|
return save(address: identity, fingerprint: fingerprint, own: false, data: publicKeyData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTrusted(identity _: MartinOMEMO.SignalAddress, key _: (any MartinOMEMO.SignalIdentityKeyProtocol)?) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTrusted(identity _: MartinOMEMO.SignalAddress, publicKeyData _: Data?) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
func setStatus(_ status: MartinOMEMO.IdentityStatus, forIdentity: MartinOMEMO.SignalAddress) -> Bool {
|
||||||
|
if let identity = OMEMOIdentity.getFor(account: credentials.bareJid, name: forIdentity.name, deviceId: forIdentity.deviceId) {
|
||||||
|
return identity.updateStatus(status.rawValue)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setStatus(active: Bool, forIdentity: MartinOMEMO.SignalAddress) -> Bool {
|
||||||
|
if let identity = OMEMOIdentity.getFor(account: credentials.bareJid, name: forIdentity.name, deviceId: forIdentity.deviceId) {
|
||||||
|
let status = IdentityStatus(rawValue: identity.status) ?? .undecidedActive
|
||||||
|
return identity.updateStatus(active ? status.toActive().rawValue : status.toInactive().rawValue)
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func identities(forName name: String) -> [MartinOMEMO.Identity] {
|
||||||
|
OMEMOIdentity.getAllFor(account: credentials.bareJid, name: name)
|
||||||
|
.compactMap { identity in
|
||||||
|
guard let status = IdentityStatus(rawValue: identity.status) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return MartinOMEMO.Identity(
|
||||||
|
address: MartinOMEMO.SignalAddress(name: identity.name, deviceId: Int32(identity.deviceId)),
|
||||||
|
status: status,
|
||||||
|
fingerprint: identity.fingerprint,
|
||||||
|
key: identity.key,
|
||||||
|
own: identity.own
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func identityFingerprint(forAddress address: MartinOMEMO.SignalAddress) -> String? {
|
||||||
|
OMEMOIdentity.getFor(account: credentials.bareJid, name: address.name, deviceId: address.deviceId)?.fingerprint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - PreKey
|
||||||
|
extension ClientMartinOMEMO: SignalPreKeyStoreProtocol {
|
||||||
|
func currentPreKeyId() -> UInt32 {
|
||||||
|
let id = OMEMOPreKey.currentIdFor(account: credentials.bareJid)
|
||||||
|
return UInt32(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadPreKey(withId: UInt32) -> Data? {
|
||||||
|
OMEMOPreKey.keyFor(account: credentials.bareJid, id: withId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func storePreKey(_ data: Data, withId: UInt32) -> Bool {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOPreKey(
|
||||||
|
account: credentials.bareJid,
|
||||||
|
id: Int(withId),
|
||||||
|
key: data,
|
||||||
|
markForDeletion: false
|
||||||
|
)
|
||||||
|
.insert(db)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error pre key store: \(error.localizedDescription)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsPreKey(withId: UInt32) -> Bool {
|
||||||
|
OMEMOPreKey.contains(account: credentials.bareJid, id: withId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deletePreKey(withId: UInt32) -> Bool {
|
||||||
|
OMEMOPreKey.markForDeletion(account: credentials.bareJid, id: withId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func flushDeletedPreKeys() -> Bool {
|
||||||
|
OMEMOPreKey.deleteMarked(account: credentials.bareJid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func preKeysWipe() {
|
||||||
|
OMEMOPreKey.wipe(account: credentials.bareJid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SignedPreKey
|
||||||
|
extension ClientMartinOMEMO: SignalSignedPreKeyStoreProtocol {
|
||||||
|
func countSignedPreKeys() -> Int {
|
||||||
|
OMEMOSignedPreKey.countsFor(account: credentials.bareJid)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSignedPreKey(withId: UInt32) -> Data? {
|
||||||
|
OMEMOSignedPreKey.keyFor(account: credentials.bareJid, id: withId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeSignedPreKey(_ data: Data, withId: UInt32) -> Bool {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOSignedPreKey(
|
||||||
|
account: credentials.bareJid,
|
||||||
|
id: Int(withId),
|
||||||
|
key: data
|
||||||
|
).insert(db)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error storing signed pre key: \(error.localizedDescription)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsSignedPreKey(withId: UInt32) -> Bool {
|
||||||
|
OMEMOSignedPreKey.keyFor(account: credentials.bareJid, id: withId) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteSignedPreKey(withId: UInt32) -> Bool {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOSignedPreKey
|
||||||
|
.filter(Column("account") == credentials.bareJid)
|
||||||
|
.filter(Column("id") == withId)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error deleting signed pre key: \(error.localizedDescription)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wipeSignedPreKeys() {
|
||||||
|
OMEMOSignedPreKey.wipe(account: credentials.bareJid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SenderKey
|
||||||
|
extension ClientMartinOMEMO: SignalSenderKeyStoreProtocol {
|
||||||
|
func storeSenderKey(_: Data, address _: MartinOMEMO.SignalAddress?, groupId _: String?) -> Bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadSenderKey(forAddress _: MartinOMEMO.SignalAddress?, groupId _: String?) -> Data? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
}
|
152
AnotherIM/AppData/Client/Client+MartinRosters.swift
Normal file
152
AnotherIM/AppData/Client/Client+MartinRosters.swift
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
final class ClientMartinRosterManager: Martin.RosterManager {
|
||||||
|
func clear(for context: Martin.Context) {
|
||||||
|
do {
|
||||||
|
try Database.shared.dbQueue.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 Database.shared.dbQueue.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 Database.shared.dbQueue.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 Database.shared.dbQueue.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 Database.shared.dbQueue.read { db in
|
||||||
|
try Roster
|
||||||
|
.filter(Column("bareJid") == context.userBareJid.stringValue)
|
||||||
|
.filter(Column("contactBareJid") == jid.stringValue)
|
||||||
|
.fetchOne(db)
|
||||||
|
}
|
||||||
|
if let roster {
|
||||||
|
_ = try Database.shared.dbQueue.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 Database.shared.dbQueue.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 Database.shared.dbQueue.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) {}
|
||||||
|
}
|
298
AnotherIM/AppData/Client/Client.swift
Normal file
298
AnotherIM/AppData/Client/Client.swift
Normal file
|
@ -0,0 +1,298 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
import MartinOMEMO
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum ClientState: Equatable {
|
||||||
|
enum ClientConnectionState {
|
||||||
|
case connected
|
||||||
|
case disconnected
|
||||||
|
}
|
||||||
|
|
||||||
|
case disabled
|
||||||
|
case enabled(ClientConnectionState)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class Client: ObservableObject {
|
||||||
|
@Published private(set) var state: ClientState = .enabled(.disconnected)
|
||||||
|
@Published private(set) var credentials: Credentials
|
||||||
|
@Published private(set) var rosters: [Roster] = []
|
||||||
|
|
||||||
|
private var connection: XMPPClient
|
||||||
|
private var connectionCancellable: AnyCancellable?
|
||||||
|
private var rostersCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
private var rosterManager = ClientMartinRosterManager()
|
||||||
|
private var chatsManager = ClientMartinChatsManager()
|
||||||
|
private var discoManager: ClientMartinDiscoManager
|
||||||
|
private var messageManager: ClientMartinMessagesManager
|
||||||
|
private var carbonsManager: ClientMartinCarbonsManager
|
||||||
|
private var mamManager: ClientMartinMAM
|
||||||
|
|
||||||
|
init(credentials: Credentials) {
|
||||||
|
self.credentials = credentials
|
||||||
|
state = credentials.isActive ? .enabled(.disconnected) : .disabled
|
||||||
|
connection = Self.prepareConnection(credentials, rosterManager, chatsManager)
|
||||||
|
discoManager = ClientMartinDiscoManager(connection)
|
||||||
|
messageManager = ClientMartinMessagesManager(connection)
|
||||||
|
carbonsManager = ClientMartinCarbonsManager(connection)
|
||||||
|
mamManager = ClientMartinMAM(connection)
|
||||||
|
connectionCancellable = connection.$state
|
||||||
|
.sink { [weak self] state in
|
||||||
|
guard let self = self else { return }
|
||||||
|
guard self.credentials.isActive else {
|
||||||
|
self.state = .disabled
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rostersCancellable = ValueObservation
|
||||||
|
.tracking { db in
|
||||||
|
try Roster
|
||||||
|
.filter(Column("bareJid") == self.credentials.bareJid)
|
||||||
|
.filter(Column("locallyDeleted") == false)
|
||||||
|
.fetchAll(db)
|
||||||
|
}
|
||||||
|
.publisher(in: Database.shared.dbQueue)
|
||||||
|
.catch { _ in Just([]) }
|
||||||
|
.sink { rosters in
|
||||||
|
self.rosters = rosters
|
||||||
|
}
|
||||||
|
switch state {
|
||||||
|
case .connected:
|
||||||
|
self.state = .enabled(.connected)
|
||||||
|
|
||||||
|
default:
|
||||||
|
self.state = .enabled(.disconnected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Client: Identifiable {
|
||||||
|
var id: String {
|
||||||
|
credentials.bareJid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Client {
|
||||||
|
func updActivity(_ isActive: Bool) async {
|
||||||
|
credentials.isActive = isActive
|
||||||
|
Task {
|
||||||
|
try? await credentials.setActive(flag: isActive)
|
||||||
|
if isActive {
|
||||||
|
self.state = .enabled(.disconnected)
|
||||||
|
} else {
|
||||||
|
self.state = .disabled
|
||||||
|
self.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRoster(_ jid: String, name: String?, groups: [String]) async throws {
|
||||||
|
_ = try await connection.module(.roster).addItem(
|
||||||
|
jid: JID(jid),
|
||||||
|
name: name,
|
||||||
|
groups: groups
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRosterLocally(_ jid: String, name: String?, groups: [String]) async throws {
|
||||||
|
try await Roster.addRosterLocally(.init(
|
||||||
|
bareJid: credentials.bareJid,
|
||||||
|
contactBareJid: jid,
|
||||||
|
name: name,
|
||||||
|
subscription: "to",
|
||||||
|
ask: true,
|
||||||
|
data: .init(groups: groups, annotations: []),
|
||||||
|
locallyDeleted: false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteRoster(_ roster: Roster) async throws {
|
||||||
|
_ = try await connection.module(.roster).removeItem(jid: JID(roster.contactBareJid))
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect() async {
|
||||||
|
guard credentials.isActive, state == .enabled(.disconnected) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try? await connection.loginAndWait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect() {
|
||||||
|
_ = connection.disconnect(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Client {
|
||||||
|
func sendMessage(_ message: Message) async throws {
|
||||||
|
guard let to = message.to else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let chat = connection.module(MessageModule.self).chatManager.createChat(for: connection.context, with: BareJID(to)) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = chat.createMessage(text: message.body ?? "??", id: message.id)
|
||||||
|
msg.oob = message.oobUrl
|
||||||
|
if message.secure {
|
||||||
|
msg = try await encryptMessage(msg)
|
||||||
|
}
|
||||||
|
try await chat.send(message: msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func uploadFile(_ localURL: URL, needEncrypt: Bool) async throws -> String {
|
||||||
|
// get data from file
|
||||||
|
guard var data = try? Data(contentsOf: localURL) else {
|
||||||
|
throw AppError.noData
|
||||||
|
}
|
||||||
|
|
||||||
|
// encrypt data if needed
|
||||||
|
var key = Data()
|
||||||
|
var iv = Data()
|
||||||
|
if needEncrypt {
|
||||||
|
key = try AESGSMEngine.generateKey()
|
||||||
|
iv = try AESGSMEngine.generateIV()
|
||||||
|
var encrypted = Data()
|
||||||
|
var tag = Data()
|
||||||
|
guard AESGSMEngine.shared.encrypt(iv: iv, key: key, message: data, output: &encrypted, tag: &tag) else {
|
||||||
|
throw AppError.securityError
|
||||||
|
}
|
||||||
|
|
||||||
|
// attach tag to end of encrypted data
|
||||||
|
encrypted.append(tag)
|
||||||
|
data = encrypted
|
||||||
|
}
|
||||||
|
|
||||||
|
// upload
|
||||||
|
let httpModule = connection.module(HttpFileUploadModule.self)
|
||||||
|
let components = try await httpModule.findHttpUploadComponents()
|
||||||
|
guard let component = components.first(where: { $0.maxSize > data.count }) else {
|
||||||
|
throw AppError.fileTooBig
|
||||||
|
}
|
||||||
|
|
||||||
|
let slot = try await httpModule.requestUploadSlot(
|
||||||
|
componentJid: component.jid,
|
||||||
|
filename: localURL.lastPathComponent,
|
||||||
|
size: data.count,
|
||||||
|
contentType: localURL.mimeType
|
||||||
|
)
|
||||||
|
var request = URLRequest(url: slot.putUri)
|
||||||
|
for (key, value) in slot.putHeaders {
|
||||||
|
request.addValue(value, forHTTPHeaderField: key)
|
||||||
|
}
|
||||||
|
request.httpMethod = "PUT"
|
||||||
|
request.httpBody = data
|
||||||
|
request.addValue(String(data.count), forHTTPHeaderField: "Content-Length")
|
||||||
|
request.addValue(localURL.mimeType, forHTTPHeaderField: "Content-Type")
|
||||||
|
let (_, response) = try await URLSession.shared.data(for: request)
|
||||||
|
switch response {
|
||||||
|
case let httpResponse as HTTPURLResponse where httpResponse.statusCode == 201:
|
||||||
|
if needEncrypt {
|
||||||
|
guard var parts = URLComponents(url: slot.getUri, resolvingAgainstBaseURL: true) else {
|
||||||
|
throw URLError(.badServerResponse)
|
||||||
|
}
|
||||||
|
parts.scheme = "aesgcm"
|
||||||
|
parts.fragment = (iv + key).map { String(format: "%02x", $0) }.joined()
|
||||||
|
guard let shareUrl = parts.url else {
|
||||||
|
throw URLError(.badServerResponse)
|
||||||
|
}
|
||||||
|
return shareUrl.absoluteString
|
||||||
|
} else {
|
||||||
|
return slot.getUri.absoluteString
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw URLError(.badServerResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchArchiveMessages(for roster: Roster, query: RSM.Query) async throws -> Martin.MessageArchiveManagementModule.QueryResult {
|
||||||
|
let module = connection.module(MessageArchiveManagementModule.self)
|
||||||
|
return try await module.queryItems(componentJid: JID(roster.bareJid), with: JID(roster.contactBareJid), queryId: UUID().uuidString, rsm: query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Client {
|
||||||
|
func encryptMessage(_ message: Martin.Message) async throws -> Martin.Message {
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
connection.module(.omemo).encode(message: message, completionHandler: { result in
|
||||||
|
switch result {
|
||||||
|
case .successMessage(let encodedMessage, _):
|
||||||
|
// guard connection.isConnected else {
|
||||||
|
// continuation.resume(returning: message)
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
continuation.resume(returning: encodedMessage)
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
var errorMessage = NSLocalizedString("It was not possible to send encrypted message due to encryption error", comment: "message encryption failure")
|
||||||
|
switch error {
|
||||||
|
case .noSession:
|
||||||
|
errorMessage = NSLocalizedString("There is no trusted device to send message to", comment: "message encryption failure")
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
continuation.resume(throwing: XMPPError.unexpected_request(errorMessage))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Client {
|
||||||
|
static func tryLogin(with credentials: Credentials) async throws -> Client {
|
||||||
|
let client = Client(credentials: credentials)
|
||||||
|
try await client.connection.loginAndWait()
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension Client {
|
||||||
|
static func prepareConnection(_ credentials: Credentials, _ roster: RosterManager, _ chat: ChatManager) -> XMPPClient {
|
||||||
|
let client = XMPPClient()
|
||||||
|
client.connectionConfiguration.resource = UIDevice.current.name
|
||||||
|
|
||||||
|
// register modules
|
||||||
|
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)))
|
||||||
|
|
||||||
|
client.modulesManager.register(RosterModule(rosterManager: roster))
|
||||||
|
|
||||||
|
client.modulesManager.register(PubSubModule())
|
||||||
|
client.modulesManager.register(PEPUserAvatarModule())
|
||||||
|
client.modulesManager.register(PEPBookmarksModule())
|
||||||
|
|
||||||
|
client.modulesManager.register(MessageModule(chatManager: chat))
|
||||||
|
client.modulesManager.register(MessageArchiveManagementModule())
|
||||||
|
client.modulesManager.register(MessageCarbonsModule())
|
||||||
|
// client.modulesManager.register(MessageDeliveryReceiptsModule()).sendReceived = false
|
||||||
|
|
||||||
|
client.modulesManager.register(HttpFileUploadModule())
|
||||||
|
|
||||||
|
client.modulesManager.register(PresenceModule())
|
||||||
|
client.modulesManager.register(SoftwareVersionModule())
|
||||||
|
client.modulesManager.register(PingModule())
|
||||||
|
client.connectionConfiguration.userJid = .init(credentials.bareJid)
|
||||||
|
client.connectionConfiguration.credentials = .password(password: credentials.pass)
|
||||||
|
|
||||||
|
// OMEMO
|
||||||
|
let omemoManager = ClientMartinOMEMO(credentials)
|
||||||
|
let (signalStorage, signalContext) = omemoManager.signal
|
||||||
|
client.modulesManager.register(OMEMOModule(aesGCMEngine: AESGSMEngine.shared, signalContext: signalContext, signalStorage: signalStorage))
|
||||||
|
|
||||||
|
// group chats
|
||||||
|
// client.modulesManager.register(MucModule(roomManager: manager))
|
||||||
|
|
||||||
|
// channels
|
||||||
|
// client.modulesManager.register(MixModule(channelManager: manager))
|
||||||
|
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
}
|
43
AnotherIM/AppData/Model/Chat.swift
Normal file
43
AnotherIM/AppData/Model/Chat.swift
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
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
|
||||||
|
var encrypted: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Chat: Equatable {}
|
||||||
|
|
||||||
|
extension Chat {
|
||||||
|
func fetchRoster() async throws -> Roster {
|
||||||
|
try await Database.shared.dbQueue.read { db in
|
||||||
|
guard
|
||||||
|
let roster = try Roster
|
||||||
|
.filter(Column("bareJid") == account && Column("contactBareJid") == participant)
|
||||||
|
.fetchOne(db)
|
||||||
|
else {
|
||||||
|
throw AppError.rosterNotFound
|
||||||
|
}
|
||||||
|
return roster
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setEncrypted(_ encrypted: Bool) async throws {
|
||||||
|
try await Database.shared.dbQueue.write { db in
|
||||||
|
var chat = self
|
||||||
|
chat.encrypted = encrypted
|
||||||
|
try chat.update(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
41
AnotherIM/AppData/Model/Credentials.swift
Normal file
41
AnotherIM/AppData/Model/Credentials.swift
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct Credentials: DBStorable, Hashable {
|
||||||
|
static let databaseTableName = "credentials"
|
||||||
|
|
||||||
|
var id: String { bareJid }
|
||||||
|
var bareJid: String
|
||||||
|
var pass: String
|
||||||
|
var isActive: Bool
|
||||||
|
|
||||||
|
func save() async throws {
|
||||||
|
let db = Database.shared.dbQueue
|
||||||
|
try await db.write { db in
|
||||||
|
try self.save(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func delete() async throws {
|
||||||
|
let db = Database.shared.dbQueue
|
||||||
|
_ = try await db.write { db in
|
||||||
|
try self.delete(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setActive(flag: Bool) async throws {
|
||||||
|
let db = Database.shared.dbQueue
|
||||||
|
_ = try await db.write { db in
|
||||||
|
var updated = self
|
||||||
|
updated.isActive = flag
|
||||||
|
try updated.save(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Credentials: UniversalInputSelectionElement {
|
||||||
|
var text: String? { bareJid }
|
||||||
|
var icon: Image? { nil }
|
||||||
|
}
|
53
AnotherIM/AppData/Model/GalleryItem.swift
Normal file
53
AnotherIM/AppData/Model/GalleryItem.swift
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import Photos
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum GalleryMediaType {
|
||||||
|
case video
|
||||||
|
case photo
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GalleryItem: Identifiable {
|
||||||
|
let id: String
|
||||||
|
let type: GalleryMediaType
|
||||||
|
var thumbnail: Image?
|
||||||
|
var duration: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GalleryItem {
|
||||||
|
static func fetchAll() async -> [GalleryItem] {
|
||||||
|
await Task {
|
||||||
|
let fetchOptions = PHFetchOptions()
|
||||||
|
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
||||||
|
let assets = PHAsset.fetchAssets(with: fetchOptions)
|
||||||
|
var tmpGalleryItems: [GalleryItem] = []
|
||||||
|
assets.enumerateObjects { asset, _, _ in
|
||||||
|
if asset.mediaType == .image {
|
||||||
|
let item = GalleryItem(id: asset.localIdentifier, type: .photo, thumbnail: nil, duration: nil)
|
||||||
|
tmpGalleryItems.append(item)
|
||||||
|
}
|
||||||
|
if asset.mediaType == .video {
|
||||||
|
let item = GalleryItem(id: asset.localIdentifier, type: .video, thumbnail: nil, duration: asset.duration.minAndSec)
|
||||||
|
tmpGalleryItems.append(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return tmpGalleryItems
|
||||||
|
}.value
|
||||||
|
}
|
||||||
|
|
||||||
|
mutating func fetchThumbnail() async throws {
|
||||||
|
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [id], options: nil).firstObject else { return }
|
||||||
|
let size = CGSize(width: Const.galleryGridSize, height: Const.galleryGridSize)
|
||||||
|
|
||||||
|
switch type {
|
||||||
|
case .photo:
|
||||||
|
let originalImage = try await PHImageManager.default().getPhoto(for: asset)
|
||||||
|
let cropped = try await originalImage.scaleAndCropImage(size)
|
||||||
|
thumbnail = Image(uiImage: cropped)
|
||||||
|
|
||||||
|
case .video:
|
||||||
|
let avAsset = try await PHImageManager.default().getVideo(for: asset)
|
||||||
|
let cropped = try await avAsset.generateVideoThumbnail(size)
|
||||||
|
thumbnail = Image(uiImage: cropped)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
87
AnotherIM/AppData/Model/Message+OMEMO.swift
Normal file
87
AnotherIM/AppData/Model/Message+OMEMO.swift
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
extension Message {
|
||||||
|
static func map(_ martinMessage: Martin.Message, context: Martin.Context?) -> Message? {
|
||||||
|
// Check that the message type is supported
|
||||||
|
var martinMessage = martinMessage
|
||||||
|
let chatTypes: [StanzaType] = [.chat, .groupchat]
|
||||||
|
guard let mType = martinMessage.type, chatTypes.contains(mType) else {
|
||||||
|
#if DEBUG
|
||||||
|
print("Unsupported martinMessage type: \(martinMessage.type?.rawValue ?? "nil")")
|
||||||
|
#endif
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type
|
||||||
|
let type = MessageType(rawValue: martinMessage.type?.rawValue ?? "") ?? .chat
|
||||||
|
|
||||||
|
// Content type
|
||||||
|
var contentType: MessageContentType = .text
|
||||||
|
if let oob = martinMessage.oob {
|
||||||
|
contentType = .attachment(.init(
|
||||||
|
type: oob.attachmentType,
|
||||||
|
localName: nil,
|
||||||
|
thumbnailName: nil,
|
||||||
|
remotePath: oob
|
||||||
|
))
|
||||||
|
} else if martinMessage.hints.contains(.noStore) {
|
||||||
|
contentType = .typing
|
||||||
|
// skip for now
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to recognize if message is omemo-encoded and decode it
|
||||||
|
var secure = false
|
||||||
|
if let omemo = context?.module(.omemo) {
|
||||||
|
let decodingResult = omemo.decode(message: martinMessage)
|
||||||
|
switch decodingResult {
|
||||||
|
case .successMessage(let decodedMessage, _):
|
||||||
|
martinMessage = decodedMessage
|
||||||
|
if let oob = martinMessage.oob {
|
||||||
|
contentType = .attachment(.init(
|
||||||
|
type: oob.attachmentType,
|
||||||
|
localName: nil,
|
||||||
|
thumbnailName: nil,
|
||||||
|
remotePath: oob
|
||||||
|
))
|
||||||
|
}
|
||||||
|
secure = true
|
||||||
|
|
||||||
|
case .successTransportKey:
|
||||||
|
return nil
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
logIt(.error, "Error decoding omemo message: \(error)")
|
||||||
|
logIt(.error, "Message: \(martinMessage)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip for non-visible messages
|
||||||
|
if martinMessage.body == nil, martinMessage.oob == nil, martinMessage.type == .chat {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// From/To
|
||||||
|
let from = martinMessage.from?.bareJid.stringValue ?? ""
|
||||||
|
let to = martinMessage.to?.bareJid.stringValue
|
||||||
|
|
||||||
|
// Msg
|
||||||
|
let msg = Message(
|
||||||
|
id: martinMessage.id ?? UUID().uuidString,
|
||||||
|
type: type,
|
||||||
|
date: Date(),
|
||||||
|
contentType: contentType,
|
||||||
|
status: .sent,
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
body: martinMessage.body,
|
||||||
|
subject: martinMessage.subject,
|
||||||
|
thread: martinMessage.thread,
|
||||||
|
oobUrl: martinMessage.oob,
|
||||||
|
secure: secure
|
||||||
|
)
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
}
|
106
AnotherIM/AppData/Model/Message.swift
Normal file
106
AnotherIM/AppData/Model/Message.swift
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
enum MessageType: String, Codable, DatabaseValueConvertible {
|
||||||
|
case chat
|
||||||
|
case groupchat
|
||||||
|
case error
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AttachmentType: Int, Codable, DatabaseValueConvertible {
|
||||||
|
case image
|
||||||
|
case video
|
||||||
|
case audio
|
||||||
|
case file
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Attachment: Codable & Equatable, DatabaseValueConvertible {
|
||||||
|
let type: AttachmentType
|
||||||
|
var localName: String?
|
||||||
|
var thumbnailName: String?
|
||||||
|
var remotePath: String?
|
||||||
|
|
||||||
|
var localPath: URL? {
|
||||||
|
guard let attachmentLocalName = localName else { return nil }
|
||||||
|
return FolderWrapper.shared.fileFolder.appendingPathComponent(attachmentLocalName)
|
||||||
|
}
|
||||||
|
|
||||||
|
var thumbnailPath: URL? {
|
||||||
|
guard let attachmentThumbnailName = thumbnailName else { return nil }
|
||||||
|
return FolderWrapper.shared.fileFolder.appendingPathComponent(attachmentThumbnailName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MessageContentType: Codable & Equatable, DatabaseValueConvertible {
|
||||||
|
case text
|
||||||
|
case typing
|
||||||
|
case invite
|
||||||
|
case attachment(Attachment)
|
||||||
|
|
||||||
|
var isAttachment: Bool {
|
||||||
|
if case .attachment = self {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MessageStatus: Int, Codable, DatabaseValueConvertible {
|
||||||
|
case pending
|
||||||
|
case sent
|
||||||
|
case error
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Message: DBStorable, Equatable {
|
||||||
|
static let databaseTableName = "messages"
|
||||||
|
|
||||||
|
let id: String
|
||||||
|
var type: MessageType
|
||||||
|
var date: Date
|
||||||
|
var contentType: MessageContentType
|
||||||
|
var status: MessageStatus
|
||||||
|
|
||||||
|
var from: String
|
||||||
|
var to: String?
|
||||||
|
|
||||||
|
var body: String?
|
||||||
|
var subject: String?
|
||||||
|
var thread: String?
|
||||||
|
var oobUrl: String?
|
||||||
|
|
||||||
|
var secure: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Message {
|
||||||
|
func save() async throws {
|
||||||
|
try await Database.shared.dbQueue.write { db in
|
||||||
|
try self.insert(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setStatus(_ status: MessageStatus) async throws {
|
||||||
|
try await Database.shared.dbQueue.write { db in
|
||||||
|
var updatedMessage = self
|
||||||
|
updatedMessage.status = status
|
||||||
|
try updatedMessage.update(db, columns: ["status"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var blank: Message {
|
||||||
|
Message(
|
||||||
|
id: UUID().uuidString,
|
||||||
|
type: .chat,
|
||||||
|
date: Date(),
|
||||||
|
contentType: .text,
|
||||||
|
status: .pending,
|
||||||
|
from: "",
|
||||||
|
to: nil,
|
||||||
|
body: nil,
|
||||||
|
subject: nil,
|
||||||
|
thread: nil,
|
||||||
|
oobUrl: nil,
|
||||||
|
secure: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
314
AnotherIM/AppData/Model/OMEMO.swift
Normal file
314
AnotherIM/AppData/Model/OMEMO.swift
Normal file
|
@ -0,0 +1,314 @@
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
// MARK: - Session
|
||||||
|
struct OMEMOSession: DBStorable {
|
||||||
|
static let databaseTableName = "omemo_sessions"
|
||||||
|
|
||||||
|
let account: String
|
||||||
|
let name: String
|
||||||
|
let deviceId: Int
|
||||||
|
let key: String
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
"\(account)_\(name)_\(deviceId)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OMEMOSession {
|
||||||
|
static func keyFor(account: String, name: String, deviceId: Int32) -> String? {
|
||||||
|
do {
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOSession
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("name") == name)
|
||||||
|
.filter(Column("deviceId") == deviceId)
|
||||||
|
.fetchOne(db)
|
||||||
|
}?.key
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func devicesIdsFor(account: String, name: String) -> [Int32] {
|
||||||
|
do {
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOSession
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("name") == name)
|
||||||
|
.fetchAll(db)
|
||||||
|
.map(\.deviceId)
|
||||||
|
}.map { Int32($0) }
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func trustedDevicesIdsFor(account: String, name: String) -> [Int32] {
|
||||||
|
do {
|
||||||
|
let sql =
|
||||||
|
"""
|
||||||
|
SELECT s.deviceId
|
||||||
|
FROM omemo_sessions s
|
||||||
|
LEFT JOIN omemo_identities i
|
||||||
|
ON s.account = i.account
|
||||||
|
AND s.name = i.name
|
||||||
|
AND s.deviceId = i.deviceId
|
||||||
|
WHERE s.account = :account
|
||||||
|
AND s.name = :name
|
||||||
|
AND ((i.status >= 0 AND i.status % 2 = 0) OR i.status IS NULL)
|
||||||
|
"""
|
||||||
|
|
||||||
|
let arguments: StatementArguments = ["account": account, "name": name]
|
||||||
|
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try Int32.fetchAll(db, sql: sql, arguments: arguments)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func wipe(account: String) {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOSession
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Failed to wipe OMEMO session: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Identity
|
||||||
|
struct OMEMOIdentity: DBStorable {
|
||||||
|
static let databaseTableName = "omemo_identities"
|
||||||
|
|
||||||
|
let account: String
|
||||||
|
let name: String
|
||||||
|
let deviceId: Int
|
||||||
|
let fingerprint: String
|
||||||
|
let key: Data
|
||||||
|
let own: Bool
|
||||||
|
let status: Int
|
||||||
|
|
||||||
|
var id: String {
|
||||||
|
"\(account)_\(name)_\(deviceId)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OMEMOIdentity {
|
||||||
|
static func wipe(account: String) {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOIdentity
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Failed to wipe OMEMO identity: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getFor(account: String, name: String, deviceId: Int32) -> OMEMOIdentity? {
|
||||||
|
do {
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOIdentity
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("name") == name)
|
||||||
|
.filter(Column("deviceId") == deviceId)
|
||||||
|
.fetchOne(db)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func existsFor(account: String, name: String, fingerprint: String) -> Bool {
|
||||||
|
do {
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOIdentity
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("name") == name)
|
||||||
|
.filter(Column("fingerprint") == fingerprint)
|
||||||
|
.fetchOne(db) != nil
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateStatus(_ status: Int) -> Bool {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOIdentity
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("name") == name)
|
||||||
|
.filter(Column("deviceId") == deviceId)
|
||||||
|
.updateAll(db, Column("status").set(to: status))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Failed to update OMEMO identity status: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getAllFor(account: String, name: String) -> [OMEMOIdentity] {
|
||||||
|
do {
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOIdentity
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("name") == name)
|
||||||
|
.fetchAll(db)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - PreKey
|
||||||
|
struct OMEMOPreKey: DBStorable {
|
||||||
|
static let databaseTableName = "omemo_pre_keys"
|
||||||
|
|
||||||
|
let account: String
|
||||||
|
let id: Int
|
||||||
|
let key: Data
|
||||||
|
let markForDeletion: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OMEMOPreKey {
|
||||||
|
static func wipe(account: String) {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOPreKey
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Failed to wipe OMEMO pre key: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func currentIdFor(account: String) -> Int {
|
||||||
|
do {
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOPreKey
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.order(Column("id").desc)
|
||||||
|
.fetchOne(db)
|
||||||
|
.map(\.id)
|
||||||
|
} ?? 0
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func keyFor(account: String, id: UInt32) -> Data? {
|
||||||
|
do {
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOPreKey
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("id") == id)
|
||||||
|
.fetchOne(db)
|
||||||
|
}?.key
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func contains(account: String, id: UInt32) -> Bool {
|
||||||
|
do {
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOPreKey
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("id") == id)
|
||||||
|
.fetchOne(db) != nil
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func markForDeletion(account: String, id: UInt32) -> Bool {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOPreKey
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("id") == id)
|
||||||
|
.updateAll(db, Column("markForDeletion").set(to: true))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Failed to mark OMEMO pre key for deletion: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func deleteMarked(account: String) -> Bool {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOPreKey
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("markForDeletion") == true)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Failed to delete marked OMEMO pre keys: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - SignedPreKey
|
||||||
|
struct OMEMOSignedPreKey: DBStorable {
|
||||||
|
static let databaseTableName = "omemo_signed_pre_keys"
|
||||||
|
|
||||||
|
let account: String
|
||||||
|
let id: Int
|
||||||
|
let key: Data
|
||||||
|
}
|
||||||
|
|
||||||
|
extension OMEMOSignedPreKey {
|
||||||
|
static func wipe(account: String) {
|
||||||
|
do {
|
||||||
|
_ = try Database.shared.dbQueue.write { db in
|
||||||
|
try OMEMOSignedPreKey
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Failed to wipe OMEMO signed pre key: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func countsFor(account: String) -> Int {
|
||||||
|
do {
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOSignedPreKey
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.fetchCount(db)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func keyFor(account: String, id: UInt32) -> Data? {
|
||||||
|
do {
|
||||||
|
return try Database.shared.dbQueue.read { db in
|
||||||
|
try OMEMOSignedPreKey
|
||||||
|
.filter(Column("account") == account)
|
||||||
|
.filter(Column("id") == id)
|
||||||
|
.fetchOne(db)
|
||||||
|
}?.key
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
AnotherIM/AppData/Model/Roster.swift
Normal file
110
AnotherIM/AppData/Model/Roster.swift
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Roster {
|
||||||
|
mutating func setLocallyDeleted(_ value: Bool) async throws {
|
||||||
|
locallyDeleted = value
|
||||||
|
let copy = self
|
||||||
|
try? await Database.shared.dbQueue.write { db in
|
||||||
|
try copy.save(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Roster {
|
||||||
|
static var allDeletedLocally: [Roster] {
|
||||||
|
get async {
|
||||||
|
do {
|
||||||
|
let rosters = try await Database.shared.dbQueue.read { db in
|
||||||
|
try Roster
|
||||||
|
.filter(Column("locallyDeleted") == true)
|
||||||
|
.fetchAll(db)
|
||||||
|
}
|
||||||
|
return rosters
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allActive: [Roster] {
|
||||||
|
get async {
|
||||||
|
do {
|
||||||
|
let rosters = try await Database.shared.dbQueue.read { db in
|
||||||
|
try Roster
|
||||||
|
.filter(Column("locallyDeleted") == false)
|
||||||
|
.fetchAll(db)
|
||||||
|
}
|
||||||
|
return rosters
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func addRosterLocally(_ roster: Roster) async throws {
|
||||||
|
try await Database.shared.dbQueue.write { db in
|
||||||
|
try roster.save(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
AnotherIM/AppData/Model/ServerFeature.swift
Normal file
21
AnotherIM/AppData/Model/ServerFeature.swift
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct ServerFeature: Identifiable & Codable {
|
||||||
|
let xep: String
|
||||||
|
let name: String
|
||||||
|
let xmppId: String?
|
||||||
|
let description: String?
|
||||||
|
|
||||||
|
var id: String { xep }
|
||||||
|
|
||||||
|
static var allFeatures: [ServerFeature] {
|
||||||
|
guard
|
||||||
|
let url = Bundle.main.url(forResource: "server_features", withExtension: "plist"),
|
||||||
|
let data = try? Data(contentsOf: url),
|
||||||
|
let loaded = try? PropertyListDecoder().decode([ServerFeature].self, from: data)
|
||||||
|
else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return loaded
|
||||||
|
}
|
||||||
|
}
|
65
AnotherIM/AppData/Services/AESGSMEngine.swift
Normal file
65
AnotherIM/AppData/Services/AESGSMEngine.swift
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import CryptoKit
|
||||||
|
import Foundation
|
||||||
|
import MartinOMEMO
|
||||||
|
|
||||||
|
final class AESGSMEngine: AES_GCM_Engine {
|
||||||
|
static let shared = AESGSMEngine()
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func encrypt(iv: Data, key: Data, message: Data, output: UnsafeMutablePointer<Data>?, tag: UnsafeMutablePointer<Data>?) -> Bool {
|
||||||
|
do {
|
||||||
|
let symmetricKey = SymmetricKey(data: key)
|
||||||
|
let sealedBox = try AES.GCM.seal(message, using: symmetricKey, nonce: AES.GCM.Nonce(data: iv))
|
||||||
|
|
||||||
|
if let output = output {
|
||||||
|
output.pointee = sealedBox.ciphertext
|
||||||
|
}
|
||||||
|
if let tag = tag {
|
||||||
|
tag.pointee = sealedBox.tag
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
print("Encryption error: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func decrypt(iv: Data, key: Data, encoded: Data, auth tag: Data?, output: UnsafeMutablePointer<Data>?) -> Bool {
|
||||||
|
do {
|
||||||
|
let symmetricKey = SymmetricKey(data: key)
|
||||||
|
|
||||||
|
let sealedBox: AES.GCM.SealedBox
|
||||||
|
if let tag {
|
||||||
|
sealedBox = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: iv), ciphertext: encoded, tag: tag)
|
||||||
|
} else {
|
||||||
|
let embeddedTag = encoded.subdata(in: (encoded.count - 16) ..< encoded.count)
|
||||||
|
let payload = encoded.subdata(in: 0 ..< (encoded.count - 16))
|
||||||
|
sealedBox = try AES.GCM.SealedBox(nonce: AES.GCM.Nonce(data: iv), ciphertext: payload, tag: embeddedTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
let decryptedData = try AES.GCM.open(sealedBox, using: symmetricKey)
|
||||||
|
if let output = output {
|
||||||
|
output.pointee = decryptedData
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
print("Decryption error: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func generateIV() throws -> Data {
|
||||||
|
var bytes = [Int8](repeating: 0, count: 12)
|
||||||
|
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)
|
||||||
|
if status != errSecSuccess {
|
||||||
|
throw AppError.securityError
|
||||||
|
}
|
||||||
|
return Data(bytes: bytes, count: bytes.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func generateKey() throws -> Data {
|
||||||
|
let key = SymmetricKey(size: .bits256)
|
||||||
|
return key.withUnsafeBytes { Data($0) }
|
||||||
|
}
|
||||||
|
}
|
117
AnotherIM/AppData/Services/Database+Migrations.swift
Normal file
117
AnotherIM/AppData/Services/Database+Migrations.swift
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
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
|
||||||
|
// credentials
|
||||||
|
try db.create(table: "credentials", 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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("type", .text).notNull()
|
||||||
|
table.column("date", .datetime).notNull()
|
||||||
|
table.column("contentType", .text).notNull()
|
||||||
|
table.column("status", .integer).notNull()
|
||||||
|
table.column("from", .text).notNull()
|
||||||
|
table.column("to", .text)
|
||||||
|
table.column("body", .text)
|
||||||
|
table.column("subject", .text)
|
||||||
|
table.column("thread", .text)
|
||||||
|
table.column("oobUrl", .text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
migrator.registerMigration("Add OMEMO tables") { db in
|
||||||
|
try db.create(table: "omemo_sessions", options: [.ifNotExists]) { table in
|
||||||
|
table.column("account", .text).notNull()
|
||||||
|
table.column("name", .text).notNull()
|
||||||
|
table.column("deviceId", .integer).notNull()
|
||||||
|
table.column("key", .text).notNull()
|
||||||
|
table.primaryKey(["account", "name", "deviceId"], onConflict: .replace)
|
||||||
|
}
|
||||||
|
|
||||||
|
try db.create(table: "omemo_identities", options: [.ifNotExists]) { table in
|
||||||
|
table.column("account", .text).notNull().collate(.nocase)
|
||||||
|
table.column("name", .text).notNull()
|
||||||
|
table.column("deviceId", .integer).notNull()
|
||||||
|
table.column("fingerprint", .text).notNull()
|
||||||
|
table.column("key", .blob).notNull()
|
||||||
|
table.column("own", .integer).notNull()
|
||||||
|
table.column("status", .integer).notNull()
|
||||||
|
table.uniqueKey(["account", "name", "fingerprint"], onConflict: .ignore)
|
||||||
|
}
|
||||||
|
|
||||||
|
try db.create(table: "omemo_pre_keys", options: [.ifNotExists]) { table in
|
||||||
|
table.column("account", .text).notNull()
|
||||||
|
table.column("id", .integer).notNull()
|
||||||
|
table.column("key", .blob).notNull()
|
||||||
|
table.column("markForDeletion", .boolean).notNull().defaults(to: false)
|
||||||
|
table.primaryKey(["account", "id"], onConflict: .replace)
|
||||||
|
}
|
||||||
|
|
||||||
|
try db.create(table: "omemo_signed_pre_keys", options: [.ifNotExists]) { table in
|
||||||
|
table.column("account", .text).notNull()
|
||||||
|
table.column("id", .integer).notNull()
|
||||||
|
table.column("key", .blob).notNull()
|
||||||
|
table.primaryKey(["account", "id"], onConflict: .replace)
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try db.alter(table: "messages") { table in
|
||||||
|
table.add(column: "secure", .boolean).notNull().defaults(to: false)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error adding columns: \(error)\nProbably already added")
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try db.alter(table: "chats") { table in
|
||||||
|
table.add(column: "encrypted", .boolean).notNull().defaults(to: false)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error adding columns: \(error)\nProbably already added")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return migrator
|
||||||
|
return migrator
|
||||||
|
}()
|
||||||
|
}
|
80
AnotherIM/AppData/Services/Database.swift
Normal file
80
AnotherIM/AppData/Services/Database.swift
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
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 dbQueue: DatabaseQueue
|
||||||
|
private var dbPath: String
|
||||||
|
|
||||||
|
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("anotherim", isDirectory: true)
|
||||||
|
try fileManager.createDirectory(at: directoryURL, withIntermediateDirectories: true)
|
||||||
|
|
||||||
|
// Open or create the database
|
||||||
|
let databaseURL = directoryURL.appendingPathComponent("db.sqlite")
|
||||||
|
dbQueue = try DatabaseQueue(path: databaseURL.path, configuration: Database.config)
|
||||||
|
dbPath = databaseURL.path
|
||||||
|
|
||||||
|
// Some debug info
|
||||||
|
#if DEBUG
|
||||||
|
print("Database path: \(databaseURL.path)")
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Apply migrations
|
||||||
|
try Database.migrator.migrate(dbQueue)
|
||||||
|
} 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)\n") }
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return config
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - flush all data for debug
|
||||||
|
#if DEBUG
|
||||||
|
extension Database {
|
||||||
|
func flushAllData() {
|
||||||
|
do {
|
||||||
|
try dbQueue.write { db in
|
||||||
|
// Fetch all table names
|
||||||
|
let tables = try String.fetchAll(db, sql: """
|
||||||
|
SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%';
|
||||||
|
""")
|
||||||
|
|
||||||
|
// Generate and execute DELETE statements for each table
|
||||||
|
for table in tables {
|
||||||
|
try db.execute(sql: "DELETE FROM \(table);")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
print("Error flushing all data: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
42
AnotherIM/AppData/Services/Logger.swift
Normal file
42
AnotherIM/AppData/Services/Logger.swift
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
let isConsoleLoggingEnabled = false
|
||||||
|
|
||||||
|
enum LogLevels: String {
|
||||||
|
case info = "\u{F449}"
|
||||||
|
case warning = "\u{F071}"
|
||||||
|
case error = "\u{EA76}"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// For thread debugging
|
||||||
|
func ptInfo(_ message: String) {
|
||||||
|
#if DEBUG
|
||||||
|
let timeStr = dateFormatter.string(from: Date())
|
||||||
|
let str = "\(timeStr) \(message) -> \(Thread.current), \(String(validatingUTF8: __dispatch_queue_get_label(nil)) ?? "no queue label")"
|
||||||
|
print(str)
|
||||||
|
if isConsoleLoggingEnabled {
|
||||||
|
NSLog(str)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
37
AnotherIM/AppData/Services/NetworkMonitor.swift
Normal file
37
AnotherIM/AppData/Services/NetworkMonitor.swift
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import Combine
|
||||||
|
import Network
|
||||||
|
|
||||||
|
extension NWPathMonitor {
|
||||||
|
func paths() -> AsyncStream<NWPath> {
|
||||||
|
AsyncStream { continuation in
|
||||||
|
pathUpdateHandler = { path in
|
||||||
|
continuation.yield(path)
|
||||||
|
}
|
||||||
|
continuation.onTermination = { [weak self] _ in
|
||||||
|
self?.cancel()
|
||||||
|
}
|
||||||
|
start(queue: DispatchQueue(label: "NSPathMonitor.paths"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final actor NetworkMonitor: ObservableObject {
|
||||||
|
static let shared = NetworkMonitor()
|
||||||
|
|
||||||
|
@Published private(set) var isOnline: Bool = false
|
||||||
|
|
||||||
|
private let monitor = NWPathMonitor()
|
||||||
|
|
||||||
|
init() {
|
||||||
|
Task(priority: .background) {
|
||||||
|
await startMonitoring()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startMonitoring() async {
|
||||||
|
let monitor = NWPathMonitor()
|
||||||
|
for await path in monitor.paths() {
|
||||||
|
isOnline = path.status == .satisfied
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
AnotherIM/AppData/Services/PersistUserSettings.swift
Normal file
61
AnotherIM/AppData/Services/PersistUserSettings.swift
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
// Wrapper
|
||||||
|
@propertyWrapper
|
||||||
|
private struct Storage<T> {
|
||||||
|
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)
|
||||||
|
UserDefaults.standard.synchronize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage
|
||||||
|
private let kBase = "conversations.classic.user.defaults"
|
||||||
|
private let kOmemoDevicesIds = "\(kBase).omemoDevicesIds"
|
||||||
|
private let kSecureChatsByDefault = "\(kBase).secureChatsByDefault"
|
||||||
|
|
||||||
|
enum UserSettings {
|
||||||
|
@Storage(key: kOmemoDevicesIds, defaultValue: [:])
|
||||||
|
private static var omemoDevicesIds: [String: UInt32]
|
||||||
|
|
||||||
|
@Storage(key: kSecureChatsByDefault, defaultValue: true)
|
||||||
|
private static var vSecureChatsByDefault: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Public
|
||||||
|
extension UserSettings {
|
||||||
|
static func reset() {
|
||||||
|
omemoDevicesIds = [:]
|
||||||
|
vSecureChatsByDefault = true
|
||||||
|
}
|
||||||
|
|
||||||
|
static func set(omemoDeviceId: UInt32, for account: String) {
|
||||||
|
var dict = UserSettings.omemoDevicesIds
|
||||||
|
dict[account] = omemoDeviceId
|
||||||
|
UserSettings.omemoDevicesIds = dict
|
||||||
|
}
|
||||||
|
|
||||||
|
static func get(omemoDeviceIdFor account: String) -> UInt32 {
|
||||||
|
UserSettings.omemoDevicesIds[account] ?? 0
|
||||||
|
}
|
||||||
|
|
||||||
|
static var secureChatsByDefault: Bool {
|
||||||
|
get { UserSettings.vSecureChatsByDefault }
|
||||||
|
set { UserSettings.vSecureChatsByDefault = newValue }
|
||||||
|
}
|
||||||
|
}
|
412
AnotherIM/AppData/Store/AttachmentsStore.swift
Normal file
412
AnotherIM/AppData/Store/AttachmentsStore.swift
Normal file
|
@ -0,0 +1,412 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Photos
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class AttachmentsStore: ObservableObject {
|
||||||
|
@Published private(set) var cameraAccessGranted = false
|
||||||
|
@Published private(set) var galleryAccessGranted = false
|
||||||
|
@Published private(set) var galleryItems: [GalleryItem] = []
|
||||||
|
|
||||||
|
private let client: Client
|
||||||
|
private let roster: Roster
|
||||||
|
private var secured: Bool = false
|
||||||
|
|
||||||
|
private var messagesCancellable: AnyCancellable?
|
||||||
|
private var chatCancellable: AnyCancellable?
|
||||||
|
private var processing: Set<String> = []
|
||||||
|
|
||||||
|
init(roster: Roster, client: Client) {
|
||||||
|
self.client = client
|
||||||
|
self.roster = roster
|
||||||
|
subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Camera and Gallery access
|
||||||
|
extension AttachmentsStore {
|
||||||
|
func checkCameraAuthorization() async {
|
||||||
|
let status = AVCaptureDevice.authorizationStatus(for: .video)
|
||||||
|
var isAuthorized = status == .authorized
|
||||||
|
if status == .notDetermined {
|
||||||
|
isAuthorized = await AVCaptureDevice.requestAccess(for: .video)
|
||||||
|
}
|
||||||
|
cameraAccessGranted = isAuthorized
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkGalleryAuthorization() async {
|
||||||
|
let status = PHPhotoLibrary.authorizationStatus()
|
||||||
|
var isAuthorized = status == .authorized
|
||||||
|
if status == .notDetermined {
|
||||||
|
let req = await PHPhotoLibrary.requestAuthorization(for: .readWrite)
|
||||||
|
isAuthorized = (req == .authorized) || (req == .limited)
|
||||||
|
}
|
||||||
|
galleryAccessGranted = isAuthorized
|
||||||
|
if isAuthorized {
|
||||||
|
await fetchGalleryItems()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchGalleryItems() async {
|
||||||
|
guard galleryAccessGranted else { return }
|
||||||
|
galleryItems = await GalleryItem.fetchAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Save outgoing attachments for future uploadings
|
||||||
|
extension AttachmentsStore {
|
||||||
|
func sendMedia(_ items: [GalleryItem]) {
|
||||||
|
Task {
|
||||||
|
for item in items {
|
||||||
|
Task {
|
||||||
|
var message = Message.blank
|
||||||
|
message.from = roster.bareJid
|
||||||
|
message.to = roster.contactBareJid
|
||||||
|
message.secure = secured
|
||||||
|
|
||||||
|
switch item.type {
|
||||||
|
case .photo:
|
||||||
|
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [item.id], options: nil).firstObject else { return }
|
||||||
|
guard let photo = try? await PHImageManager.default().getPhoto(for: asset) else { return }
|
||||||
|
guard let data = photo.jpegData(compressionQuality: 1.0) else { return }
|
||||||
|
let localName = "\(message.id)_\(UUID().uuidString).jpg"
|
||||||
|
let localUrl = FolderWrapper.shared.fileFolder.appendingPathComponent(localName)
|
||||||
|
try? data.write(to: localUrl)
|
||||||
|
message.contentType = .attachment(
|
||||||
|
Attachment(
|
||||||
|
type: .image,
|
||||||
|
localName: localName,
|
||||||
|
thumbnailName: nil,
|
||||||
|
remotePath: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try? await message.save()
|
||||||
|
|
||||||
|
case .video:
|
||||||
|
guard let asset = PHAsset.fetchAssets(withLocalIdentifiers: [item.id], options: nil).firstObject else { return }
|
||||||
|
guard let video = try? await PHImageManager.default().getVideo(for: asset) else { return }
|
||||||
|
// swiftlint:disable:next force_cast
|
||||||
|
let assetURL = video as! AVURLAsset
|
||||||
|
let url = assetURL.url
|
||||||
|
let localName = "\(message.id)_\(UUID().uuidString).mov"
|
||||||
|
let localUrl = FolderWrapper.shared.fileFolder.appendingPathComponent(localName)
|
||||||
|
try? FileManager.default.copyItem(at: url, to: localUrl)
|
||||||
|
message.contentType = .attachment(
|
||||||
|
Attachment(
|
||||||
|
type: .video,
|
||||||
|
localName: localName,
|
||||||
|
thumbnailName: nil,
|
||||||
|
remotePath: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try? await message.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendCaptured(_ data: Data, _ type: GalleryMediaType) {
|
||||||
|
Task {
|
||||||
|
var message = Message.blank
|
||||||
|
message.from = roster.bareJid
|
||||||
|
message.to = roster.contactBareJid
|
||||||
|
message.secure = secured
|
||||||
|
|
||||||
|
let localName: String
|
||||||
|
let msgType: AttachmentType
|
||||||
|
do {
|
||||||
|
(localName, msgType) = try await Task {
|
||||||
|
// local name
|
||||||
|
let fileId = UUID().uuidString
|
||||||
|
let localName: String
|
||||||
|
let msgType: AttachmentType
|
||||||
|
switch type {
|
||||||
|
case .photo:
|
||||||
|
localName = "\(message.id)_\(fileId).jpg"
|
||||||
|
msgType = .image
|
||||||
|
|
||||||
|
case .video:
|
||||||
|
localName = "\(message.id)_\(fileId).mov"
|
||||||
|
msgType = .video
|
||||||
|
}
|
||||||
|
|
||||||
|
// save
|
||||||
|
let localUrl = FolderWrapper.shared.fileFolder.appendingPathComponent(localName)
|
||||||
|
try data.write(to: localUrl)
|
||||||
|
return (localName, msgType)
|
||||||
|
}.value
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Can't save file for uploading: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save message
|
||||||
|
message.contentType = .attachment(
|
||||||
|
Attachment(
|
||||||
|
type: msgType,
|
||||||
|
localName: localName,
|
||||||
|
thumbnailName: nil,
|
||||||
|
remotePath: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
do {
|
||||||
|
try await message.save()
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Can't save message: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendDocuments(_ data: [Data], _ extensions: [String]) {
|
||||||
|
Task {
|
||||||
|
for (index, data) in data.enumerated() {
|
||||||
|
Task {
|
||||||
|
let newMessageId = UUID().uuidString
|
||||||
|
let fileId = UUID().uuidString
|
||||||
|
let localName = "\(newMessageId)_\(fileId).\(extensions[index])"
|
||||||
|
let localUrl = FolderWrapper.shared.fileFolder.appendingPathComponent(localName)
|
||||||
|
do {
|
||||||
|
try data.write(to: localUrl)
|
||||||
|
} catch {
|
||||||
|
print("FileProcessing: Error writing document: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = Message.blank
|
||||||
|
message.from = roster.bareJid
|
||||||
|
message.to = roster.contactBareJid
|
||||||
|
message.secure = secured
|
||||||
|
message.contentType = .attachment(
|
||||||
|
Attachment(
|
||||||
|
type: localName.attachmentType,
|
||||||
|
localName: localName,
|
||||||
|
thumbnailName: nil,
|
||||||
|
remotePath: nil
|
||||||
|
)
|
||||||
|
)
|
||||||
|
do {
|
||||||
|
try await message.save()
|
||||||
|
} catch {
|
||||||
|
print("FileProcessing: Error saving document: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Processing attachments
|
||||||
|
private extension AttachmentsStore {
|
||||||
|
func subscribe() {
|
||||||
|
messagesCancellable = ValueObservation.tracking(Message
|
||||||
|
.filter(
|
||||||
|
(Column("to") == roster.bareJid && Column("from") == roster.contactBareJid) ||
|
||||||
|
(Column("from") == roster.bareJid && Column("to") == roster.contactBareJid)
|
||||||
|
)
|
||||||
|
.order(Column("date").desc)
|
||||||
|
.fetchAll
|
||||||
|
)
|
||||||
|
.publisher(in: Database.shared.dbQueue, scheduling: .immediate)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { _ in
|
||||||
|
} receiveValue: { [weak self] messages in
|
||||||
|
let forProcessing = messages
|
||||||
|
.filter { $0.status != .error }
|
||||||
|
.filter { self?.processing.contains($0.id) == false }
|
||||||
|
.filter { $0.contentType.isAttachment }
|
||||||
|
for message in forProcessing {
|
||||||
|
if case .attachment(let attachment) = message.contentType {
|
||||||
|
let localPath = attachment.localPath
|
||||||
|
if localPath != nil, attachment.remotePath == nil {
|
||||||
|
// Uploading
|
||||||
|
self?.processing.insert(message.id)
|
||||||
|
Task {
|
||||||
|
await self?.uploadAttachment(message)
|
||||||
|
}
|
||||||
|
} else if localPath == nil, attachment.remotePath != nil {
|
||||||
|
// Downloading
|
||||||
|
self?.processing.insert(message.id)
|
||||||
|
Task {
|
||||||
|
await self?.downloadAttachment(message)
|
||||||
|
}
|
||||||
|
} else if localPath != nil, attachment.remotePath != nil, attachment.thumbnailName == nil, attachment.type == .image {
|
||||||
|
// Generate thumbnail
|
||||||
|
self?.processing.insert(message.id)
|
||||||
|
Task {
|
||||||
|
await self?.generateThumbnail(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chatCancellable = ValueObservation.tracking(Chat
|
||||||
|
.filter(Column("account") == roster.bareJid && Column("participant") == roster.contactBareJid)
|
||||||
|
.fetchOne
|
||||||
|
)
|
||||||
|
.publisher(in: Database.shared.dbQueue, scheduling: .immediate)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { _ in
|
||||||
|
} receiveValue: { [weak self] chat in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.secured = chat?.encrypted ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Uploadings/Downloadings
|
||||||
|
extension AttachmentsStore {
|
||||||
|
private func uploadAttachment(_ message: Message) async {
|
||||||
|
do {
|
||||||
|
try await message.setStatus(.pending)
|
||||||
|
var message = message
|
||||||
|
guard case .attachment(let attachment) = message.contentType else {
|
||||||
|
throw AppError.invalidContentType
|
||||||
|
}
|
||||||
|
guard let localName = attachment.localPath else {
|
||||||
|
throw AppError.invalidLocalName
|
||||||
|
}
|
||||||
|
let remotePath = try await client.uploadFile(localName, needEncrypt: message.secure)
|
||||||
|
message.contentType = .attachment(
|
||||||
|
Attachment(
|
||||||
|
type: attachment.type,
|
||||||
|
localName: attachment.localName,
|
||||||
|
thumbnailName: nil,
|
||||||
|
remotePath: remotePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
message.body = remotePath
|
||||||
|
message.oobUrl = remotePath
|
||||||
|
try await message.save()
|
||||||
|
try await client.sendMessage(message)
|
||||||
|
processing.remove(message.id)
|
||||||
|
try await message.setStatus(.sent)
|
||||||
|
} catch {
|
||||||
|
processing.remove(message.id)
|
||||||
|
try? await message.setStatus(.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func downloadAttachment(_ message: Message) async {
|
||||||
|
guard case .attachment(let attachment) = message.contentType else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let remotePath = attachment.remotePath, var remoteUrl = URL(string: remotePath) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
// if attachment encrypted, extract the key
|
||||||
|
// and format remote url
|
||||||
|
var encryptionKey: String?
|
||||||
|
if remoteUrl.scheme == "aesgcm", var components = URLComponents(url: remoteUrl, resolvingAgainstBaseURL: true) {
|
||||||
|
encryptionKey = components.fragment
|
||||||
|
components.scheme = "https"
|
||||||
|
components.fragment = nil
|
||||||
|
if let tmpUrl = components.url {
|
||||||
|
remoteUrl = tmpUrl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// make local name/path
|
||||||
|
let localName = "\(message.id)_\(UUID().uuidString).\(remoteUrl.lastPathComponent)"
|
||||||
|
let localUrl = FolderWrapper.shared.fileFolder.appendingPathComponent(localName)
|
||||||
|
|
||||||
|
// Download the file
|
||||||
|
let (tempUrl, _) = try await URLSession.shared.download(from: remoteUrl)
|
||||||
|
try FileManager.default.moveItem(at: tempUrl, to: localUrl)
|
||||||
|
|
||||||
|
if let encryptionKey {
|
||||||
|
// Decrypt the file
|
||||||
|
guard encryptionKey.count % 2 == 0, encryptionKey.count > 64 else {
|
||||||
|
throw AppError.securityError
|
||||||
|
}
|
||||||
|
let fragmentData = encryptionKey.map { char -> UInt8 in
|
||||||
|
return UInt8(char.hexDigitValue ?? 0)
|
||||||
|
}
|
||||||
|
let ivLen = fragmentData.count - (32 * 2)
|
||||||
|
var iv = Data()
|
||||||
|
var key = Data()
|
||||||
|
|
||||||
|
for index in 0 ..< (ivLen / 2) {
|
||||||
|
iv.append(fragmentData[index * 2] * 16 + fragmentData[index * 2 + 1])
|
||||||
|
}
|
||||||
|
for index in (ivLen / 2) ..< (fragmentData.count / 2) {
|
||||||
|
key.append(fragmentData[index * 2] * 16 + fragmentData[index * 2 + 1])
|
||||||
|
}
|
||||||
|
|
||||||
|
let encodedData = try Data(contentsOf: localUrl)
|
||||||
|
var result = Data()
|
||||||
|
|
||||||
|
guard AESGSMEngine.shared.decrypt(iv: iv, key: key, encoded: encodedData, auth: nil, output: &result) else {
|
||||||
|
throw AppError.securityError
|
||||||
|
}
|
||||||
|
|
||||||
|
try result.write(to: localUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
var message = message
|
||||||
|
message.contentType = .attachment(
|
||||||
|
Attachment(
|
||||||
|
type: attachment.type,
|
||||||
|
localName: localName,
|
||||||
|
thumbnailName: attachment.thumbnailName,
|
||||||
|
remotePath: remotePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
processing.remove(message.id)
|
||||||
|
try await message.save()
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Can't download attachment: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateThumbnail(_ message: Message) async {
|
||||||
|
guard case .attachment(let attachment) = message.contentType else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard attachment.type == .image else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let localName = attachment.localName, let localPath = attachment.localPath else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let thumbnailFileName = "thumb_\(localName)"
|
||||||
|
let thumbnailUrl = FolderWrapper.shared.fileFolder.appendingPathComponent(thumbnailFileName)
|
||||||
|
|
||||||
|
//
|
||||||
|
if !FileManager.default.fileExists(atPath: thumbnailUrl.path) {
|
||||||
|
guard let image = UIImage(contentsOfFile: localPath.path) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let targetSize = CGSize(width: Const.attachmentPreviewSize, height: Const.attachmentPreviewSize)
|
||||||
|
guard let thumbnail = try? await image.scaleAndCropImage(targetSize) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let data = thumbnail.jpegData(compressionQuality: 0.5) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
try data.write(to: thumbnailUrl)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
var message = message
|
||||||
|
message.contentType = .attachment(
|
||||||
|
Attachment(
|
||||||
|
type: attachment.type,
|
||||||
|
localName: attachment.localName,
|
||||||
|
thumbnailName: thumbnailFileName,
|
||||||
|
remotePath: attachment.remotePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
processing.remove(message.id)
|
||||||
|
try? await message.save()
|
||||||
|
}
|
||||||
|
}
|
46
AnotherIM/AppData/Store/ChatSettingsStore.swift
Normal file
46
AnotherIM/AppData/Store/ChatSettingsStore.swift
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Photos
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class ChatSettingsStore: ObservableObject {
|
||||||
|
@Published var chat: Chat?
|
||||||
|
|
||||||
|
private let client: Client
|
||||||
|
private let roster: Roster
|
||||||
|
|
||||||
|
private var chatCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
init(roster: Roster, client: Client) {
|
||||||
|
self.client = client
|
||||||
|
self.roster = roster
|
||||||
|
|
||||||
|
subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ChatSettingsStore {
|
||||||
|
func setSecured(_ secured: Bool) {
|
||||||
|
Task {
|
||||||
|
try? await chat?.setEncrypted(secured)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Processing attachments
|
||||||
|
private extension ChatSettingsStore {
|
||||||
|
func subscribe() {
|
||||||
|
chatCancellable = ValueObservation.tracking(Chat
|
||||||
|
.filter(Column("account") == roster.bareJid && Column("participant") == roster.contactBareJid)
|
||||||
|
.fetchOne
|
||||||
|
)
|
||||||
|
.publisher(in: Database.shared.dbQueue, scheduling: .immediate)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { _ in
|
||||||
|
} receiveValue: { [weak self] chat in
|
||||||
|
self?.chat = chat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
254
AnotherIM/AppData/Store/ClientsStore.swift
Normal file
254
AnotherIM/AppData/Store/ClientsStore.swift
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
enum ClientsListState {
|
||||||
|
case empty
|
||||||
|
case allDisabled
|
||||||
|
case haveSomeEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class ClientsStore: ObservableObject {
|
||||||
|
static let shared = ClientsStore()
|
||||||
|
|
||||||
|
@Published private(set) var ready = false
|
||||||
|
@Published private(set) var clients: [Client] = []
|
||||||
|
@Published private(set) var actualRosters: [Roster] = []
|
||||||
|
@Published private(set) var actualChats: [Chat] = []
|
||||||
|
@Published private(set) var listState: ClientsListState = .empty
|
||||||
|
|
||||||
|
private var credentialsCancellable: AnyCancellable?
|
||||||
|
private var rostersCancellable: AnyCancellable?
|
||||||
|
private var chatsCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
credentialsCancellable = ValueObservation
|
||||||
|
.tracking { db in
|
||||||
|
try Credentials.fetchAll(db)
|
||||||
|
}
|
||||||
|
.publisher(in: Database.shared.dbQueue)
|
||||||
|
.catch { _ in Just([]) }
|
||||||
|
.sink { [weak self] creds in
|
||||||
|
self?.processCredentials(creds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processCredentials(_ credentials: [Credentials]) {
|
||||||
|
let existsJids = Set(clients.map { $0.credentials.bareJid })
|
||||||
|
let credentialsJids = Set(credentials.map { $0.bareJid })
|
||||||
|
|
||||||
|
let forAdd = credentials.filter { !existsJids.contains($0.bareJid) }
|
||||||
|
let newClients = forAdd.map { Client(credentials: $0) }
|
||||||
|
|
||||||
|
let forRemove = clients.filter { !credentialsJids.contains($0.credentials.bareJid) }
|
||||||
|
forRemove.forEach { $0.disconnect() }
|
||||||
|
|
||||||
|
var updatedClients = clients.filter { credentialsJids.contains($0.credentials.bareJid) }
|
||||||
|
updatedClients.append(contentsOf: newClients)
|
||||||
|
clients = updatedClients
|
||||||
|
|
||||||
|
if !ready {
|
||||||
|
ready = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if credentials.isEmpty {
|
||||||
|
listState = .empty
|
||||||
|
} else if credentials.allSatisfy({ !$0.isActive }) {
|
||||||
|
listState = .allDisabled
|
||||||
|
} else {
|
||||||
|
listState = .haveSomeEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
resubscribeRosters()
|
||||||
|
resubscribeChats()
|
||||||
|
reconnectNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func client(for credentials: Credentials) -> Client? {
|
||||||
|
clients.first { $0.credentials == credentials }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Login/Connections
|
||||||
|
extension ClientsStore {
|
||||||
|
func tryLogin(_ jidStr: String, _ pass: String) async throws {
|
||||||
|
if let client = clients.first(where: { $0.credentials.bareJid == jidStr }) {
|
||||||
|
// check if credentials already exist and enable it
|
||||||
|
// this change will invoke reconnect automatically
|
||||||
|
await client.updActivity(true)
|
||||||
|
} else {
|
||||||
|
// new client login with fake timeout
|
||||||
|
async let sleep: Void? = try? await Task.sleep(nanoseconds: 1 * NSEC_PER_SEC)
|
||||||
|
async let request = try await Client.tryLogin(with: .init(bareJid: jidStr, pass: pass, isActive: true))
|
||||||
|
let client = try await(request, sleep).0
|
||||||
|
|
||||||
|
try? await client.credentials.save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reconnectNeeded() {
|
||||||
|
Task {
|
||||||
|
await withTaskGroup(of: Void.self) { taskGroup in
|
||||||
|
for client in clients {
|
||||||
|
if !client.credentials.isActive && client.state == .enabled(.connected) {
|
||||||
|
taskGroup.addTask {
|
||||||
|
client.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if client.credentials.isActive && client.state != .enabled(.connected) {
|
||||||
|
taskGroup.addTask {
|
||||||
|
await client.connect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Manage Rosters
|
||||||
|
extension ClientsStore {
|
||||||
|
func addRoster(_ credentials: Credentials, contactJID: String, name: String?, groups: [String]) async throws {
|
||||||
|
// check that roster exist in db as locally deleted and undelete it
|
||||||
|
let deletedLocally = await Roster.allDeletedLocally
|
||||||
|
if var roster = deletedLocally.first(where: { $0.contactBareJid == contactJID }) {
|
||||||
|
try await roster.setLocallyDeleted(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// add new roster
|
||||||
|
guard let client = client(for: credentials) else {
|
||||||
|
throw AppError.clientNotFound
|
||||||
|
}
|
||||||
|
try await client.addRoster(contactJID, name: name, groups: groups)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteRoster(_ roster: Roster) async throws {
|
||||||
|
guard let client = clients.first(where: { $0.credentials.bareJid == roster.bareJid }) else {
|
||||||
|
throw AppError.clientNotFound
|
||||||
|
}
|
||||||
|
try await client.deleteRoster(roster)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ClientsStore {
|
||||||
|
func addRosterForNewChatIfNeeded(_ chat: Chat) async throws {
|
||||||
|
let exists = try? await chat.fetchRoster()
|
||||||
|
if exists == nil {
|
||||||
|
guard let client = clients.first(where: { $0.credentials.bareJid == chat.account }) else {
|
||||||
|
throw AppError.clientNotFound
|
||||||
|
}
|
||||||
|
try await addRoster(client.credentials, contactJID: chat.participant, name: nil, groups: [])
|
||||||
|
// Hack here. Because we want to show chat immediately after adding roster (without waiting for server
|
||||||
|
// response and update rosters list) we need to write it to db manually
|
||||||
|
try await client.addRosterLocally(chat.participant, name: nil, groups: [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Produce stores for conversation
|
||||||
|
extension ClientsStore {
|
||||||
|
// swiftlint:disable:next large_tuple
|
||||||
|
func conversationStores(for roster: Roster) async throws -> (MessagesStore, AttachmentsStore, ChatSettingsStore) {
|
||||||
|
while !ready {
|
||||||
|
await Task.yield()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let client = clients.first(where: { $0.credentials.bareJid == roster.bareJid }) else {
|
||||||
|
throw AppError.clientNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
let conversationStore = MessagesStore(roster: roster, client: client)
|
||||||
|
let attachmentsStore = AttachmentsStore(roster: roster, client: client)
|
||||||
|
let settingsStore = ChatSettingsStore(roster: roster, client: client)
|
||||||
|
return (conversationStore, attachmentsStore, settingsStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
// swiftlint:disable:next large_tuple
|
||||||
|
func conversationStores(for chat: Chat) async throws -> (MessagesStore, AttachmentsStore, ChatSettingsStore) {
|
||||||
|
while !ready {
|
||||||
|
await Task.yield()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let client = clients.first(where: { $0.credentials.bareJid == chat.account }) else {
|
||||||
|
throw AppError.clientNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
let roster = try await chat.fetchRoster()
|
||||||
|
let conversationStore = MessagesStore(roster: roster, client: client)
|
||||||
|
let attachmentsStore = AttachmentsStore(roster: roster, client: client)
|
||||||
|
let settingsStore = ChatSettingsStore(roster: roster, client: client)
|
||||||
|
return (conversationStore, attachmentsStore, settingsStore)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Subscriptions
|
||||||
|
private extension ClientsStore {
|
||||||
|
private func resubscribeRosters() {
|
||||||
|
let clientsJids = clients
|
||||||
|
.filter { $0.credentials.isActive }
|
||||||
|
.map { $0.credentials.bareJid }
|
||||||
|
|
||||||
|
rostersCancellable = ValueObservation.tracking { db in
|
||||||
|
try Roster
|
||||||
|
.filter(clientsJids.contains(Column("bareJid")))
|
||||||
|
.filter(Column("locallyDeleted") == false)
|
||||||
|
.fetchAll(db)
|
||||||
|
}
|
||||||
|
.publisher(in: Database.shared.dbQueue)
|
||||||
|
.catch { _ in Just([]) }
|
||||||
|
.sink { [weak self] rosters in
|
||||||
|
self?.actualRosters = rosters
|
||||||
|
.sorted {
|
||||||
|
if $0.bareJid != $1.bareJid {
|
||||||
|
return $0.bareJid < $1.bareJid
|
||||||
|
} else {
|
||||||
|
return $0.contactBareJid < $1.contactBareJid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resubscribeChats() {
|
||||||
|
let clientsJids = clients
|
||||||
|
.filter { $0.credentials.isActive }
|
||||||
|
.map { $0.credentials.bareJid }
|
||||||
|
|
||||||
|
chatsCancellable = ValueObservation.tracking { db in
|
||||||
|
try Chat
|
||||||
|
.filter(clientsJids.contains(Column("account")))
|
||||||
|
.fetchAll(db)
|
||||||
|
}
|
||||||
|
.publisher(in: Database.shared.dbQueue)
|
||||||
|
.catch { _ in Just([]) }
|
||||||
|
.sink { [weak self] chats in
|
||||||
|
self?.actualChats = chats
|
||||||
|
.sorted {
|
||||||
|
if $0.account != $1.account {
|
||||||
|
return $0.account < $1.account
|
||||||
|
} else {
|
||||||
|
return $0.participant < $1.participant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ClientsStore {
|
||||||
|
func reconnectOnActiveState() {
|
||||||
|
reconnectNeeded()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Remove all data for debug
|
||||||
|
#if DEBUG
|
||||||
|
extension ClientsStore {
|
||||||
|
func flushAllData() {
|
||||||
|
clients.forEach { $0.disconnect() }
|
||||||
|
clients.removeAll()
|
||||||
|
actualRosters.removeAll()
|
||||||
|
actualChats.removeAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
164
AnotherIM/AppData/Store/MessagesStore.swift
Normal file
164
AnotherIM/AppData/Store/MessagesStore.swift
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Martin
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class MessagesStore: ObservableObject {
|
||||||
|
@Published private(set) var messages: [Message] = []
|
||||||
|
@Published var replyText = ""
|
||||||
|
|
||||||
|
private(set) var roster: Roster
|
||||||
|
private let client: Client
|
||||||
|
private var secured: Bool = false
|
||||||
|
|
||||||
|
private var messagesCancellable: AnyCancellable?
|
||||||
|
private var chatCancellable: AnyCancellable?
|
||||||
|
private let archiver = ArchiveMessageFetcher()
|
||||||
|
|
||||||
|
init(roster: Roster, client: Client) {
|
||||||
|
self.client = client
|
||||||
|
self.roster = roster
|
||||||
|
subscribe()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Send message
|
||||||
|
extension MessagesStore {
|
||||||
|
func sendMessage(_ message: String) {
|
||||||
|
Task {
|
||||||
|
var msg = Message.blank
|
||||||
|
msg.from = roster.bareJid
|
||||||
|
msg.to = roster.contactBareJid
|
||||||
|
msg.body = message
|
||||||
|
msg.secure = secured
|
||||||
|
|
||||||
|
// store as pending on db, and send
|
||||||
|
do {
|
||||||
|
try await msg.save()
|
||||||
|
try await client.sendMessage(msg)
|
||||||
|
try await msg.setStatus(.sent)
|
||||||
|
} catch {
|
||||||
|
try? await msg.setStatus(.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendContact(_ jidStr: String) {
|
||||||
|
sendMessage("contact:\(jidStr)")
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendLocation(_ lat: Double, _ lon: Double) {
|
||||||
|
sendMessage("geo:\(lat),\(lon)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Subscriptions
|
||||||
|
private extension MessagesStore {
|
||||||
|
func subscribe() {
|
||||||
|
messagesCancellable = ValueObservation.tracking(Message
|
||||||
|
.filter(
|
||||||
|
(Column("to") == roster.bareJid && Column("from") == roster.contactBareJid) ||
|
||||||
|
(Column("from") == roster.bareJid && Column("to") == roster.contactBareJid)
|
||||||
|
)
|
||||||
|
.order(Column("date").desc)
|
||||||
|
.fetchAll
|
||||||
|
)
|
||||||
|
.publisher(in: Database.shared.dbQueue, scheduling: .immediate)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { _ in
|
||||||
|
} receiveValue: { [weak self] messages in
|
||||||
|
guard let self else { return }
|
||||||
|
self.messages = messages
|
||||||
|
Task {
|
||||||
|
await self.archiver.initialFetch(messages, self.roster, self.client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chatCancellable = ValueObservation.tracking(Chat
|
||||||
|
.filter(Column("account") == roster.bareJid && Column("participant") == roster.contactBareJid)
|
||||||
|
.fetchOne
|
||||||
|
)
|
||||||
|
.publisher(in: Database.shared.dbQueue, scheduling: .immediate)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { _ in
|
||||||
|
} receiveValue: { [weak self] chat in
|
||||||
|
guard let self = self else { return }
|
||||||
|
self.secured = chat?.encrypted ?? false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Archived messages
|
||||||
|
extension MessagesStore {
|
||||||
|
func scrolledMessage(_ messageId: String) {
|
||||||
|
if messageId == messages.last?.id {
|
||||||
|
Task {
|
||||||
|
await archiver.fetchBackward(roster, client)
|
||||||
|
}
|
||||||
|
} else if messageId == messages.first?.id {
|
||||||
|
Task {
|
||||||
|
await archiver.fetchForward(roster, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private actor ArchiveMessageFetcher {
|
||||||
|
private var initFetchStarted = false
|
||||||
|
private var forwardRsm: RSM.Query?
|
||||||
|
private var backwardRsm: RSM.Query?
|
||||||
|
private var fetchInProgress = false
|
||||||
|
|
||||||
|
func initialFetch(_ messages: [Message], _ roster: Roster, _ client: Client) async {
|
||||||
|
if initFetchStarted { return }
|
||||||
|
initFetchStarted = true
|
||||||
|
fetchInProgress = true
|
||||||
|
|
||||||
|
do {
|
||||||
|
if let firstExistId = messages.first?.id {
|
||||||
|
let result = try await client.fetchArchiveMessages(for: roster, query: .init(before: firstExistId, max: Const.mamRequestPageSize))
|
||||||
|
result.complete ? forwardRsm = nil : (forwardRsm = .init(after: result.rsm?.last, max: Const.mamRequestPageSize))
|
||||||
|
result.complete ? backwardRsm = nil : (backwardRsm = .init(before: result.rsm?.first, max: Const.mamRequestPageSize))
|
||||||
|
} else {
|
||||||
|
let result = try await client.fetchArchiveMessages(for: roster, query: .init(lastItems: Const.mamRequestPageSize))
|
||||||
|
result.complete ? backwardRsm = nil : (backwardRsm = .init(before: result.rsm?.first, max: Const.mamRequestPageSize))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
logIt(.error, "Error requesting archived messages: \(error)")
|
||||||
|
initFetchStarted = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchInProgress = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchForward(_ roster: Roster, _ client: Client) async {
|
||||||
|
while !initFetchStarted {
|
||||||
|
await Task.yield()
|
||||||
|
}
|
||||||
|
guard let rsm = forwardRsm else { return }
|
||||||
|
if fetchInProgress { return }
|
||||||
|
|
||||||
|
fetchInProgress = true
|
||||||
|
Task {
|
||||||
|
let result = try await client.fetchArchiveMessages(for: roster, query: rsm)
|
||||||
|
result.complete ? (forwardRsm = nil) : (forwardRsm = .init(after: result.rsm?.last, max: Const.mamRequestPageSize))
|
||||||
|
fetchInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchBackward(_ roster: Roster, _ client: Client) async {
|
||||||
|
while !initFetchStarted {
|
||||||
|
await Task.yield()
|
||||||
|
}
|
||||||
|
guard let rsm = backwardRsm else { return }
|
||||||
|
if fetchInProgress { return }
|
||||||
|
|
||||||
|
fetchInProgress = true
|
||||||
|
Task {
|
||||||
|
let result = try await client.fetchArchiveMessages(for: roster, query: rsm)
|
||||||
|
result.complete ? (backwardRsm = nil) : (backwardRsm = .init(before: result.rsm?.first, max: Const.mamRequestPageSize))
|
||||||
|
fetchInProgress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
2
AnotherIM/Generated/.gitignore
vendored
Normal file
2
AnotherIM/Generated/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
16
AnotherIM/Helpers/AVAsset+Thumbnail.swift
Normal file
16
AnotherIM/Helpers/AVAsset+Thumbnail.swift
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import AVFoundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension AVAsset {
|
||||||
|
func generateVideoThumbnail(_ size: CGSize) async throws -> UIImage {
|
||||||
|
try await Task {
|
||||||
|
let assetImgGenerate = AVAssetImageGenerator(asset: self)
|
||||||
|
assetImgGenerate.appliesPreferredTrackTransform = true
|
||||||
|
let time = CMTimeMakeWithSeconds(Float64(1), preferredTimescale: 600)
|
||||||
|
let cgImage = try assetImgGenerate.copyCGImage(at: time, actualTime: nil)
|
||||||
|
let image = UIImage(cgImage: cgImage)
|
||||||
|
let result = try await image.scaleAndCropImage(size)
|
||||||
|
return result
|
||||||
|
}.value
|
||||||
|
}
|
||||||
|
}
|
12
AnotherIM/Helpers/Binding+Extensions.swift
Normal file
12
AnotherIM/Helpers/Binding+Extensions.swift
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
50
AnotherIM/Helpers/ButtonStyles.swift
Normal file
50
AnotherIM/Helpers/ButtonStyles.swift
Normal file
|
@ -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(.Material.Shape.white)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: ButtonSizes.cornerRadius)
|
||||||
|
.foregroundColor(isEnabled ? .Material.Elements.active : .Material.Shape.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.Elements.active : .Material.Shape.separator)
|
||||||
|
.background {
|
||||||
|
RoundedRectangle(cornerRadius: ButtonSizes.cornerRadius)
|
||||||
|
.stroke(isEnabled ? Color.Material.Elements.active : Color.Material.Shape.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)
|
||||||
|
}
|
||||||
|
}
|
13
AnotherIM/Helpers/Colors+Tappable.swift
Normal file
13
AnotherIM/Helpers/Colors+Tappable.swift
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public extension Color {
|
||||||
|
static let clearTappable = Color.white.opacity(0.0001)
|
||||||
|
// 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
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
51
AnotherIM/Helpers/Const.swift
Normal file
51
AnotherIM/Helpers/Const.swift
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum Const {
|
||||||
|
// 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 ?? "another.im"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trusted servers
|
||||||
|
enum TrustedServers: String {
|
||||||
|
case narayana = "narayana.im"
|
||||||
|
case conversations = "conversations.im"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit for video for sharing
|
||||||
|
static let videoDurationLimit = 60.0
|
||||||
|
|
||||||
|
// Grid size for gallery preview (3 in a row)
|
||||||
|
static let galleryGridSize = UIScreen.main.bounds.width / 3
|
||||||
|
|
||||||
|
// Size for map preview for location messages
|
||||||
|
static let mapPreviewSize = UIScreen.main.bounds.width * 0.5
|
||||||
|
|
||||||
|
// Size for attachment preview
|
||||||
|
static let attachmentPreviewSize = UIScreen.main.bounds.width * 0.5
|
||||||
|
|
||||||
|
// MAM request page size
|
||||||
|
static let mamRequestPageSize = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
final class FolderWrapper {
|
||||||
|
static let shared = FolderWrapper()
|
||||||
|
let fileFolder: URL
|
||||||
|
private init() {
|
||||||
|
// swiftlint:disable:next force_unwrapping
|
||||||
|
let documentsURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!
|
||||||
|
let subdirectoryURL = documentsURL.appendingPathComponent("Downloads")
|
||||||
|
if !FileManager.default.fileExists(atPath: subdirectoryURL.path) {
|
||||||
|
try? FileManager.default.createDirectory(at: subdirectoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||||
|
}
|
||||||
|
fileFolder = subdirectoryURL
|
||||||
|
}
|
||||||
|
}
|
15
AnotherIM/Helpers/EdgeInsets+Extensions.swift
Normal file
15
AnotherIM/Helpers/EdgeInsets+Extensions.swift
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
16
AnotherIM/Helpers/Map+Extensions.swift
Normal file
16
AnotherIM/Helpers/Map+Extensions.swift
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
import MapKit
|
||||||
|
|
||||||
|
extension MKCoordinateRegion: Equatable {
|
||||||
|
public static func == (lhs: MKCoordinateRegion, rhs: MKCoordinateRegion) -> Bool {
|
||||||
|
lhs.center.latitude == rhs.center.latitude &&
|
||||||
|
lhs.center.longitude == rhs.center.longitude &&
|
||||||
|
lhs.span.latitudeDelta == rhs.span.latitudeDelta &&
|
||||||
|
lhs.span.longitudeDelta == rhs.span.longitudeDelta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension CLLocationCoordinate2D: Identifiable {
|
||||||
|
public var id: String {
|
||||||
|
"\(latitude)-\(longitude)"
|
||||||
|
}
|
||||||
|
}
|
43
AnotherIM/Helpers/PHImageManager+Fetch.swift
Normal file
43
AnotherIM/Helpers/PHImageManager+Fetch.swift
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
import Photos
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension PHImageManager {
|
||||||
|
func getPhoto(for asset: PHAsset) async throws -> UIImage {
|
||||||
|
let options = PHImageRequestOptions()
|
||||||
|
options.version = .original
|
||||||
|
options.isSynchronous = true
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
requestImage(
|
||||||
|
for: asset,
|
||||||
|
targetSize: PHImageManagerMaximumSize,
|
||||||
|
contentMode: .aspectFill,
|
||||||
|
options: options
|
||||||
|
) { image, _ in
|
||||||
|
if let image {
|
||||||
|
continuation.resume(returning: image)
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: AppError.imageNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getVideo(for asset: PHAsset) async throws -> AVAsset {
|
||||||
|
let options = PHVideoRequestOptions()
|
||||||
|
options.version = .original
|
||||||
|
options.deliveryMode = .highQualityFormat
|
||||||
|
options.isNetworkAccessAllowed = true
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
requestAVAsset(
|
||||||
|
forVideo: asset,
|
||||||
|
options: options
|
||||||
|
) { avAsset, _, _ in
|
||||||
|
if let avAsset {
|
||||||
|
continuation.resume(returning: avAsset)
|
||||||
|
} else {
|
||||||
|
continuation.resume(throwing: AppError.videoNotFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
98
AnotherIM/Helpers/String+Extensions.swift
Normal file
98
AnotherIM/Helpers/String+Extensions.swift
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
import CoreLocation
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
var firstLetter: String {
|
||||||
|
String(prefix(1)).uppercased()
|
||||||
|
}
|
||||||
|
|
||||||
|
var makeReply: String {
|
||||||
|
let allLines = components(separatedBy: .newlines)
|
||||||
|
let nonBlankLines = allLines.filter { !$0.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty }
|
||||||
|
var result = nonBlankLines.joined(separator: "\n")
|
||||||
|
result = "> \(result)"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var isLocation: Bool {
|
||||||
|
hasPrefix("geo:")
|
||||||
|
}
|
||||||
|
|
||||||
|
var getLatLon: CLLocationCoordinate2D {
|
||||||
|
let geo = components(separatedBy: ":")[1]
|
||||||
|
let parts = geo.components(separatedBy: ",")
|
||||||
|
let lat = Double(parts[0]) ?? 0.0
|
||||||
|
let lon = Double(parts[1]) ?? 0.0
|
||||||
|
return CLLocationCoordinate2D(latitude: lat, longitude: lon)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isContact: Bool {
|
||||||
|
hasPrefix("contact:")
|
||||||
|
}
|
||||||
|
|
||||||
|
var getContactJid: String {
|
||||||
|
components(separatedBy: ":")[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
var attachmentType: AttachmentType {
|
||||||
|
let ext = (self as NSString).pathExtension.lowercased()
|
||||||
|
if ext.contains("jpeg") || ext.contains("jpg") || ext.contains("png") || ext.contains("gif") {
|
||||||
|
return .image
|
||||||
|
} else if ext.contains("mov") || ext.contains("mp4") || ext.contains("avi") {
|
||||||
|
return .video
|
||||||
|
} else if ext.contains("mp3") || ext.contains("wav") || ext.contains("m4a") {
|
||||||
|
return .audio
|
||||||
|
} else {
|
||||||
|
return .file
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
var firstLetterColor: Color {
|
||||||
|
let firstLetter = self.firstLetter
|
||||||
|
switch firstLetter {
|
||||||
|
case "A", "M", "Y":
|
||||||
|
return Color.Rainbow.tortoiseLight500
|
||||||
|
|
||||||
|
case "B", "N", "Z":
|
||||||
|
return Color.Rainbow.orangeLight500
|
||||||
|
|
||||||
|
case "C", "O":
|
||||||
|
return Color.Rainbow.yellowLight500
|
||||||
|
|
||||||
|
case "D", "P":
|
||||||
|
return Color.Rainbow.greenLight500
|
||||||
|
|
||||||
|
case "E", "Q":
|
||||||
|
return Color.Rainbow.blueLight500
|
||||||
|
|
||||||
|
case "F", "R":
|
||||||
|
return Color.Rainbow.magentaLight500
|
||||||
|
|
||||||
|
case "G", "S":
|
||||||
|
return Color.Rainbow.tortoiseDark500
|
||||||
|
|
||||||
|
case "H", "T":
|
||||||
|
return Color.Rainbow.orangeDark500
|
||||||
|
|
||||||
|
case "I", "U":
|
||||||
|
return Color.Rainbow.yellowDark500
|
||||||
|
|
||||||
|
case "J", "V":
|
||||||
|
return Color.Rainbow.greenDark500
|
||||||
|
|
||||||
|
case "K", "W":
|
||||||
|
return Color.Rainbow.blueDark500
|
||||||
|
|
||||||
|
case "L", "X":
|
||||||
|
return Color.Rainbow.magentaDark500
|
||||||
|
|
||||||
|
default:
|
||||||
|
return Color.Rainbow.tortoiseLight500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
9
AnotherIM/Helpers/TimeInterval+Extensions.swift
Normal file
9
AnotherIM/Helpers/TimeInterval+Extensions.swift
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension TimeInterval {
|
||||||
|
var minAndSec: String {
|
||||||
|
let minutes = Int(self) / 60
|
||||||
|
let seconds = Int(self) % 60
|
||||||
|
return String(format: "%02d:%02d", minutes, seconds)
|
||||||
|
}
|
||||||
|
}
|
13
AnotherIM/Helpers/Typography.swift
Normal file
13
AnotherIM/Helpers/Typography.swift
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
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)
|
||||||
|
static let sub2 = Font.system(size: 8, weight: .regular, design: .rounded)
|
||||||
|
}
|
10
AnotherIM/Helpers/UIApplication+Extensions.swift
Normal file
10
AnotherIM/Helpers/UIApplication+Extensions.swift
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
func openAppSettings() {
|
||||||
|
if
|
||||||
|
let appSettingsUrl = URL(string: UIApplication.openSettingsURLString),
|
||||||
|
UIApplication.shared.canOpenURL(appSettingsUrl)
|
||||||
|
{
|
||||||
|
UIApplication.shared.open(appSettingsUrl, completionHandler: nil)
|
||||||
|
}
|
||||||
|
}
|
30
AnotherIM/Helpers/UIImage+Crop.swift
Normal file
30
AnotherIM/Helpers/UIImage+Crop.swift
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
extension UIImage {
|
||||||
|
func scaleAndCropImage(_ size: CGSize) async throws -> UIImage {
|
||||||
|
try await Task {
|
||||||
|
let aspect = self.size.width / self.size.height
|
||||||
|
let targetAspect = size.width / size.height
|
||||||
|
var newWidth: CGFloat
|
||||||
|
var newHeight: CGFloat
|
||||||
|
if aspect < targetAspect {
|
||||||
|
newWidth = size.width
|
||||||
|
newHeight = size.width / aspect
|
||||||
|
} else {
|
||||||
|
newHeight = size.height
|
||||||
|
newWidth = size.height * aspect
|
||||||
|
}
|
||||||
|
|
||||||
|
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
|
||||||
|
self.draw(in: CGRect(x: (size.width - newWidth) / 2, y: (size.height - newHeight) / 2, width: newWidth, height: newHeight))
|
||||||
|
let newImage = UIGraphicsGetImageFromCurrentImageContext()
|
||||||
|
UIGraphicsEndImageContext()
|
||||||
|
if let newImage = newImage {
|
||||||
|
return newImage
|
||||||
|
} else {
|
||||||
|
throw NSError(domain: "UIImage", code: -900, userInfo: nil)
|
||||||
|
}
|
||||||
|
}.value
|
||||||
|
}
|
||||||
|
}
|
13
AnotherIM/Helpers/URL+Extensions.swift
Normal file
13
AnotherIM/Helpers/URL+Extensions.swift
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
extension URL {
|
||||||
|
var mimeType: String {
|
||||||
|
let pathExtension = self.pathExtension
|
||||||
|
|
||||||
|
if let uti = UTType(filenameExtension: pathExtension) {
|
||||||
|
return uti.preferredMIMEType ?? "application/octet-stream"
|
||||||
|
} else {
|
||||||
|
return "application/octet-stream"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
AnotherIM/Helpers/Vibration.swift
Normal file
17
AnotherIM/Helpers/Vibration.swift
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
enum Vibration: String {
|
||||||
|
case error
|
||||||
|
case success
|
||||||
|
|
||||||
|
public func vibrate() {
|
||||||
|
switch self {
|
||||||
|
case .error:
|
||||||
|
UINotificationFeedbackGenerator().notificationOccurred(.error)
|
||||||
|
|
||||||
|
case .success:
|
||||||
|
UINotificationFeedbackGenerator().notificationOccurred(.success)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
AnotherIM/Helpers/View+Debug.swift
Normal file
35
AnotherIM/Helpers/View+Debug.swift
Normal file
|
@ -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
|
15
AnotherIM/Helpers/View+Flip.swift
Normal file
15
AnotherIM/Helpers/View+Flip.swift
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct FlipView: ViewModifier {
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.rotationEffect(.radians(Double.pi))
|
||||||
|
.scaleEffect(x: -1, y: 1, anchor: .center)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func flip() -> some View {
|
||||||
|
modifier(FlipView())
|
||||||
|
}
|
||||||
|
}
|
11
AnotherIM/Helpers/View+If.swift
Normal file
11
AnotherIM/Helpers/View+If.swift
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public extension View {
|
||||||
|
@ViewBuilder func `if`<Content: View>(_ condition: @autoclosure () -> Bool, transform: (Self) -> Content) -> some View {
|
||||||
|
if condition() {
|
||||||
|
transform(self)
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
AnotherIM/Helpers/View+OnLoad.swift
Normal file
27
AnotherIM/Helpers/View+OnLoad.swift
Normal file
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
AnotherIM/Helpers/View+TappableArea.swift
Normal file
27
AnotherIM/Helpers/View+TappableArea.swift
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
6
AnotherIM/Resources/Assets/Colors.xcassets/Contents.json
Normal file
6
AnotherIM/Resources/Assets/Colors.xcassets/Contents.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xE4",
|
||||||
|
"green" : "0xE4",
|
||||||
|
"red" : "0xE4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0x4D",
|
||||||
|
"green" : "0x46",
|
||||||
|
"red" : "0x3C"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"color" : {
|
||||||
|
"color-space" : "srgb",
|
||||||
|
"components" : {
|
||||||
|
"alpha" : "1.000",
|
||||||
|
"blue" : "0xAC",
|
||||||
|
"green" : "0xA3",
|
||||||
|
"red" : "0x95"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
},
|
||||||
|
"properties" : {
|
||||||
|
"provides-namespace" : true
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue