Compare commits
60 commits
master
...
monal-base
Author | SHA1 | Date | |
---|---|---|---|
9b91add453 | |||
413d4f1c72 | |||
55b3469284 | |||
868f326bbc | |||
edfc3785e9 | |||
ae767456dd | |||
bfb7bd00aa | |||
cc2207ba82 | |||
b4a708ffad | |||
cbe67fe3f1 | |||
4378b2806c | |||
8515b79b57 | |||
e6b3d856a7 | |||
02801e46b4 | |||
8e3bc01632 | |||
406b2fde1d | |||
c8cf403d15 | |||
006ae2a612 | |||
f8030ed60e | |||
f489425b99 | |||
6a4acfb6f3 | |||
f8ce451b63 | |||
880408b04a | |||
6934b0fa97 | |||
eb037c452a | |||
87642a5a10 | |||
32e42f8506 | |||
c5750f9825 | |||
340bb5a1b4 | |||
598b07661c | |||
ca5e4b930c | |||
dce50cddca | |||
515ed2197e | |||
2b876e5ae6 | |||
9b8b5d1ae6 | |||
60e14a5406 | |||
13ace49560 | |||
a59d3bbeb7 | |||
477e58f35c | |||
ea333acb1a | |||
9d6f610b63 | |||
57793daf3d | |||
c4d70ef77d | |||
9e07aed5fd | |||
49f1028d69 | |||
4c42b21559 | |||
d1e3bc54bf | |||
ad7a2b4f59 | |||
22b6f415d2 | |||
ee2f68e33b | |||
3640f0ec06 | |||
e1f7971ee7 | |||
e210861382 | |||
f06208ec66 | |||
f9de9e3800 | |||
a808af8057 | |||
cc7a3b9286 | |||
98061025f3 | |||
4061484298 | |||
10475f9a72 |
176
.gitignore
vendored
|
@ -1,123 +1,77 @@
|
|||
docs
|
||||
Frameworks
|
||||
|
||||
#########################
|
||||
# **.gitignore** file for Xcode4 / OS X Source projects
|
||||
#
|
||||
# NB: if you are storing "built" products, this WILL NOT WORK,
|
||||
# and you should use a different **.gitignore** (or none at all)
|
||||
# This file is for SOURCE projects, where there are many extra
|
||||
# files that we want to exclude
|
||||
#
|
||||
# For updates, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects
|
||||
#########################
|
||||
# rust bridge
|
||||
rust/monal-rust-swift-bridge/generated
|
||||
rust/LibMonalRustSwiftBridge
|
||||
|
||||
#####
|
||||
# OS X temporary files that should never be committed
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/rust
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=rust
|
||||
|
||||
### Rust ###
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
!rust/Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/rust
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.swp
|
||||
profile
|
||||
|
||||
# Xcode
|
||||
Monal/Monal.xcodeproj/xcuserdata/*
|
||||
Monal/Monal.xcodeproj/project.xcworkspace/xcshareddata/*
|
||||
Monal/Monal.xcodeproj/project.xcworkspace/xcuserdata/*
|
||||
contents.xcworkspacedata
|
||||
._*
|
||||
*.xcscmblueprint
|
||||
*.xccheckout
|
||||
Monal/Monal.xcworkspace/xcshareddata/*
|
||||
Monal/Monal.xcworkspace/xcuserdata/*
|
||||
|
||||
####
|
||||
# Xcode temporary files that should never be committed
|
||||
#
|
||||
# NB: NIB/XIB files still exist even on Storyboard projects, so we want this...
|
||||
|
||||
*~.nib
|
||||
|
||||
|
||||
####
|
||||
# Xcode build files -
|
||||
#
|
||||
# NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData"
|
||||
|
||||
DerivedData/
|
||||
|
||||
# NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build"
|
||||
|
||||
build/
|
||||
|
||||
|
||||
#####
|
||||
# Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups)
|
||||
#
|
||||
# This is complicated:
|
||||
#
|
||||
# SOMETIMES you need to put this file in version control.
|
||||
# Apple designed it poorly - if you use "custom executables", they are
|
||||
# saved in this file.
|
||||
# 99% of projects do NOT use those, so they do NOT want to version control this file.
|
||||
# ..but if you're in the 1%, comment out the line "*.pbxuser"
|
||||
|
||||
Monal/build/*
|
||||
*.pbxuser
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.perspectivev3
|
||||
# NB: also, whitelist the default ones, some projects need to use these
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
|
||||
####
|
||||
# Xcode 4 - semi-personal settings, often included in workspaces
|
||||
#
|
||||
# You can safely ignore the xcuserdata files - but do NOT ignore the files next to them
|
||||
#
|
||||
|
||||
xcuserdata
|
||||
|
||||
####
|
||||
# XCode 4 workspaces - more detailed
|
||||
#
|
||||
# Workspaces are important! They are a core feature of Xcode - don't exclude them :)
|
||||
#
|
||||
# Workspace layout is quite spammy. For reference:
|
||||
#
|
||||
# (root)/
|
||||
# (project-name).xcodeproj/
|
||||
# project.pbxproj
|
||||
# project.xcworkspace/
|
||||
# contents.xcworkspacedata
|
||||
# xcuserdata/
|
||||
# (your name)/xcuserdatad/
|
||||
# xcuserdata/
|
||||
# (your name)/xcuserdatad/
|
||||
#
|
||||
#
|
||||
#
|
||||
# Xcode 4 workspaces - SHARED
|
||||
#
|
||||
# This is UNDOCUMENTED (google: "developer.apple.com xcshareddata" - 0 results
|
||||
# But if you're going to kill personal workspaces, at least keep the shared ones...
|
||||
#
|
||||
#
|
||||
!xcshareddata
|
||||
|
||||
####
|
||||
# XCode 4 build-schemes
|
||||
#
|
||||
# PRIVATE ones are stored inside xcuserdata
|
||||
!xcschemes
|
||||
|
||||
####
|
||||
# Xcode 4 - Deprecated classes
|
||||
#
|
||||
# Allegedly, if you manually "deprecate" your classes, they get moved here.
|
||||
#
|
||||
# We're using source-control, so this is a "feature" that we do not want!
|
||||
|
||||
profile
|
||||
*.moved-aside
|
||||
/.idea
|
||||
/ConversationsClassic/.idea
|
||||
/ConversationsClassic.xcodeproj
|
||||
/Info.plist
|
||||
/ConversationsClassic/ConversationsClassic.entitlements
|
||||
/XMPPSwift/Client/VoIP/rickroll.mp4
|
||||
/.nvim
|
||||
/buildServer.json
|
||||
TODO.txt
|
||||
PASSWD.txt
|
||||
DerivedData
|
||||
|
||||
#
|
||||
Monal/.nvim
|
||||
Monal/buildServer.json
|
||||
Monal/MY_MARKS.txt
|
||||
|
||||
# Pods
|
||||
Monal/Pods
|
||||
|
||||
#Don't accidentally commit localization state
|
||||
Monal/localization/external
|
||||
Monal/localization/external/*
|
||||
Monal/shareSheet-iOS/localization/external
|
||||
Monal/shareSheet-iOS/localization/external/*
|
||||
|
||||
# certs and other encrypted stuff
|
||||
*.pem
|
||||
*.key
|
||||
*.csr
|
||||
*.cer
|
||||
*.mobileprovision
|
||||
*.provisionprofile
|
||||
*.p12
|
||||
Monal/Classes/secrets.h
|
||||
|
|
125
.swiftlint.yml
Normal file
|
@ -0,0 +1,125 @@
|
|||
---
|
||||
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"
|
||||
- Classes
|
||||
- Pods
|
||||
- MonalUITests
|
||||
- MonalXMPPUnitTests
|
BIN
Art/alpha_logo.png
Normal file
After Width: | Height: | Size: 182 KiB |
BIN
Art/callkit_logo.png
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
Art/chat.png
Normal file
After Width: | Height: | Size: 195 KiB |
BIN
Art/chat2.png
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
Art/chat_dark.png
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
Art/friends.png
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
Art/friends2.png
Normal file
After Width: | Height: | Size: 146 KiB |
BIN
Art/friends_dark.png
Normal file
After Width: | Height: | Size: 190 KiB |
BIN
Art/logo.png
Normal file
After Width: | Height: | Size: 188 KiB |
149
Art/monal.svg
Normal file
|
@ -0,0 +1,149 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="20mm"
|
||||
height="20.085215mm"
|
||||
viewBox="0 0 20 20.085215"
|
||||
version="1.1"
|
||||
id="svg3834"
|
||||
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
|
||||
sodipodi:docname="monal.svg"
|
||||
style="enable-background:new"
|
||||
inkscape:export-filename="/Users/anurodhp/Desktop/monal.png"
|
||||
inkscape:export-xdpi="1300.48"
|
||||
inkscape:export-ydpi="1300.48">
|
||||
<defs
|
||||
id="defs3828">
|
||||
<inkscape:perspective
|
||||
sodipodi:type="inkscape:persp3d"
|
||||
inkscape:vp_x="0 : 10.042608 : 1"
|
||||
inkscape:vp_y="0 : 1000 : 0"
|
||||
inkscape:vp_z="20 : 10.042608 : 1"
|
||||
inkscape:persp3d-origin="10 : 6.6950717 : 1"
|
||||
id="perspective841" />
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
id="base"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:zoom="9.0725624"
|
||||
inkscape:cx="28.787632"
|
||||
inkscape:cy="44.279616"
|
||||
inkscape:document-units="mm"
|
||||
inkscape:current-layer="layer3"
|
||||
showgrid="false"
|
||||
inkscape:window-width="1280"
|
||||
inkscape:window-height="700"
|
||||
inkscape:window-x="-4"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:pagecheckerboard="false"
|
||||
fit-margin-top="0"
|
||||
fit-margin-left="0"
|
||||
fit-margin-right="0"
|
||||
fit-margin-bottom="0" />
|
||||
<metadata
|
||||
id="metadata3831">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-181.15717,-221.0978)"
|
||||
style="display:inline">
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer3"
|
||||
inkscape:label="Layer 2"
|
||||
style="display:inline">
|
||||
<rect
|
||||
id="rect30"
|
||||
width="20"
|
||||
height="20"
|
||||
x="181.15717"
|
||||
y="221.18301"
|
||||
style="stroke-width:0.26458332" />
|
||||
<rect
|
||||
style="fill:#2cd3e3;fill-opacity:1;stroke-width:0.26458332;opacity:1"
|
||||
id="rect48"
|
||||
width="20"
|
||||
height="20"
|
||||
x="181.15717"
|
||||
y="221.18301"
|
||||
inkscape:export-xdpi="1299.6801"
|
||||
inkscape:export-ydpi="1299.6801" />
|
||||
</g>
|
||||
<g
|
||||
id="g25"
|
||||
transform="matrix(1.25,0,0,1.25,-45.477532,-55.597438)">
|
||||
<g
|
||||
id="g4578"
|
||||
transform="translate(0.8477441,0.34537723)">
|
||||
<path
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.21166666"
|
||||
d="m 191.56803,221.43167 c 0.44531,-0.54927 1.00283,-1.55943 1.42657,-1.47418 -1.6117,3.28945 -5.09363,5.55983 -6.77643,8.41276 -1.48053,1.02259 -5.59221,4.97552 -3.50788,4.62647 2.55426,-0.33289 4.26942,-0.18434 6.47037,-0.0863 2.97857,0.44913 2.99008,0.35922 1.42237,-0.94939 -4.08407,-2.92741 -7.3886,-5.96422 -2.55345,-2.51434 1.19608,0.89899 3.58977,2.88011 4.62961,3.96256 0.14192,0.38081 -1.36875,-1.03083 -1.63269,-1.38151 1.00787,0.47021 4.41857,3.94275 5.65285,3.45204 1.11708,-0.36357 1.5434,-1.4884 2.64165,-0.91497 0.73976,-0.52896 -0.81203,-1.84714 0.109,-2.45498 1.18118,-0.18435 -0.2902,-1.51785 0.93686,-1.87197 0.72674,-0.62762 -1.09372,-1.01894 -1.52077,-1.45287 -0.89602,-0.45402 -0.35512,-1.55824 -0.004,-1.79086 0.0846,-1.03756 1.78245,-0.86434 2.36976,-1.66716 0.74024,-0.20999 1.24713,-1.13045 0.10892,-1.05465 -1.29104,-0.33585 -2.48308,0.3858 -3.65095,0.80824 -0.75744,0.43619 -1.51475,-0.3745 -0.67602,-0.9122 0.086,-0.87983 -0.49258,-4.71845 2.78132,-3.06301 0.34122,0.76448 -1.29924,0.73222 -1.56848,1.44997 -0.51725,0.58889 -0.82268,1.36738 -0.81609,2.1534 1.37222,-0.61392 1.59347,-2.39848 2.89681,-3.09724 0.62326,-0.9785 1.02124,0.86222 0.0967,0.71688 -0.41512,0.46396 -1.82667,1.32778 -1.43703,1.64655 0.99965,-0.74709 2.16479,-0.20775 3.17351,0.0358 1.13327,0.97208 -0.7695,1.66177 -1.40739,2.19644 -0.72103,0.3951 -1.48929,0.73257 -1.53088,1.60897 -0.81354,-0.32254 0.12422,1.14184 0.65017,1.27543 0.43315,0.20734 0.91104,0.44698 0.50741,-0.12798 0.30174,-0.53571 1.10164,0.28036 0.4594,-0.4453 0.0479,-1.20018 1.96828,-0.76448 2.64659,-1.55994 1.00428,-0.71951 -0.0847,-1.22662 -0.85018,-1.0046 -0.50182,0.06 -1.07873,-0.43882 -0.22366,-0.39397 0.83328,-0.14122 2.91477,0.25377 1.94632,1.4131 -1.06336,0.52011 -1.39764,1.49535 -1.68824,2.5366 -0.64436,0.88909 0.0567,1.65993 0.51433,2.21278 -0.0755,1.02463 -0.9696,1.82839 -1.60005,2.58943 -0.34214,0.34256 -1.17179,0.99352 -0.4029,0.26644 0.9274,-1.03713 1.17061,-2.97085 -0.091,-3.84667 -0.84429,-0.40075 -0.98573,0.46872 -0.61694,1.00935 -0.25867,0.8 -0.87545,1.54176 -0.22698,2.35526 -0.13692,1.16852 -1.3052,0.0978 -1.98479,0.71485 -0.94554,0.26803 -0.56905,0.54435 -1.5424,0.66888 1.68588,0.55388 2.10672,-0.0925 3.78947,-0.34726 -0.64558,0.75646 -1.80506,1.10898 -2.81213,1.13009 -2.03076,-0.002 -3.80891,-1.10663 -5.61502,-1.89118 -1.12924,-0.54753 -4.82644,-1.3203 -6.13091,-1.1211 -1.74005,0.0829 -1.83717,0.0547 -3.444,0.29301 -1.26438,-0.004 -1.42104,-0.75538 -0.51486,-2.02424 3.57949,-3.11534 6.69133,-7.03177 9.59613,-10.08739 z m -1.14004,8.46391 c -6.64532,4.78894 -3.32266,2.39447 0,0 z m -1.20322,0.0301 c -1.1412,-0.63874 -0.31307,-2.07894 0.17279,-2.89173 0.89169,-1.36972 2.86985,-1.39236 4.16156,-0.61159 1.54938,1.0298 2.07366,1.03412 2.06974,1.79612 -1.4368,0.34261 -2.55154,1.41885 -3.96896,1.80186 -0.79909,0.16878 -1.64355,0.0662 -2.43562,-0.0947 z m 3.57167,-0.77942 c 1.35312,-0.0504 0.6421,-1.87946 -0.27756,-2.06232 -2.33109,-0.63103 -2.8492,0.97005 -3.34013,1.46545 0.0455,1.37625 2.35919,1.3962 3.29693,0.77529 0.10657,-0.0601 -0.10797,0.0576 0,0 z m -3.61769,-0.59687 c -5.81249,5.68643 -2.90624,2.84321 0,0 z m 4.87351,-8.29404 c -9.0615,11.21579 -4.53075,5.60789 0,0 z"
|
||||
id="path3882"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" />
|
||||
</g>
|
||||
</g>
|
||||
<ellipse
|
||||
style="fill:#ffffff;fill-opacity:1;stroke-width:0.26458332"
|
||||
id="path4583"
|
||||
cx="185.16042"
|
||||
cy="234.62871"
|
||||
rx="0.74570084"
|
||||
ry="0.51021636" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer2"
|
||||
inkscape:label="Layer 3"
|
||||
style="display:inline">
|
||||
<path
|
||||
style="opacity:1;vector-effect:none;fill:#2cd3e3;fill-opacity:1;fill-rule:evenodd;stroke-width:0.43717846;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path884"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="7.5222836"
|
||||
sodipodi:cy="13.490612"
|
||||
sodipodi:rx="3.1455584"
|
||||
sodipodi:ry="1.9101746"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="6.1045858"
|
||||
sodipodi:open="true"
|
||||
d="M 10.667842,13.490612 A 3.1455584,1.9101746 0 0 1 7.6626857,15.398883 3.1455584,1.9101746 0 0 1 4.3892589,13.660964 3.1455584,1.9101746 0 0 1 7.102196,11.597548 3.1455584,1.9101746 0 0 1 10.617807,13.151267"
|
||||
transform="matrix(0.93916761,-0.34345916,0.51106856,0.85953995,0,0)" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Layer 4"
|
||||
style="display:inline">
|
||||
<circle
|
||||
style="opacity:1;vector-effect:none;fill:#b3ff80;fill-opacity:1;fill-rule:evenodd;stroke-width:0.38550848;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
|
||||
id="path907"
|
||||
cx="13.877442"
|
||||
cy="8.9755268"
|
||||
r="1.5" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.9 KiB |
BIN
Art/park_black_white.png
Normal file
After Width: | Height: | Size: 307 KiB |
BIN
Art/park_colors.png
Normal file
After Width: | Height: | Size: 477 KiB |
BIN
Art/park_white_black.png
Normal file
After Width: | Height: | Size: 358 KiB |
BIN
Art/screenshots/01_groupchats.png
Normal file
After Width: | Height: | Size: 623 KiB |
BIN
Art/screenshots/02_chats.png
Normal file
After Width: | Height: | Size: 502 KiB |
BIN
Art/screenshots/04_contacts.png
Normal file
After Width: | Height: | Size: 617 KiB |
BIN
Art/screenshots/ipad_01_groupchats.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
29
LICENSE
Normal file
|
@ -0,0 +1,29 @@
|
|||
BSD 2-Clause License
|
||||
|
||||
Copyright (c) 2024, Thilo Molitor
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
The views and conclusions contained in the software and documentation are those
|
||||
of the authors and should not be interpreted as representing official policies,
|
||||
either expressed or implied, of Anurodh Pokharel
|
27
Monal/.bartycrouch.toml
Normal file
|
@ -0,0 +1,27 @@
|
|||
[update]
|
||||
tasks = ["interfaces", "code", "normalize"]
|
||||
|
||||
[update.interfaces]
|
||||
paths = ["."]
|
||||
defaultToBase = true
|
||||
ignoreEmptyStrings = true
|
||||
unstripped = false
|
||||
|
||||
[update.code]
|
||||
codePaths = ["Classes", "shareSheet-iOS", "NotificationService"]
|
||||
localizablePaths = ["localization", "shareSheet-iOS/localization"]
|
||||
defaultToKeys = true
|
||||
additive = true
|
||||
unstripped = false
|
||||
plistArguments = true
|
||||
|
||||
[update.normalize]
|
||||
paths = ["."]
|
||||
sourceLocale = "base"
|
||||
harmonizeWithSource = true
|
||||
sortByKeys = true
|
||||
|
||||
[lint]
|
||||
paths = ["."]
|
||||
duplicateKeys = true
|
||||
emptyValues = true
|
|
@ -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
Monal/.swiftgen/bin/swiftgen
Executable file
29
Monal/.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
Monal/.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
Monal/.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
Monal/.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
|
125
Monal/.swiftlint.yml
Normal file
|
@ -0,0 +1,125 @@
|
|||
---
|
||||
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"
|
||||
- Classes
|
||||
- Pods
|
||||
- MonalUITests
|
||||
- MonalXMPPUnitTests
|
BIN
Monal/AlertSounds/alert1.aif
Normal file
BIN
Monal/AlertSounds/alert10.aif
Normal file
BIN
Monal/AlertSounds/alert11.aif
Normal file
BIN
Monal/AlertSounds/alert12.aif
Normal file
BIN
Monal/AlertSounds/alert2.aif
Normal file
BIN
Monal/AlertSounds/alert3.aif
Normal file
BIN
Monal/AlertSounds/alert4.aif
Normal file
BIN
Monal/AlertSounds/alert5.aif
Normal file
BIN
Monal/AlertSounds/alert6.aif
Normal file
BIN
Monal/AlertSounds/alert7.aif
Normal file
BIN
Monal/AlertSounds/alert8.aif
Normal file
BIN
Monal/AlertSounds/alert9.aif
Normal file
14
Monal/Alpha.shareSheet.entitlements
Normal file
|
@ -0,0 +1,14 @@
|
|||
<?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>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.monalalpha</string>
|
||||
</array>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
BIN
Monal/CallSounds/busy.wav
Normal file
BIN
Monal/CallSounds/error.wav
Normal file
BIN
Monal/CallSounds/ringing.wav
Normal file
26
Monal/Classes/AESGcm.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// AESGcm.h
|
||||
// Monal
|
||||
//
|
||||
// Created by Anurodh Pokharel on 4/19/19.
|
||||
// Copyright © 2019 Monal.im. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "MLEncryptedPayload.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface AESGcm : NSObject
|
||||
/**
|
||||
key size should be 16 or 32
|
||||
*/
|
||||
+(MLEncryptedPayload* _Nullable) encrypt:(NSData*) body keySize:(int) keySize;
|
||||
+(MLEncryptedPayload* _Nullable) encrypt:(NSData*) body withKey:(NSData*) gcmKey;
|
||||
+(NSData* _Nullable) decrypt:(NSData *)body withKey:(NSData *) key andIv:(NSData *)iv withAuth:(NSData * _Nullable) auth;
|
||||
+(NSData* _Nullable) genIV;
|
||||
+(NSData* _Nullable) genKey:(int) keySize;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
71
Monal/Classes/AESGcm.m
Normal file
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// AESGcm.m
|
||||
// Monal
|
||||
//
|
||||
// Created by Anurodh Pokharel on 4/19/19.
|
||||
// Copyright © 2019 Monal.im. All rights reserved.
|
||||
//
|
||||
|
||||
#import "MLConstants.h"
|
||||
#import "AESGcm.h"
|
||||
#import <monalxmpp/monalxmpp-Swift.h>
|
||||
|
||||
@class MLCrypto;
|
||||
|
||||
@implementation AESGcm
|
||||
|
||||
+(MLEncryptedPayload*) encrypt:(NSData*) body keySize:(int) keySize
|
||||
{
|
||||
NSData* gcmKey = [self genKey:keySize];
|
||||
if(!gcmKey)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
return [self encrypt:body withKey:gcmKey];
|
||||
}
|
||||
|
||||
+(MLEncryptedPayload*) encrypt:(NSData*) body withKey:(NSData*) gcmKey
|
||||
{
|
||||
MLCrypto* crypto = [MLCrypto new];
|
||||
EncryptedPayload* payload = [crypto encryptGCMWithKey:gcmKey decryptedContent:body];
|
||||
if(payload == nil)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
NSMutableData* combinedKey = [NSMutableData dataWithData:gcmKey];
|
||||
[combinedKey appendData:payload.tag];
|
||||
if(combinedKey == nil)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
return [[MLEncryptedPayload alloc] initWithBody:payload.body key:combinedKey iv:payload.iv authTag:payload.tag];
|
||||
}
|
||||
|
||||
+(NSData*) genIV
|
||||
{
|
||||
MLCrypto* crypto = [MLCrypto new];
|
||||
return [crypto genIV];
|
||||
}
|
||||
|
||||
+(NSData*) genKey:(int) keySize
|
||||
{
|
||||
uint8_t randomBytes[keySize];
|
||||
if(SecRandomCopyBytes(kSecRandomDefault, keySize, randomBytes) != 0)
|
||||
return nil;
|
||||
return [[NSData alloc] initWithBytes:randomBytes length:keySize];
|
||||
}
|
||||
|
||||
+(NSData*) decrypt:(NSData*) body withKey:(NSData*) key andIv:(NSData*) iv withAuth:(NSData* _Nullable) auth
|
||||
{
|
||||
MLCrypto* crypto = [MLCrypto new];
|
||||
|
||||
NSMutableData* combined = [NSMutableData new];
|
||||
[combined appendData:iv];
|
||||
[combined appendData:body];
|
||||
[combined appendData:auth]; //if auth is nil assume it already was apended to body
|
||||
|
||||
NSData* toReturn = [crypto decryptGCMWithKey:key encryptedContent:combined];
|
||||
return toReturn;
|
||||
}
|
||||
|
||||
@end
|
594
Monal/Classes/AVCallUI.swift
Normal file
|
@ -0,0 +1,594 @@
|
|||
//
|
||||
// AVCallUI.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Thilo Molitor on 20.12.22.
|
||||
// Copyright © 2021 Monal.im. All rights reserved.
|
||||
//
|
||||
import WebRTC
|
||||
import AVFoundation
|
||||
import CallKit
|
||||
import AVKit
|
||||
|
||||
struct VideoView: UIViewRepresentable {
|
||||
var renderer: RTCMTLVideoView
|
||||
|
||||
init(renderer: RTCMTLVideoView) {
|
||||
self.renderer = renderer
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> RTCMTLVideoView {
|
||||
return self.renderer
|
||||
}
|
||||
|
||||
func updateUIView(_ renderer: RTCMTLVideoView, context: Context) {
|
||||
DDLogDebug("updateUIView called...")
|
||||
//do nothing
|
||||
}
|
||||
}
|
||||
|
||||
struct AVCallUI: View {
|
||||
@StateObject private var appDelegate: ObservableKVOWrapper<MonalAppDelegate>
|
||||
@StateObject private var call: ObservableKVOWrapper<MLCall>
|
||||
@StateObject private var contact: ObservableKVOWrapper<MLContact>
|
||||
@State private var showMicAlert = false
|
||||
@State private var showSecurityHelpAlert: MLCallEncryptionState? = nil
|
||||
@State private var controlsVisible = true
|
||||
@State private var localRendererLocation: CGPoint = CGPoint(
|
||||
x: UIScreen.main.bounds.size.width - (UIScreen.main.bounds.size.width/5.0/2.0 + 24.0),
|
||||
y: UIScreen.main.bounds.size.height/5.0/2.0 + 16.0
|
||||
)
|
||||
@State private var cameraPosition: AVCaptureDevice.Position = .front
|
||||
@State private var sendingVideo = true
|
||||
private var ringingPlayer: AVAudioPlayer!
|
||||
private var busyPlayer: AVAudioPlayer!
|
||||
private var errorPlayer: AVAudioPlayer!
|
||||
private var delegate: SheetDismisserProtocol
|
||||
private var formatter: DateComponentsFormatter
|
||||
private var localRenderer: RTCMTLVideoView
|
||||
private var remoteRenderer: RTCMTLVideoView
|
||||
|
||||
init(delegate: SheetDismisserProtocol, call: MLCall) {
|
||||
_call = StateObject(wrappedValue: ObservableKVOWrapper(call))
|
||||
_contact = StateObject(wrappedValue: ObservableKVOWrapper(call.contact))
|
||||
_appDelegate = StateObject(wrappedValue: ObservableKVOWrapper(UIApplication.shared.delegate as! MonalAppDelegate))
|
||||
self.delegate = delegate
|
||||
self.formatter = DateComponentsFormatter()
|
||||
self.formatter.allowedUnits = [.hour, .minute, .second]
|
||||
self.formatter.unitsStyle = .positional
|
||||
self.formatter.zeroFormattingBehavior = .pad
|
||||
|
||||
//use the complete screen for remote video
|
||||
self.remoteRenderer = RTCMTLVideoView(frame: UIScreen.main.bounds)
|
||||
self.remoteRenderer.videoContentMode = .scaleAspectFill
|
||||
|
||||
self.localRenderer = RTCMTLVideoView(frame: UIScreen.main.bounds)
|
||||
self.localRenderer.videoContentMode = .scaleAspectFill
|
||||
self.localRenderer.transform = CGAffineTransformMakeScale(-1.0, 1.0) //local video should be displayed as "mirrored"
|
||||
|
||||
self.ringingPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"ringing", withExtension:"wav", subdirectory:"CallSounds")!)
|
||||
self.busyPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"busy", withExtension:"wav", subdirectory:"CallSounds")!)
|
||||
self.errorPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"error", withExtension:"wav", subdirectory:"CallSounds")!)
|
||||
}
|
||||
|
||||
func maybeStartRenderer() {
|
||||
if MLCallType(rawValue:call.callType) == .video && MLCallState(rawValue:call.state) == .connected {
|
||||
DDLogInfo("Starting local and remote video renderers...")
|
||||
call.obj.startCaptureLocalVideo(withRenderer: self.localRenderer, andCameraPosition:cameraPosition)
|
||||
call.obj.renderRemoteVideo(withRenderer: self.remoteRenderer)
|
||||
}
|
||||
}
|
||||
|
||||
func handleStateChange(_ state:MLCallState, _ audioState:MLAudioState) {
|
||||
switch state {
|
||||
case .unknown:
|
||||
DDLogDebug("state: unknown")
|
||||
ringingPlayer.stop()
|
||||
busyPlayer.stop()
|
||||
errorPlayer.play()
|
||||
case .discovering:
|
||||
DDLogDebug("state: discovering")
|
||||
ringingPlayer.stop()
|
||||
busyPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
case .ringing:
|
||||
DDLogDebug("state: ringing")
|
||||
busyPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
ringingPlayer.play()
|
||||
case .connecting:
|
||||
DDLogDebug("state: connecting")
|
||||
ringingPlayer.stop()
|
||||
busyPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
case .reconnecting:
|
||||
DDLogDebug("state: reconnecting")
|
||||
ringingPlayer.stop()
|
||||
busyPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
case .connected:
|
||||
DDLogDebug("state: connected")
|
||||
maybeStartRenderer()
|
||||
case .finished:
|
||||
DDLogDebug("state: finished: \(String(describing:call.finishReason as NSNumber))")
|
||||
//check audio state before trying to play anything (if we are still in state .call,
|
||||
//callkit will deactivate this audio session shortly, stopping our players)
|
||||
if audioState == .normal {
|
||||
switch MLCallFinishReason(rawValue:call.finishReason) {
|
||||
case .unknown:
|
||||
DDLogDebug("state: finished: unknown")
|
||||
ringingPlayer.stop()
|
||||
busyPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
case .connectivityError:
|
||||
DDLogDebug("state: finished: connectivityError")
|
||||
ringingPlayer.stop()
|
||||
busyPlayer.stop()
|
||||
errorPlayer.play()
|
||||
case .securityError:
|
||||
DDLogDebug("state: finished: securityError")
|
||||
ringingPlayer.stop()
|
||||
busyPlayer.stop()
|
||||
errorPlayer.play()
|
||||
case .unanswered:
|
||||
DDLogDebug("state: finished: unanswered")
|
||||
ringingPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
busyPlayer.play()
|
||||
case .retracted:
|
||||
DDLogDebug("state: finished: retracted")
|
||||
ringingPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
busyPlayer.play()
|
||||
case .rejected:
|
||||
DDLogDebug("state: finished: rejected")
|
||||
ringingPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
busyPlayer.play()
|
||||
case .declined:
|
||||
DDLogDebug("state: finished: declined")
|
||||
ringingPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
busyPlayer.play()
|
||||
case .error:
|
||||
DDLogDebug("state: finished: error")
|
||||
ringingPlayer.stop()
|
||||
busyPlayer.stop()
|
||||
errorPlayer.play()
|
||||
// case .normal:
|
||||
// case .answeredElsewhere:
|
||||
default:
|
||||
DDLogDebug("state: finished: default")
|
||||
ringingPlayer.stop()
|
||||
busyPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
}
|
||||
}
|
||||
default:
|
||||
DDLogDebug("state: default")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.background
|
||||
.ignoresSafeArea()
|
||||
|
||||
if MLCallType(rawValue:call.callType) == .video && MLCallState(rawValue:call.state) == .connected {
|
||||
VideoView(renderer:self.remoteRenderer)
|
||||
|
||||
ZStack {
|
||||
VideoView(renderer:self.localRenderer)
|
||||
//this will sometimes only honor the width and ignore the height
|
||||
.frame(width: UIScreen.main.bounds.size.width/5.0, height: UIScreen.main.bounds.size.height/5.0)
|
||||
|
||||
if controlsVisible {
|
||||
Button(action: {
|
||||
if cameraPosition == .front {
|
||||
cameraPosition = .back
|
||||
} else {
|
||||
cameraPosition = .front
|
||||
}
|
||||
call.obj.stopCaptureLocalVideo()
|
||||
maybeStartRenderer()
|
||||
}, label: {
|
||||
Image(systemName: "arrow.triangle.2.circlepath.camera.fill")
|
||||
.resizable()
|
||||
.frame(width: 32.0, height: 32.0)
|
||||
.foregroundColor(.primary)
|
||||
})
|
||||
}
|
||||
}
|
||||
.position(localRendererLocation)
|
||||
.gesture(DragGesture().onChanged { value in
|
||||
self.localRendererLocation = value.location
|
||||
})
|
||||
.onTapGesture(count: 2) {
|
||||
if sendingVideo {
|
||||
call.obj.hideVideo()
|
||||
} else {
|
||||
call.obj.showVideo()
|
||||
}
|
||||
sendingVideo = !sendingVideo
|
||||
}
|
||||
}
|
||||
|
||||
if MLCallType(rawValue:call.callType) == .audio ||
|
||||
(MLCallType(rawValue:call.callType) == .video && (MLCallState(rawValue:call.state) != .connected || controlsVisible)) {
|
||||
VStack {
|
||||
Group {
|
||||
Spacer().frame(height: 24)
|
||||
|
||||
HStack(alignment: .top) {
|
||||
Spacer().frame(width:20)
|
||||
|
||||
VStack {
|
||||
Spacer().frame(height: 8)
|
||||
switch MLCallDirection(rawValue:call.direction) {
|
||||
case .incoming:
|
||||
Image(systemName: "phone.arrow.down.left")
|
||||
.resizable()
|
||||
.frame(width: 20.0, height: 20.0)
|
||||
.foregroundColor(.primary)
|
||||
case .outgoing:
|
||||
Image(systemName: "phone.arrow.up.right")
|
||||
.resizable()
|
||||
.frame(width: 20.0, height: 20.0)
|
||||
.foregroundColor(.primary)
|
||||
default: //should never be reached
|
||||
Text("")
|
||||
}
|
||||
}
|
||||
|
||||
VStack {
|
||||
Spacer().frame(height: 8)
|
||||
Button(action: {
|
||||
//show dialog explaining different encryption states
|
||||
self.showSecurityHelpAlert = MLCallEncryptionState(rawValue:call.encryptionState)
|
||||
}, label: {
|
||||
switch MLCallEncryptionState(rawValue:call.encryptionState) {
|
||||
case .unknown:
|
||||
Text("")
|
||||
case .clear:
|
||||
Spacer().frame(width: 10)
|
||||
Image(systemName: "xmark.shield.fill")
|
||||
.resizable()
|
||||
.frame(width: 20.0, height: 20.0)
|
||||
.foregroundColor(.red)
|
||||
case .toFU:
|
||||
Spacer().frame(width: 10)
|
||||
Image(systemName: "checkmark.shield.fill")
|
||||
.resizable()
|
||||
.frame(width: 20.0, height: 20.0)
|
||||
.foregroundColor(.yellow)
|
||||
case .trusted:
|
||||
Spacer().frame(width: 10)
|
||||
Image(systemName: "checkmark.shield.fill")
|
||||
.resizable()
|
||||
.frame(width: 20.0, height: 20.0)
|
||||
.foregroundColor(.green)
|
||||
default: //should never be reached
|
||||
Text("")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(contact.contactDisplayName as String)
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
VStack {
|
||||
Spacer().frame(height: 8)
|
||||
Button(action: {
|
||||
if let activeChats = self.appDelegate.obj.activeChats {
|
||||
//make sure we don't animate anything
|
||||
activeChats.dismissCompleteViewChain(withAnimation: false) {
|
||||
activeChats.presentChat(with:self.contact.obj)
|
||||
}
|
||||
} else {
|
||||
//self.delegate.dismissWithoutAnimation()
|
||||
unreachable("active chats should always be accessible from AVCallUI!")
|
||||
}
|
||||
}, label: {
|
||||
Image(systemName: "text.bubble")
|
||||
.resizable()
|
||||
.frame(width: 28.0, height: 28.0)
|
||||
.foregroundColor(.primary)
|
||||
})
|
||||
}
|
||||
|
||||
Spacer().frame(width:20)
|
||||
}
|
||||
|
||||
Spacer().frame(height: 16)
|
||||
|
||||
//this is needed because ObservableKVOWrapper somehow extracts an NSNumber? from it's wrapped object
|
||||
//which results in a runtime error when trying to cast NSNumber? to MLCallState
|
||||
switch MLCallState(rawValue:call.state) {
|
||||
case .discovering:
|
||||
Text("Discovering devices...")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .ringing:
|
||||
Text("Ringing...")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .connecting:
|
||||
Text("Connecting...")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .reconnecting:
|
||||
Text("Reconnecting...")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .connected:
|
||||
Text("Connected: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .finished:
|
||||
switch MLCallFinishReason(rawValue:call.finishReason) {
|
||||
case .unknown:
|
||||
Text("Call ended for an unknown reason")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .normal:
|
||||
if call.wasConnectedOnce {
|
||||
Text("Call ended, duration: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
} else {
|
||||
Text("Call ended")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
case .connectivityError:
|
||||
if call.wasConnectedOnce {
|
||||
Text("Call ended: connection failed\nDuration: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
} else {
|
||||
Text("Call ended: connection failed")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
case .securityError:
|
||||
Text("Call ended: couldn't establish encryption")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .unanswered:
|
||||
Text("Call was not answered")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .answeredElsewhere:
|
||||
Text("Call ended: answered with other device")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .retracted:
|
||||
//this will only be displayed for timer-induced retractions,
|
||||
//reflect that in our text instead of using some generic "hung up"
|
||||
//Text("Call ended: hung up")
|
||||
Text("Call ended: remote busy")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .rejected:
|
||||
Text("Call ended: remote busy")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .declined:
|
||||
Text("Call ended: declined")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
case .error:
|
||||
Text("Call ended: application error")
|
||||
.bold()
|
||||
.foregroundColor(.primary)
|
||||
default: //should never be reached
|
||||
Text("")
|
||||
}
|
||||
default: //should never be reached
|
||||
Text("")
|
||||
}
|
||||
|
||||
Spacer().frame(height: 48)
|
||||
|
||||
if MLCallType(rawValue:call.callType) == .audio || MLCallState(rawValue:call.state) != .connected {
|
||||
Image(uiImage: contact.avatar)
|
||||
.resizable()
|
||||
.frame(minWidth: 100, idealWidth: 150, maxWidth: 200, minHeight: 100, idealHeight: 150, maxHeight: 200, alignment: .center)
|
||||
.scaledToFit()
|
||||
.shadow(radius: 7)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
|
||||
if MLCallState(rawValue:call.state) == .finished {
|
||||
HStack() {
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
self.delegate.dismissWithoutAnimation()
|
||||
if let activeChats = self.appDelegate.obj.activeChats {
|
||||
activeChats.call(contact.obj, with:MLCallType(rawValue:call.callType)!)
|
||||
}
|
||||
}) {
|
||||
Image(systemName: "arrow.clockwise.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 64.0, height: 64.0)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, .green)
|
||||
.shadow(radius: 7)
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
Spacer().frame(width: 64)
|
||||
|
||||
Button(action: {
|
||||
delegate.dismissWithoutAnimation()
|
||||
}) {
|
||||
Image(systemName: "x.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 64.0, height: 64.0)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, .red)
|
||||
.shadow(radius: 7)
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
Spacer()
|
||||
}
|
||||
} else {
|
||||
HStack() {
|
||||
Spacer()
|
||||
|
||||
if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting {
|
||||
Button(action: {
|
||||
call.muted = !call.muted
|
||||
}) {
|
||||
Image(systemName: "mic.slash.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 64.0, height: 64.0)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(call.muted ? .black : .white, call.muted ? .white : .black)
|
||||
.shadow(radius: 7)
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
Spacer().frame(width: 32)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
call.obj.end()
|
||||
self.delegate.dismissWithoutAnimation()
|
||||
}) {
|
||||
Image(systemName: "phone.down.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 64.0, height: 64.0)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, .red)
|
||||
.shadow(radius: 7)
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting {
|
||||
Spacer().frame(width: 32)
|
||||
Button(action: {
|
||||
call.speaker = !call.speaker
|
||||
}) {
|
||||
Image(systemName: "speaker.wave.2.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 64.0, height: 64.0)
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(call.speaker ? .black : .white, call.speaker ? .white : .black)
|
||||
.shadow(radius: 7)
|
||||
}
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer().frame(height: 32)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture(count: 1) {
|
||||
controlsVisible = !controlsVisible
|
||||
}
|
||||
.alert(isPresented: $showMicAlert) {
|
||||
Alert(
|
||||
title: Text("Missing permission"),
|
||||
message: Text("You need to grant microphone access in iOS Settings-> Privacy-> Microphone, if you want that others can hear you."),
|
||||
dismissButton: .default(Text("OK"))
|
||||
)
|
||||
}
|
||||
.richAlert(isPresented:$showSecurityHelpAlert, title:Text("Call security help")) {
|
||||
VStack(alignment: .leading) {
|
||||
HStack {
|
||||
Image(systemName: "xmark.shield.fill")
|
||||
.resizable()
|
||||
.frame(width: 20.0, height: 20.0)
|
||||
.foregroundColor(.red)
|
||||
Spacer().frame(width: 10)
|
||||
Text("Red x-mark shield:")
|
||||
}.font(Font.body.weight(showSecurityHelpAlert == .clear ? .heavy : .medium))
|
||||
Text("This means your call is encrypted, but the remote party could not be verified using OMEMO encryption.\nYour or the callee's XMPP server could possibly Man-In-The-Middle you.")
|
||||
Spacer().frame(height: 20)
|
||||
|
||||
HStack {
|
||||
Image(systemName: "checkmark.shield.fill")
|
||||
.resizable()
|
||||
.frame(width: 20.0, height: 20.0)
|
||||
.foregroundColor(.yellow)
|
||||
Spacer().frame(width: 10)
|
||||
Text("Yellow checkmark shield:")
|
||||
}.font(Font.body.weight(showSecurityHelpAlert == .toFU ? .heavy : .medium))
|
||||
Text("This means your call is encrypted and the remote party was verified using OMEMO encryption.\nBut since you did not manually verify the callee's OMEMO fingerprints, your or the callee's XMPP server could possibly have inserted their own OMEMO keys to Man-In-The-Middle you.")
|
||||
Spacer().frame(height: 20)
|
||||
|
||||
HStack {
|
||||
Image(systemName: "checkmark.shield.fill")
|
||||
.resizable()
|
||||
.frame(width: 20.0, height: 20.0)
|
||||
.foregroundColor(.green)
|
||||
Spacer().frame(width: 10)
|
||||
Text("Green checkmark shield:")
|
||||
}.font(Font.body.weight(showSecurityHelpAlert == .trusted ? .heavy : .medium))
|
||||
Text("This means your call is encrypted and the remote party was verified using OMEMO encryption.\nYou manually verified the used OMEMO keys and no Man-In-The-Middle can take place.")
|
||||
Spacer().frame(height: 20)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
//force portrait mode and lock ui there
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
||||
self.appDelegate.obj.orientationLock = .portrait
|
||||
UIApplication.shared.isIdleTimerDisabled = true
|
||||
|
||||
self.ringingPlayer.numberOfLoops = -1
|
||||
self.busyPlayer.numberOfLoops = -1
|
||||
self.errorPlayer.numberOfLoops = -1
|
||||
|
||||
//ask for mic permissions
|
||||
AVAudioSession.sharedInstance().requestRecordPermission { granted in
|
||||
if !granted {
|
||||
showMicAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
maybeStartRenderer()
|
||||
}
|
||||
.onDisappear {
|
||||
//allow all orientations again
|
||||
self.appDelegate.obj.orientationLock = .all
|
||||
UIApplication.shared.isIdleTimerDisabled = false
|
||||
|
||||
ringingPlayer.stop()
|
||||
busyPlayer.stop()
|
||||
errorPlayer.stop()
|
||||
|
||||
if MLCallType(rawValue:call.callType) == .video {
|
||||
call.obj.stopCaptureLocalVideo()
|
||||
}
|
||||
}
|
||||
.onChange(of: MLCallState(rawValue:call.state)) { state in
|
||||
DDLogVerbose("call state changed: \(String(describing:call.state as NSNumber))")
|
||||
handleStateChange(call.obj.state, appDelegate.obj.audioState)
|
||||
}
|
||||
.onChange(of: MLAudioState(rawValue:appDelegate.audioState)) { audioState in
|
||||
DDLogVerbose("audioState changed: \(String(describing:appDelegate.audioState as NSNumber))")
|
||||
handleStateChange(call.obj.state, appDelegate.obj.audioState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AVCallUI_Previews: PreviewProvider {
|
||||
static var delegate = SheetDismisserProtocol()
|
||||
static var previews: some View {
|
||||
AVCallUI(delegate:delegate, call:MLCall.makeDummyCall(0))
|
||||
}
|
||||
}
|
20
Monal/Classes/AccountListController.h
Normal file
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// AccountListController.h
|
||||
// Monal
|
||||
//
|
||||
// Created by Anurodh Pokharel on 6/14/13.
|
||||
//
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Monal-Swift.h>
|
||||
#import "MLSwitchCell.h"
|
||||
|
||||
@interface AccountListController : UITableViewController
|
||||
|
||||
-(NSUInteger) getAccountNum;
|
||||
-(NSNumber*) getAccountIDByIndex:(NSUInteger) index;
|
||||
-(void) setupAccountsView;
|
||||
-(void) refreshAccountList;
|
||||
-(void) initContactCell:(MLSwitchCell*) cell forAccNo:(NSUInteger) accNo;
|
||||
@end
|
114
Monal/Classes/AccountListController.m
Normal file
|
@ -0,0 +1,114 @@
|
|||
//
|
||||
// AccountListController.m
|
||||
// Monal
|
||||
//
|
||||
// Created by Anurodh Pokharel on 6/14/13.
|
||||
//
|
||||
//
|
||||
|
||||
#import "AccountListController.h"
|
||||
#import "DataLayer.h"
|
||||
#import "MLXMPPManager.h"
|
||||
#import "HelperTools.h"
|
||||
|
||||
@interface AccountListController ()
|
||||
@property (nonatomic, strong) NSDateFormatter* uptimeFormatter;
|
||||
|
||||
@property (nonatomic, strong) NSIndexPath* selected; // User-selected account - needed for segue
|
||||
@property (nonatomic, strong) UITableView* accountsTable;
|
||||
@property (nonatomic, strong) NSArray<NSDictionary*>* accountList;
|
||||
|
||||
@end
|
||||
|
||||
@implementation AccountListController
|
||||
|
||||
|
||||
#pragma mark View life cycle
|
||||
- (void) setupAccountsView
|
||||
{
|
||||
// Do any additional setup after loading the view.
|
||||
self.accountsTable.backgroundView = nil;
|
||||
self.accountsTable = self.tableView;
|
||||
self.accountsTable.delegate = self;
|
||||
self.accountsTable.dataSource = self;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.accountsTable reloadData];
|
||||
});
|
||||
|
||||
self.uptimeFormatter = [NSDateFormatter new];
|
||||
self.uptimeFormatter.dateStyle = NSDateFormatterShortStyle;
|
||||
self.uptimeFormatter.timeStyle = NSDateFormatterShortStyle;
|
||||
self.uptimeFormatter.doesRelativeDateFormatting = YES;
|
||||
self.uptimeFormatter.locale = [NSLocale currentLocale];
|
||||
self.uptimeFormatter.timeZone = [NSTimeZone systemTimeZone];
|
||||
|
||||
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
|
||||
[nc addObserver:self selector:@selector(refreshAccountList) name:kMonalAccountStatusChanged object:nil];
|
||||
}
|
||||
|
||||
-(void) dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
-(NSUInteger) getAccountNum
|
||||
{
|
||||
return self.accountList.count;
|
||||
}
|
||||
|
||||
-(NSNumber*) getAccountIDByIndex:(NSUInteger) index
|
||||
{
|
||||
NSNumber* result = [[self.accountList objectAtIndex: index] objectForKey:@"account_id"];
|
||||
MLAssert(result != nil, @"getAccountIDByIndex, result should not be nil");
|
||||
return result;
|
||||
}
|
||||
|
||||
-(void) refreshAccountList
|
||||
{
|
||||
NSArray* accountList = [[DataLayer sharedInstance] accountList];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.accountList = accountList;
|
||||
[self.accountsTable reloadData];
|
||||
});
|
||||
}
|
||||
|
||||
-(void) initContactCell:(MLSwitchCell*) cell forAccNo:(NSUInteger) accNo
|
||||
{
|
||||
[cell initTapCell:@"\n\n"];
|
||||
NSDictionary* account = [self.accountList objectAtIndex:accNo];
|
||||
MLAssert(account != nil, ([NSString stringWithFormat:@"Expected non nil account in row %lu", (unsigned long)accNo]));
|
||||
if([(NSString*)[account objectForKey:@"domain"] length] > 0) {
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"%@@%@", [[self.accountList objectAtIndex:accNo] objectForKey:@"username"],
|
||||
[[self.accountList objectAtIndex:accNo] objectForKey:@"domain"]];
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.textLabel.text = [[self.accountList objectAtIndex:accNo] objectForKey:@"username"];
|
||||
}
|
||||
|
||||
UIImageView* accessory = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
|
||||
|
||||
if([[account objectForKey:@"enabled"] boolValue] == YES)
|
||||
{
|
||||
cell.imageView.image = [UIImage systemImageNamed:@"checkmark.circle"];
|
||||
if([[MLXMPPManager sharedInstance] isAccountForIdConnected:[[self.accountList objectAtIndex:accNo] objectForKey:@"account_id"]])
|
||||
{
|
||||
accessory.image = [UIImage imageNamed:@"Connected"];
|
||||
cell.accessoryView = accessory;
|
||||
}
|
||||
else
|
||||
{
|
||||
accessory.image = [UIImage imageNamed:@"Disconnected"];
|
||||
cell.accessoryView = accessory;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
cell.imageView.image = [UIImage systemImageNamed:@"circle"];
|
||||
accessory.image = nil;
|
||||
cell.accessoryView = accessory;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
66
Monal/Classes/AccountPicker.swift
Normal file
|
@ -0,0 +1,66 @@
|
|||
//
|
||||
// AccountPicker.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Thilo Molitor on 20.01.23.
|
||||
// Copyright © 2023 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
struct AccountPicker: View {
|
||||
let contacts: [MLContact]
|
||||
let callType: MLCallType
|
||||
#if IS_ALPHA
|
||||
let appLogoId = "AlphaAppLogo"
|
||||
#elseif IS_QUICKSY
|
||||
let appLogoId = "QuicksyAppLogo"
|
||||
#else
|
||||
let appLogoId = "AppLogo"
|
||||
#endif
|
||||
|
||||
init(contacts:[MLContact], callType: MLCallType) {
|
||||
self.contacts = contacts
|
||||
self.callType = callType
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
//ScrollView {
|
||||
VStack {
|
||||
HStack () {
|
||||
Image(decorative: appLogoId)
|
||||
.resizable()
|
||||
.frame(width: CGFloat(120), height: CGFloat(120), alignment: .center)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
.padding()
|
||||
Text("You are trying to call '\(contacts.first!.contactDisplayName)' (\(contacts.first!.contactJid)), but this contact can be reached using different accounts. Please select the account you want to place the outgoing call with.")
|
||||
.padding()
|
||||
.padding(.leading, -16.0)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color(UIColor.systemBackground))
|
||||
|
||||
List {
|
||||
ForEach(contacts) { contact in
|
||||
if let accountEntry = DataLayer.sharedInstance().details(forAccount:contact.accountID) {
|
||||
let accountJid = "\(accountEntry["username"] ?? "<unknown>" as NSString)@\(accountEntry["domain"] ?? "<unknown>" as NSString)"
|
||||
let accountContact = MLContact.createContact(fromJid:accountJid, andAccountID:accountEntry["account_id"] as! NSNumber)
|
||||
Button {
|
||||
(UIApplication.shared.delegate as! MonalAppDelegate).activeChats!.call(contact, with:callType)
|
||||
} label: {
|
||||
ContactEntry(contact:ObservableKVOWrapper(accountContact), selfnotesPrefix:false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
//}
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.navigationBarTitle(Text("Account Picker"))
|
||||
}
|
||||
}
|
||||
|
||||
struct AccountPicker_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
AccountPicker(contacts:[MLContact.makeDummyContact(0)], callType:.audio)
|
||||
}
|
||||
}
|
64
Monal/Classes/ActiveChatsViewController.h
Normal file
|
@ -0,0 +1,64 @@
|
|||
//
|
||||
// ActiveChatsViewController.h
|
||||
// Monal
|
||||
//
|
||||
// Created by Anurodh Pokharel on 6/14/13.
|
||||
//
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "MLConstants.h"
|
||||
#import "MLContact.h"
|
||||
#import "MLCall.h"
|
||||
#import <DZNEmptyDataSet/UIScrollView+EmptyDataSet.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@class UIHostingControllerWorkaround;
|
||||
@class chatViewController;
|
||||
@class MLCall;
|
||||
|
||||
@interface SizeClassWrapper: NSObject
|
||||
@property (atomic) UIUserInterfaceSizeClass horizontal;
|
||||
@end
|
||||
|
||||
@interface ActiveChatsViewController : UITableViewController <DZNEmptyDataSetSource, DZNEmptyDataSetDelegate>
|
||||
|
||||
@property (nonatomic, strong) UITableView* chatListTable;
|
||||
@property (nonatomic, weak) IBOutlet UIBarButtonItem* settingsButton;
|
||||
@property (weak, nonatomic) IBOutlet UIBarButtonItem* spinnerButton;
|
||||
@property (nonatomic, weak) IBOutlet UIBarButtonItem* composeButton;
|
||||
@property (nonatomic, strong) UIActivityIndicatorView* spinner;
|
||||
@property (atomic, strong) SizeClassWrapper* sizeClass;
|
||||
@property (atomic, readonly) chatViewController* _Nullable currentChatView;
|
||||
|
||||
-(void) showCallContactNotFoundAlert:(NSString*) jid;
|
||||
-(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender;
|
||||
-(void) callContact:(MLContact*) contact withCallType:(MLCallType) callType;
|
||||
-(void) presentAccountPickerForContacts:(NSArray<MLContact*>*) contacts andCallType:(MLCallType) callType;
|
||||
-(void) presentCall:(MLCall*) call;
|
||||
-(void) presentChatWithContact:(MLContact* _Nullable) contact;
|
||||
-(void) presentChatWithContact:(MLContact* _Nullable) contact andCompletion:(monal_id_block_t _Nullable) completion;
|
||||
-(void) presentSplitPlaceholder;
|
||||
-(void) refreshDisplay;
|
||||
|
||||
-(void) showContacts;
|
||||
-(void) deleteConversation;
|
||||
-(void) showSettings;
|
||||
-(void) showGeneralSettings;
|
||||
-(void) prependGeneralSettings;
|
||||
-(void) showNotificationSettings;
|
||||
-(void) showDetails;
|
||||
-(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host withToken:(NSString* _Nullable) token usingCompletion:(monal_id_block_t _Nullable) callback;
|
||||
-(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) preauthToken prefillAccount:(xmpp* _Nullable) account andOmemoFingerprints:(NSDictionary* _Nullable) fingerprints;
|
||||
-(void) showAddContact;
|
||||
-(void) sheetDismissed;
|
||||
|
||||
-(void) segueToIntroScreensIfNeeded;
|
||||
-(void) resetViewQueue;
|
||||
-(void) dismissCompleteViewChainWithAnimation:(BOOL) animation andCompletion:(monal_void_block_t _Nullable) completion;
|
||||
-(void) updateSizeClass;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
1558
Monal/Classes/ActiveChatsViewController.m
Executable file
345
Monal/Classes/AddContactMenu.swift
Normal file
|
@ -0,0 +1,345 @@
|
|||
//
|
||||
// AddContactMenu.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Jan on 27.10.22.
|
||||
// Copyright © 2022 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
import MobileCoreServices
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
struct AddContactMenu: View {
|
||||
var delegate: SheetDismisserProtocol
|
||||
private static let jidFaultyPattern = "^([^@]+@)?.+(\\..{2,})?$"
|
||||
|
||||
@State private var enabledAccounts: [xmpp]
|
||||
@State private var selectedAccount: Int
|
||||
@State private var scannedFingerprints: [NSNumber: Data]? = nil
|
||||
@State private var importScannedFingerprints: Bool = false
|
||||
@State private var toAdd: String = ""
|
||||
|
||||
@State private var showInvitationError = false
|
||||
@State private var showAlert = false
|
||||
// note: dismissLabel is not accessed but defined at the .alert() section
|
||||
@State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close"))
|
||||
@State private var invitationResult: [String: AnyObject]? = nil
|
||||
|
||||
@StateObject private var overlay = LoadingOverlayState()
|
||||
|
||||
@State private var showQRCodeScanner = false
|
||||
@State private var success = false
|
||||
@State private var newContact: MLContact?
|
||||
|
||||
@State private var isEditingJid = false
|
||||
|
||||
private let dismissWithNewContact: (MLContact) -> Void
|
||||
private let preauthToken: String?
|
||||
|
||||
init(delegate: SheetDismisserProtocol, dismissWithNewContact: @escaping (MLContact) -> Void, prefillJid: String = "", preauthToken: String? = nil, prefillAccount: xmpp? = nil, omemoFingerprints: [NSNumber: Data]? = nil) {
|
||||
self.delegate = delegate
|
||||
self.dismissWithNewContact = dismissWithNewContact
|
||||
// self.toAdd = State(wrappedValue: prefillJid)
|
||||
toAdd = prefillJid
|
||||
self.preauthToken = preauthToken
|
||||
// only display omemo ui part if there are any fingerprints (the checks below test for nil, not for 0)
|
||||
if omemoFingerprints?.count ?? 0 > 0 {
|
||||
scannedFingerprints = omemoFingerprints
|
||||
}
|
||||
|
||||
let enabledAccounts = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp]
|
||||
self.enabledAccounts = enabledAccounts
|
||||
selectedAccount = enabledAccounts.first != nil ? 0 : -1
|
||||
if let prefillAccount = prefillAccount {
|
||||
for index in enabledAccounts.indices {
|
||||
if enabledAccounts[index].accountID.isEqual(to: prefillAccount.accountID) {
|
||||
selectedAccount = index
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: duplicate code from WelcomeLogIn.swift, maybe move to SwiftuiHelpers
|
||||
private var toAddEmptyAlert: Bool {
|
||||
alertPrompt.title = Text("No Empty Values!")
|
||||
alertPrompt.message = Text("Please make sure you have entered a valid jid.")
|
||||
return toAddEmpty
|
||||
}
|
||||
|
||||
private var toAddInvalidAlert: Bool {
|
||||
alertPrompt.title = Text("Invalid Credentials!")
|
||||
alertPrompt.message = Text("The jid you want to add should be in in the format user@domain.tld.")
|
||||
return toAddInvalid
|
||||
}
|
||||
|
||||
private func errorAlert(title: Text, message: Text = Text("")) {
|
||||
alertPrompt.title = title
|
||||
alertPrompt.message = message
|
||||
showAlert = true
|
||||
}
|
||||
|
||||
private func successAlert(title: Text, message: Text) {
|
||||
alertPrompt.title = title
|
||||
alertPrompt.message = message
|
||||
success = true // < dismiss entire view on close
|
||||
showAlert = true
|
||||
}
|
||||
|
||||
private var toAddEmpty: Bool {
|
||||
toAdd.isEmpty
|
||||
}
|
||||
|
||||
private var toAddInvalid: Bool {
|
||||
toAdd.range(of: AddContactMenu.jidFaultyPattern, options: .regularExpression) == nil
|
||||
}
|
||||
|
||||
func trustFingerprints(_ fingerprints: [NSNumber: Data]?, for jid: String, on account: xmpp) {
|
||||
// we don't untrust other devices not included in here, because conversations only exports its own fingerprint
|
||||
if let fingerprints = fingerprints {
|
||||
for (deviceId, fingerprint) in fingerprints {
|
||||
let address = SignalAddress(name: jid, deviceId: deviceId.int32Value)
|
||||
let knownDevices = Array(account.omemo.knownDevices(forAddressName: jid))
|
||||
if !knownDevices.contains(deviceId) {
|
||||
account.omemo.addIdentityManually(address, identityKey: fingerprint)
|
||||
assert(account.omemo.getIdentityFor(address) == fingerprint, "The stored and created fingerprint should match")
|
||||
}
|
||||
// trust device/fingerprint if fingerprints match
|
||||
let knownFingerprintHex = HelperTools.signalHexKey(with: account.omemo.getIdentityFor(address))
|
||||
let addedFingerprintHex = HelperTools.signalHexKey(with: fingerprint)
|
||||
if knownFingerprintHex.uppercased() == addedFingerprintHex.uppercased() {
|
||||
account.omemo.updateTrust(true, for: address)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addJid(jid: String) {
|
||||
let account = enabledAccounts[selectedAccount]
|
||||
let contact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID)
|
||||
if contact.isInRoster {
|
||||
newContact = contact
|
||||
// import omemo fingerprints as manually trusted, if requested
|
||||
trustFingerprints(importScannedFingerprints ? scannedFingerprints : [:], for: jid, on: account)
|
||||
// only alert of already known contact if we did not import the omemo fingerprints
|
||||
if !importScannedFingerprints || scannedFingerprints?.count ?? 0 == 0 {
|
||||
if enabledAccounts.count > 1 {
|
||||
success = true
|
||||
successAlert(title: Text("Already present"), message: Text("This contact is already in the contact list of the selected account"))
|
||||
} else {
|
||||
success = true
|
||||
successAlert(title: Text("Already present"), message: Text("This contact is already in your contact list"))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
showPromisingLoadingOverlay(overlay, headline: NSLocalizedString("Adding...", comment: ""), description: "") {
|
||||
account.checkJidType(jid)
|
||||
}.done { type in
|
||||
let type = type as! String
|
||||
if type == "account" {
|
||||
let contact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID)
|
||||
self.newContact = contact
|
||||
MLXMPPManager.sharedInstance().add(contact, withPreauthToken: preauthToken)
|
||||
// import omemo fingerprints as manually trusted, if requested
|
||||
trustFingerprints(self.importScannedFingerprints ? self.scannedFingerprints : [:], for: jid, on: account)
|
||||
successAlert(title: Text("Permission Requested"), message: Text("The new contact will be added to your contacts list when the person you've added has approved your request."))
|
||||
} else if type == "muc" {
|
||||
showPromisingLoadingOverlay(overlay, headlineView: Text("Adding Group/Channel..."), descriptionView: Text("")) {
|
||||
promisifyMucAction(account: account, mucJid: jid) {
|
||||
account.joinMuc(jid)
|
||||
}
|
||||
}.done { _ in
|
||||
self.newContact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID)
|
||||
successAlert(title: Text("Success!"), message: Text("Successfully joined group/channel \(jid)!"))
|
||||
}.catch { error in
|
||||
errorAlert(title: Text("Error entering group/channel!"), message: Text("\(String(describing: error))"))
|
||||
}
|
||||
}
|
||||
}.catch { error in
|
||||
errorAlert(title: Text("Error"), message: Text(error.localizedDescription))
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let account = enabledAccounts[selectedAccount]
|
||||
let splitJid = HelperTools.splitJid(account.connectionProperties.identity.jid)
|
||||
Form {
|
||||
if enabledAccounts.isEmpty {
|
||||
Text("Please make sure at least one account has connected before trying to add a contact or channel.")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
if DataLayer.sharedInstance().allContactRequests().count > 0 {
|
||||
ContactRequestsMenu()
|
||||
}
|
||||
|
||||
Section(header: Text("Contact and Group/Channel Jids are usually in the format: name@domain.tld")) {
|
||||
if enabledAccounts.count > 1 {
|
||||
Picker("Use account", selection: $selectedAccount) {
|
||||
ForEach(Array(self.enabledAccounts.enumerated()), id: \.element) { idx, account in
|
||||
Text(account.connectionProperties.identity.jid).tag(idx)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
|
||||
TextField(NSLocalizedString("Contact-, Group- or Channel-Jid", comment: "placeholder when adding jid"), text: $toAdd, onEditingChanged: { isEditingJid = $0 })
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocapitalization(.none)
|
||||
.autocorrectionDisabled()
|
||||
.keyboardType(.emailAddress)
|
||||
.addClearButton(isEditing: isEditingJid, text: $toAdd)
|
||||
.disabled(scannedFingerprints != nil)
|
||||
.foregroundColor(scannedFingerprints != nil ? .secondary : .primary)
|
||||
.onChange(of: toAdd) { _ in toAdd = toAdd.replacingOccurrences(of: " ", with: "") }
|
||||
|
||||
if scannedFingerprints != nil && !scannedFingerprints!.isEmpty {
|
||||
Section(header: Text("A contact was scanned through the QR code scanner")) {
|
||||
Toggle(isOn: $importScannedFingerprints) {
|
||||
Text("Import and trust OMEMO fingerprints from QR code")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if scannedFingerprints != nil {
|
||||
Button(action: {
|
||||
toAdd = ""
|
||||
importScannedFingerprints = true
|
||||
scannedFingerprints = nil
|
||||
}, label: {
|
||||
Text("Clear scanned contact")
|
||||
.foregroundColor(.red)
|
||||
})
|
||||
}
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
Button(action: {
|
||||
showAlert = toAddEmptyAlert || toAddInvalidAlert
|
||||
|
||||
if !showAlert {
|
||||
let jidComponents = HelperTools.splitJid(toAdd)
|
||||
if jidComponents["host"] == nil || jidComponents["host"]!.isEmpty {
|
||||
errorAlert(title: Text("Error"), message: Text("Something went wrong while parsing your input..."))
|
||||
showAlert = true
|
||||
return
|
||||
}
|
||||
// use the canonized jid from now on (lowercased, resource removed etc.)
|
||||
addJid(jid: jidComponents["user"]!)
|
||||
}
|
||||
}) {
|
||||
scannedFingerprints == nil ? Text("Add") : Text("Add scanned contact")
|
||||
}
|
||||
.disabled(toAddEmpty || toAddInvalid)
|
||||
.buttonStyle(MonalProminentButtonStyle())
|
||||
}
|
||||
}
|
||||
|
||||
if DataLayer.sharedInstance().allContactRequests().count == 0 {
|
||||
Section {
|
||||
ContactRequestsMenu()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.alert(isPresented: $showAlert) {
|
||||
Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton: .default(Text("Close"), action: {
|
||||
showAlert = false
|
||||
if self.success == true {
|
||||
if self.newContact != nil {
|
||||
self.dismissWithNewContact(newContact!)
|
||||
} else {
|
||||
self.delegate.dismiss()
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
.richAlert(isPresented: $invitationResult, title: Text("Invitation for \(splitJid["host"]!) created")) { data in
|
||||
VStack {
|
||||
Image(uiImage: createQrCode(value: data["landing"] as! String))
|
||||
.interpolation(.none)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
|
||||
if let expires = data["expires"] as? Date {
|
||||
Text("This invitation will expire on \(expires.formatted(date: .numeric, time: .shortened))")
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
} buttons: { data in
|
||||
Button(action: {
|
||||
UIPasteboard.general.setValue(data["landing"] as! String, forPasteboardType: UTType.utf8PlainText.identifier as String)
|
||||
invitationResult = nil
|
||||
}) {
|
||||
ShareLink("Share invitation link", item: URL(string: data["landing"] as! String)!)
|
||||
}
|
||||
Button(action: {
|
||||
invitationResult = nil
|
||||
}) {
|
||||
Text("Close")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showQRCodeScanner) {
|
||||
NavigationStack {
|
||||
MLQRCodeScanner(handleClose: {
|
||||
self.showQRCodeScanner = false
|
||||
})
|
||||
.navigationTitle("QR-Code Scanner")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar(content: {
|
||||
ToolbarItem(placement: .navigationBarLeading, content: {
|
||||
Button(action: {
|
||||
self.showQRCodeScanner = false
|
||||
}, label: {
|
||||
Text("Close")
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(Text("Add Contact or Channel"), displayMode: .inline)
|
||||
.toolbar(content: {
|
||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
||||
if account.connectionProperties.discoveredAdhocCommands["urn:xmpp:invite#invite"] != nil {
|
||||
Button(action: {
|
||||
DDLogVerbose("Trying to create invitation for: \(String(describing: splitJid["host"]!))")
|
||||
showLoadingOverlay(overlay, headline: NSLocalizedString("Creating invitation...", comment: ""))
|
||||
account.createInvitation(completion: {
|
||||
let result = $0 as! [String: AnyObject]
|
||||
DispatchQueue.main.async {
|
||||
hideLoadingOverlay(overlay)
|
||||
DDLogVerbose("Got invitation result: \(String(describing: result))")
|
||||
if result["success"] as! Bool == true {
|
||||
invitationResult = result
|
||||
} else {
|
||||
errorAlert(title: Text("Failed to create invitation for \(splitJid["host"]!)"), message: Text(result["error"] as! String))
|
||||
}
|
||||
}
|
||||
})
|
||||
}, label: {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
})
|
||||
}
|
||||
Button(action: {
|
||||
self.showQRCodeScanner = true
|
||||
}, label: {
|
||||
Image(systemName: "camera.fill")
|
||||
})
|
||||
}
|
||||
})
|
||||
.addLoadingOverlay(overlay)
|
||||
}
|
||||
}
|
||||
|
||||
struct AddContactMenu_Previews: PreviewProvider {
|
||||
static var delegate = SheetDismisserProtocol()
|
||||
static var previews: some View {
|
||||
AddContactMenu(delegate: delegate, dismissWithNewContact: { _ in
|
||||
})
|
||||
}
|
||||
}
|
100
Monal/Classes/BackgroundSettings.swift
Normal file
|
@ -0,0 +1,100 @@
|
|||
//
|
||||
// BackgroundSettings.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Thilo Molitor on 14.11.22.
|
||||
// Copyright © 2022 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
//swiftui is somehow needed to let the PhotosUI import succeed, even if it's already imported by SwiftuiHelpers.swift using @_exported
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
@ViewBuilder
|
||||
func title(contact: ObservableKVOWrapper<MLContact>?) -> some View {
|
||||
if let contact = contact {
|
||||
Text("Select a background to display behind conversations with \(contact.contactDisplayName as String)")
|
||||
} else {
|
||||
Text("Select a default background to display behind conversations.")
|
||||
}
|
||||
}
|
||||
|
||||
struct BackgroundSettings: View {
|
||||
@State private var selectedItem: PhotosPickerItem? = nil
|
||||
@State private var showingImagePicker = false
|
||||
@State private var inputImage: UIImage?
|
||||
let contact: ObservableKVOWrapper<MLContact>?
|
||||
|
||||
init(contact: ObservableKVOWrapper<MLContact>?) {
|
||||
self.contact = contact
|
||||
_inputImage = State(initialValue:MLImageManager.sharedInstance().getBackgroundFor(self.contact?.obj))
|
||||
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Form {
|
||||
Section(header:title(contact:contact)) {
|
||||
VStack(spacing: 20) {
|
||||
Spacer().frame(height: 0)
|
||||
|
||||
PhotosPicker(selection:$selectedItem, matching:.images, photoLibrary:.shared()) {
|
||||
if let inputImage = inputImage {
|
||||
HStack(alignment: .center) {
|
||||
Image(uiImage:inputImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
.addTopRight {
|
||||
Button(action: {
|
||||
self.inputImage = nil
|
||||
}, label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 32.0, height: 32.0)
|
||||
.accessibilityLabel(Text("Remove Background Image"))
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, .red)
|
||||
})
|
||||
.buttonStyle(.borderless)
|
||||
.offset(x: 12, y: -12)
|
||||
}
|
||||
} else {
|
||||
Text("Select background image")
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
}
|
||||
.accessibilityLabel(Text("Change Background Image"))
|
||||
.onChange(of:selectedItem) { newItem in
|
||||
// Retrive selected asset in the form of Data
|
||||
newItem?.loadTransferable(type:Data.self) { result in
|
||||
guard let data = try? result.get() else {
|
||||
self.inputImage = nil
|
||||
return
|
||||
}
|
||||
guard let loadedImage = UIImage(data: data) else {
|
||||
self.inputImage = nil
|
||||
return
|
||||
}
|
||||
self.inputImage = loadedImage
|
||||
}
|
||||
}
|
||||
|
||||
Spacer().frame(height: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(contact != nil ? Text("Chat Background") : Text("Default Background"))
|
||||
.onChange(of:inputImage) { _ in
|
||||
MLImageManager.sharedInstance().saveBackgroundImageData(inputImage?.pngData(), for:self.contact?.obj)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BackgroundSettings_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BackgroundSettings(contact:nil)
|
||||
}
|
||||
}
|
119
Monal/Classes/BlockedUsers.swift
Normal file
|
@ -0,0 +1,119 @@
|
|||
//
|
||||
// BlockedUsers.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by lissine on 10/9/2024.
|
||||
// Copyright © 2024 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
struct BlockedUsers: View {
|
||||
let xmppAccount: xmpp
|
||||
static private let jidPattern = "^([^@]+@)?[^/\\n]+(\\..{2,})?(/.+)?$"
|
||||
|
||||
@State private var blockedJids: [String] = []
|
||||
@State private var jidToBlock = ""
|
||||
@State private var showAddingToBlocklistForm = false
|
||||
@State private var showBlockingUnsupportedPlaceholder = false
|
||||
@State private var showInvalidJidAlert = false
|
||||
@StateObject private var overlay = LoadingOverlayState()
|
||||
|
||||
private var blockingUnsupported: Bool {
|
||||
return !xmppAccount.connectionProperties.serverDiscoFeatures.contains("urn:xmpp:blocking")
|
||||
}
|
||||
|
||||
private func reloadBlocksFromDB() {
|
||||
self.blockedJids = DataLayer.sharedInstance().blockedJids(forAccount: xmppAccount.accountID)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if showBlockingUnsupportedPlaceholder {
|
||||
ContentUnavailableShimView("Blocking unsupported", systemImage: "iphone.homebutton.slash", description: Text("Your server does not support blocking (XEP-0191)."))
|
||||
} else {
|
||||
List {
|
||||
ForEach(blockedJids, id: \.self) { blockedJid in
|
||||
Text(blockedJid)
|
||||
}
|
||||
.onDelete { indexSet in
|
||||
for row in indexSet {
|
||||
showLoadingOverlay(overlay, headlineView: Text("Saving changes to server"), descriptionView: Text(""))
|
||||
// unblock the jid
|
||||
MLXMPPManager.sharedInstance().block(false, fullJid: self.blockedJids[row], onAccount: self.xmppAccount.accountID)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.navigationTitle("Blocked Users")
|
||||
.animation(.default, value: blockedJids)
|
||||
.onAppear {
|
||||
if !(xmppAccount.accountState.rawValue >= xmppState.stateBound.rawValue && xmppAccount.connectionProperties.accountDiscoDone) {
|
||||
showLoadingOverlay(overlay, headlineView: Text("Account is connecting..."), descriptionView: Text(""))
|
||||
}
|
||||
showBlockingUnsupportedPlaceholder = blockingUnsupported
|
||||
reloadBlocksFromDB()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalAccountDiscoDone")).receive(on: RunLoop.main)) { notification in
|
||||
guard let notificationAccountID = notification.userInfo?["accountID"] as? NSNumber,
|
||||
notificationAccountID.intValue == xmppAccount.accountID.intValue else {
|
||||
return
|
||||
}
|
||||
|
||||
// recompute this state variable, so the view is re-rendered if it changed.
|
||||
showBlockingUnsupportedPlaceholder = blockingUnsupported
|
||||
reloadBlocksFromDB()
|
||||
hideLoadingOverlay(overlay)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalBlockListRefresh")).receive(on: RunLoop.main)) { notification in
|
||||
guard let notificationAccountID = notification.userInfo?["accountID"] as? NSNumber,
|
||||
notificationAccountID.intValue == xmppAccount.accountID.intValue else {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
reloadBlocksFromDB()
|
||||
DDLogVerbose("Got block list update from account \(xmppAccount)...")
|
||||
hideLoadingOverlay(overlay)
|
||||
}
|
||||
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
showAddingToBlocklistForm = true
|
||||
}, label: {
|
||||
Image(systemName: "plus")
|
||||
})
|
||||
}
|
||||
}
|
||||
.alert("Enter the jid that you want to block", isPresented: $showAddingToBlocklistForm, actions: {
|
||||
TextField("user@example.org/resource", text: $jidToBlock)
|
||||
.textInputAutocapitalization(.never)
|
||||
.keyboardType(.emailAddress)
|
||||
.autocorrectionDisabled()
|
||||
|
||||
Button("Block", role: .destructive) {
|
||||
guard (jidToBlock.range(of: BlockedUsers.jidPattern, options: .regularExpression) != nil) else {
|
||||
showInvalidJidAlert = true
|
||||
return
|
||||
}
|
||||
|
||||
showLoadingOverlay(overlay, headlineView: Text("Saving changes to server"), descriptionView: Text(""))
|
||||
// block the jid
|
||||
MLXMPPManager.sharedInstance().block(true, fullJid: jidToBlock, onAccount: self.xmppAccount.accountID)
|
||||
}
|
||||
|
||||
Button("Cancel", role: .cancel, action: {})
|
||||
}
|
||||
)
|
||||
// If .onDisappear is applied to the alert or any of its subviews, its perform action won't
|
||||
// get executed until the whole Blocked Users view is dismissed. Therefore .onChange is used instead
|
||||
.onChange(of: showAddingToBlocklistForm) { _ in
|
||||
if !showAddingToBlocklistForm {
|
||||
// The alert has been dismissed
|
||||
jidToBlock = ""
|
||||
}
|
||||
}
|
||||
.alert("Input is not a valid jid", isPresented: $showInvalidJidAlert, actions: {})
|
||||
.addLoadingOverlay(overlay)
|
||||
}
|
||||
}
|
||||
}
|
254
Monal/Classes/BoardingCards.swift
Normal file
|
@ -0,0 +1,254 @@
|
|||
//
|
||||
// BoardingCards.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Vaidik Dubey on 05/06/24.
|
||||
// Copyright © 2024 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
import FrameUp
|
||||
|
||||
class OnboardingState: ObservableObject {
|
||||
@defaultsDB("hasCompletedOnboarding")
|
||||
var hasCompletedOnboarding: Bool
|
||||
}
|
||||
|
||||
struct OnboardingCard: Identifiable {
|
||||
let id = UUID()
|
||||
let title: Text?
|
||||
let description: Text?
|
||||
let imageName: String?
|
||||
let articleText: Text?
|
||||
let customView: AnyView?
|
||||
let nextText: String?
|
||||
}
|
||||
|
||||
struct OnboardingView: View {
|
||||
var delegate: SheetDismisserProtocol
|
||||
let cards: [OnboardingCard]
|
||||
@ObservedObject var onboardingState = OnboardingState()
|
||||
@State private var currentIndex = 0
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
/// Ensure the ZStack takes the entire area
|
||||
Color.clear
|
||||
|
||||
ForEach(Array(zip(cards, cards.indices)), id: \.1) { card, index in
|
||||
/// Only show card that's visible
|
||||
if index == currentIndex {
|
||||
GeometryReader { proxy in
|
||||
SmartScrollView(.vertical, showsIndicators: true, optionalScrolling: true, shrinkToFit: false) {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
|
||||
if currentIndex > 0 {
|
||||
Button {
|
||||
currentIndex -= 1
|
||||
} label: {
|
||||
Label("Back", systemImage: "chevron.left")
|
||||
.labelStyle(.iconOnly)
|
||||
.padding(10)
|
||||
}
|
||||
} else {
|
||||
//make sure the space the "back" label will take, is already reserved to not have "jumps" when pressing next
|
||||
Text("").padding(10)
|
||||
}
|
||||
|
||||
HStack {
|
||||
if let imageName = card.imageName {
|
||||
Image(systemName: imageName)
|
||||
.font(.custom("MarkerFelt-Wide", size: 80))
|
||||
.foregroundColor(.accentColor)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
}
|
||||
|
||||
card.title?
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.primary)
|
||||
.padding(.bottom, 4)
|
||||
/// This ensures text doesn't get truncated which sometimes happens in ScrollView
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityAddTraits(.isHeader)
|
||||
|
||||
if let description = card.description {
|
||||
description
|
||||
.font(.custom("HelveticaNeue-Medium", size: 20))
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.leading)
|
||||
/// This ensures text doesn't get truncated which sometimes happens in ScrollView
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
if card.imageName != nil || card.description != nil || card.imageName != nil {
|
||||
Spacer().frame(height: 1)
|
||||
Divider()
|
||||
Spacer().frame(height: 1)
|
||||
}
|
||||
|
||||
card.articleText?
|
||||
.font(.custom("HelveticaNeue-Medium", size: 20))
|
||||
.foregroundColor(.primary)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
card.customView
|
||||
|
||||
Spacer()
|
||||
|
||||
Group {
|
||||
if index < cards.count - 1 {
|
||||
Button {
|
||||
currentIndex += 1
|
||||
} label: {
|
||||
HStack {
|
||||
Text(card.nextText ?? NSLocalizedString("Next", comment:"onboarding"))
|
||||
.fontWeight(.bold)
|
||||
Image(systemName: "chevron.right")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
onboardingState.hasCompletedOnboarding = true
|
||||
delegate.dismissWithoutAnimation()
|
||||
} label: {
|
||||
Text(card.nextText ?? NSLocalizedString("Close", comment:"onboarding"))
|
||||
}
|
||||
.buttonStyle(MonalProminentButtonStyle())
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .trailing)
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
.padding()
|
||||
/// Sets the minimum frame height to the available height of the scrollview and the maxHeight to infinity
|
||||
.frame(minHeight: proxy.size.height, maxHeight: .infinity)
|
||||
}
|
||||
}
|
||||
.accessibilityAddTraits(.isModal)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if UIDevice.current.userInterfaceIdiom != .pad {
|
||||
//force portrait mode and lock ui there
|
||||
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
|
||||
(UIApplication.shared.delegate as! MonalAppDelegate).orientationLock = .portrait
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func createOnboardingView(delegate: SheetDismisserProtocol) -> some View {
|
||||
#if IS_QUICKSY
|
||||
let cards = [
|
||||
OnboardingCard(
|
||||
title: Text("Welcome to Quicksy !"),
|
||||
description: nil,
|
||||
imageName: "hand.wave",
|
||||
articleText: Text("""
|
||||
Quicksy syncs your contact list in regular intervals to make suggestions about possible contacts who are already on Quicksy.
|
||||
|
||||
Quicksy shares and stores images, audio recordings, videos and other media to deliver them to the intended recipients. Files will be stored for up to 30 days.
|
||||
|
||||
Find more Information in our [Privacy Policy](https://quicksy.im/privacy.htm).
|
||||
"""),
|
||||
customView: nil,
|
||||
nextText: "Accept and continue"
|
||||
),
|
||||
]
|
||||
#else
|
||||
let cards = [
|
||||
OnboardingCard(
|
||||
title: Text("Welcome to Monal !"),
|
||||
description: Text("Become part of a worldwide decentralized chat network!"),
|
||||
imageName: "hand.wave",
|
||||
articleText: Text("""
|
||||
Modern iOS and macOS XMPP chat client.\n\nXMPP is a federated network: Just like email, you can register your account on many servers and still talk to anyone, even if they signed up on a different server.\n\nUsing Monal instead of a centralized chat app therefore increases your digital sovereignty.
|
||||
"""),
|
||||
customView: nil,
|
||||
nextText: nil
|
||||
),
|
||||
OnboardingCard(
|
||||
title: Text("Features"),
|
||||
description: nil,
|
||||
imageName: "sparkles",
|
||||
articleText: Text("""
|
||||
🛜 Decentralized Network :
|
||||
Leverages the decentralized nature of XMPP, avoiding central servers and increasing your digital sovereignty.
|
||||
|
||||
🌐 Data privacy :
|
||||
We do not sell or track information for external parties (nor for anyone else).
|
||||
|
||||
🔐 End-to-end encryption :
|
||||
Secure multi-end messaging using the OMEMO protocol.
|
||||
|
||||
👨💻 Open Source :
|
||||
The app's source code is publicly available for audit and contribution.
|
||||
"""),
|
||||
customView: nil,
|
||||
nextText: nil
|
||||
),
|
||||
OnboardingCard(
|
||||
title: Text("Settings"),
|
||||
description: Text("These are important privacy settings you may want to review!"),
|
||||
imageName: "gear",
|
||||
articleText: nil,
|
||||
customView: AnyView(PrivacySettingsSubview(onboardingPart:0)),
|
||||
nextText: nil
|
||||
),
|
||||
OnboardingCard(
|
||||
title: Text("Settings"),
|
||||
description: Text("These are important privacy settings you may want to review!"),
|
||||
imageName: "gear",
|
||||
articleText: nil,
|
||||
customView: AnyView(PrivacySettingsSubview(onboardingPart:1)),
|
||||
nextText: nil
|
||||
),
|
||||
OnboardingCard(
|
||||
title: Text("Even more to customize!"),
|
||||
description: Text("You can customize even more, just use the button below to open the settings."),
|
||||
imageName: "hand.wave",
|
||||
articleText: nil,
|
||||
customView: AnyView(TakeMeToSettingsView(delegate:delegate)),
|
||||
nextText: nil
|
||||
),
|
||||
]
|
||||
#endif
|
||||
OnboardingView(delegate: delegate, cards: cards)
|
||||
}
|
||||
|
||||
struct TakeMeToSettingsView: View {
|
||||
@ObservedObject var onboardingState = OnboardingState()
|
||||
var delegate: SheetDismisserProtocol
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
|
||||
if let activeChats = appDelegate.activeChats {
|
||||
activeChats.prependGeneralSettings()
|
||||
}
|
||||
onboardingState.hasCompletedOnboarding = true
|
||||
delegate.dismissWithoutAnimation()
|
||||
}) {
|
||||
Text("Take me to settings")
|
||||
}
|
||||
.buttonStyle(MonalProminentButtonStyle())
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct OnboardingView_Previews: PreviewProvider {
|
||||
static var delegate = SheetDismisserProtocol()
|
||||
static var previews: some View {
|
||||
createOnboardingView(delegate: delegate)
|
||||
.environmentObject(OnboardingState())
|
||||
}
|
||||
}
|
77
Monal/Classes/ChannelMemberList.swift
Normal file
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// ChannelMemberList.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Friedrich Altheide on 17.02.24.
|
||||
// Copyright © 2024 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
import OrderedCollections
|
||||
|
||||
struct ChannelMemberList: View {
|
||||
private let account: xmpp
|
||||
@State private var ownAffiliation: String;
|
||||
@StateObject var channel: ObservableKVOWrapper<MLContact>
|
||||
@State private var participants: OrderedDictionary<String, String>
|
||||
|
||||
init(mucContact: ObservableKVOWrapper<MLContact>) {
|
||||
account = mucContact.obj.account! as xmpp
|
||||
_channel = StateObject(wrappedValue:mucContact)
|
||||
_ownAffiliation = State(wrappedValue:kMucAffiliationNone)
|
||||
_participants = State(wrappedValue:OrderedDictionary<String, String>())
|
||||
}
|
||||
|
||||
func updateParticipantList() {
|
||||
ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:channel.obj) ?? kMucAffiliationNone
|
||||
participants.removeAll(keepingCapacity:true)
|
||||
for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:channel.contactJid, forAccountID:account.accountID)) {
|
||||
//ignore ourselves
|
||||
if let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String {
|
||||
if jid == account.connectionProperties.identity.jid {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if let nick = memberInfo["room_nick"] as? String {
|
||||
participants[nick] = memberInfo["affiliation"] as? String ?? kMucAffiliationNone
|
||||
}
|
||||
}
|
||||
participants.sort {
|
||||
(mucAffiliationToInt($0.value), $0.key.lowercased()) < (mucAffiliationToInt($1.value), $1.key.lowercased())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("\(self.channel.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) {
|
||||
ForEach(participants.keys, id: \.self) { participant_key in
|
||||
ZStack(alignment: .topLeading) {
|
||||
HStack(alignment: .center) {
|
||||
Text(participant_key)
|
||||
Spacer()
|
||||
Text(mucAffiliationToString(participants[participant_key]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(Text("Channel Participants"), displayMode: .inline)
|
||||
.onAppear {
|
||||
updateParticipantList()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in
|
||||
if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact {
|
||||
DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...")
|
||||
if contact == channel {
|
||||
updateParticipantList()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChannelMemberList_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChannelMemberList(mucContact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(3)));
|
||||
}
|
||||
}
|
29
Monal/Classes/ChatPlaceholder.swift
Normal file
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// ChatPlaceholder.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Thilo Molitor on 30.11.22.
|
||||
// Copyright © 2022 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
struct ChatPlaceholder: View {
|
||||
@Environment(\.colorScheme) var colorScheme
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if colorScheme == .dark {
|
||||
Color.black
|
||||
} else {
|
||||
Color.white
|
||||
}
|
||||
Image(colorScheme == .dark ? "park_white_black" : "park_colors")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatPlaceholder_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatPlaceholder()
|
||||
}
|
||||
}
|
701
Monal/Classes/ContactDetails.swift
Normal file
|
@ -0,0 +1,701 @@
|
|||
//
|
||||
// ContactDetails.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Jan on 22.10.21.
|
||||
// Copyright © 2021 Monal.im. All rights reserved.
|
||||
//
|
||||
|
||||
struct ContactDetails: View {
|
||||
@Environment(\.presentationMode) private var presentationMode
|
||||
@State private var ownRole = kMucRoleParticipant
|
||||
@State private var ownAffiliation = kMucAffiliationNone
|
||||
@StateObject var contact: ObservableKVOWrapper<MLContact>
|
||||
@State private var showingRemoveAvatarConfirmation = false
|
||||
@State private var showingBlockContactConfirmation = false
|
||||
@State private var showingCannotBlockAlert = false
|
||||
@State private var showingRemoveContactConfirmation = false
|
||||
@State private var showingAddContactConfirmation = false
|
||||
@State private var showingClearHistoryConfirmation = false
|
||||
@State private var showingResetOmemoSessionConfirmation = false
|
||||
@State private var showingCannotEncryptAlert = false
|
||||
@State private var showingShouldDisableEncryptionAlert = false
|
||||
@State private var isEditingNickname = false
|
||||
@State private var inputImage: UIImage?
|
||||
@State private var showingImagePicker = false
|
||||
@State private var showingSheetEditSubject = false
|
||||
@State private var showingDestroyConfirmation = false
|
||||
@State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close"))
|
||||
@State private var showAlert = false
|
||||
@State private var success = false
|
||||
@State private var successCallback: monal_void_block_t?
|
||||
@StateObject private var overlay = LoadingOverlayState()
|
||||
var delegate: SheetDismisserProtocol?
|
||||
private var account: xmpp
|
||||
|
||||
init(delegate: SheetDismisserProtocol?, contact: ObservableKVOWrapper<MLContact>) {
|
||||
self.delegate = delegate
|
||||
_contact = StateObject(wrappedValue: contact)
|
||||
self.account = contact.obj.account!
|
||||
}
|
||||
|
||||
private func updateRoleAndAffiliation() {
|
||||
if contact.isMuc {
|
||||
self.ownRole = DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact.obj) ?? kMucRoleNone
|
||||
self.ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? kMucAffiliationNone
|
||||
} else {
|
||||
self.ownRole = kMucRoleParticipant
|
||||
self.ownAffiliation = kMucAffiliationNone
|
||||
}
|
||||
}
|
||||
|
||||
private func errorAlert(title: Text, message: Text = Text("")) {
|
||||
alertPrompt.title = title
|
||||
alertPrompt.message = message
|
||||
showAlert = true
|
||||
}
|
||||
|
||||
private func successAlert(title: Text, message: Text = Text("")) {
|
||||
alertPrompt.title = title
|
||||
alertPrompt.message = message
|
||||
showAlert = true
|
||||
success = true // < dismiss entire view on close
|
||||
}
|
||||
|
||||
private func showImagePicker() {
|
||||
#if targetEnvironment(macCatalyst)
|
||||
let picker = DocumentPickerViewController(
|
||||
supportedTypes: [UTType.image],
|
||||
onPick: { url in
|
||||
if let imageData = try? Data(contentsOf: url) {
|
||||
if let loadedImage = UIImage(data: imageData) {
|
||||
self.inputImage = loadedImage
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss: {
|
||||
//do nothing on dismiss
|
||||
}
|
||||
)
|
||||
UIApplication.shared.windows.first?.rootViewController?.present(picker, animated: true)
|
||||
#else
|
||||
showingImagePicker = true
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
Section {
|
||||
VStack(spacing: 20) {
|
||||
if !contact.isSelfChat {
|
||||
Image(uiImage: contact.avatar)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.applyClosure {view in
|
||||
if contact.isMuc {
|
||||
if ownAffiliation == kMucAffiliationOwner {
|
||||
view.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Change Group Avatar") : Text("Change Channel Avatar"))
|
||||
.onTapGesture {
|
||||
showImagePicker()
|
||||
}
|
||||
.addTopRight {
|
||||
if contact.hasAvatar {
|
||||
Button(action: {
|
||||
showingRemoveAvatarConfirmation = true
|
||||
}, label: {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 24.0, height: 24.0)
|
||||
.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Remove Group Avatar") : Text("Remove Channel Avatar"))
|
||||
.symbolRenderingMode(.palette)
|
||||
.foregroundStyle(.white, .red)
|
||||
})
|
||||
.buttonStyle(.borderless)
|
||||
.offset(x: 8, y: -8)
|
||||
} else {
|
||||
Button(action: {
|
||||
showImagePicker()
|
||||
}, label: {
|
||||
Image(systemName: "pencil.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 24.0, height: 24.0)
|
||||
.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Change Group Avatar") : Text("Change Channel Avatar"))
|
||||
})
|
||||
.buttonStyle(.borderless)
|
||||
.offset(x: 8, y: -8)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
view.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Group Avatar") : Text("Channel Avatar"))
|
||||
}
|
||||
} else {
|
||||
view.accessibilityLabel(Text("Avatar"))
|
||||
}
|
||||
}
|
||||
.frame(width: 150, height: 150, alignment: .center)
|
||||
.shadow(radius: 7)
|
||||
.actionSheet(isPresented: $showingRemoveAvatarConfirmation) {
|
||||
ActionSheet(
|
||||
title: Text("Really remove avatar?"),
|
||||
message: Text("This will remove the current avatar image and revert this group/channel to the default one."),
|
||||
buttons: [
|
||||
.cancel(),
|
||||
.destructive(
|
||||
Text("Yes"),
|
||||
action: {
|
||||
showPromisingLoadingOverlay(overlay, headlineView:Text("Removing avatar..."), descriptionView:Text("")) {
|
||||
promisifyMucAction(account:account, mucJid:contact.contactJid) {
|
||||
self.account.mucProcessor.publishAvatar(nil, forMuc: contact.contactJid)
|
||||
}
|
||||
}.catch { error in
|
||||
errorAlert(title: Text("Error removing avatar!"), message: Text("\(String(describing:error))"))
|
||||
hideLoadingOverlay(overlay)
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String)
|
||||
UIAccessibility.post(notification: .announcement, argument: "JID Copied")
|
||||
} label: {
|
||||
HStack {
|
||||
Text(contact.contactJid as String)
|
||||
|
||||
Image(systemName: "doc.on.doc")
|
||||
.foregroundColor(.primary)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.accessibilityHint("Copies JID")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
|
||||
// //TODO: wait for account edit to become swiftui
|
||||
// if contact.isSelfChat {
|
||||
// Button {
|
||||
// //TODO: open account edit
|
||||
// } label: {
|
||||
// Text("Open account settings")
|
||||
// .accessibilityHint("Open account settings")
|
||||
// }
|
||||
// .buttonStyle(.borderless)
|
||||
// }
|
||||
|
||||
|
||||
//only show account jid if more than one is configured
|
||||
if MLXMPPManager.sharedInstance().connectedXMPP.count > 1 && !contact.isSelfChat {
|
||||
Text("Account: \(account.connectionProperties.identity.jid)")
|
||||
}
|
||||
|
||||
if !contact.isSelfChat && !contact.isMuc {
|
||||
if let lastInteractionTime = contact.lastInteractionTime as Date? {
|
||||
if lastInteractionTime.timeIntervalSince1970 > 0 {
|
||||
Text(String(format: NSLocalizedString("Last seen: %@", comment: ""),
|
||||
DateFormatter.localizedString(from: lastInteractionTime, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short)))
|
||||
} else {
|
||||
Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("now", comment: "")))
|
||||
}
|
||||
} else {
|
||||
Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("unknown", comment: "")))
|
||||
}
|
||||
}
|
||||
|
||||
if !contact.isMuc, let statusMessage = contact.statusMessage as String?, statusMessage.count > 0 {
|
||||
VStack {
|
||||
Text("Status message:")
|
||||
Text(contact.statusMessage as String)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
if contact.isMuc && ((contact.groupSubject as String).count > 0 || ownRole == kMucRoleModerator) {
|
||||
VStack {
|
||||
if ownRole == kMucRoleModerator {
|
||||
Button {
|
||||
showingSheetEditSubject.toggle()
|
||||
} label: {
|
||||
if contact.obj.mucType == kMucTypeGroup {
|
||||
HStack {
|
||||
Text("Group subject:")
|
||||
Spacer().frame(width:8)
|
||||
Image(systemName: "pencil")
|
||||
.foregroundColor(.primary)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.accessibilityHint("Edit Group Subject")
|
||||
} else {
|
||||
HStack {
|
||||
Text("Channel subject:")
|
||||
Spacer().frame(width:8)
|
||||
Image(systemName: "pencil")
|
||||
.foregroundColor(.primary)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
.accessibilityHint("Edit Channel Subject")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
} else {
|
||||
Text("Group subject:")
|
||||
}
|
||||
|
||||
Text(contact.groupSubject as String)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
.padding([.top, .bottom])
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
|
||||
// info/nondestructive buttons
|
||||
Section {
|
||||
if !contact.isSelfChat {
|
||||
Button {
|
||||
if contact.isMuc {
|
||||
if !contact.isMuted && !contact.isMentionOnly {
|
||||
contact.obj.toggleMentionOnly(true)
|
||||
} else if !contact.isMuted && contact.isMentionOnly {
|
||||
contact.obj.toggleMentionOnly(false)
|
||||
contact.obj.toggleMute(true)
|
||||
} else {
|
||||
contact.obj.toggleMentionOnly(false)
|
||||
contact.obj.toggleMute(false)
|
||||
}
|
||||
} else {
|
||||
contact.obj.toggleMute(!contact.isMuted)
|
||||
}
|
||||
} label: {
|
||||
if contact.isMuted {
|
||||
Label {
|
||||
contact.isMuc ? Text("Notifications disabled") : Text("Contact is muted")
|
||||
} icon: {
|
||||
Image(systemName: "bell.slash.fill")
|
||||
}
|
||||
.foregroundStyle(Color.red)
|
||||
} else if contact.isMuc && contact.isMentionOnly {
|
||||
Label {
|
||||
Text("Notify only when mentioned")
|
||||
} icon: {
|
||||
Image(systemName: "bell.badge")
|
||||
}
|
||||
.foregroundStyle(Color.primary)
|
||||
} else {
|
||||
Label {
|
||||
contact.isMuc ? Text("Notify on all messages") : Text("Contact is not muted")
|
||||
} icon: {
|
||||
Image(systemName: "bell.fill")
|
||||
}
|
||||
.foregroundStyle(Color.green)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if !DISABLE_OMEMO
|
||||
if (!contact.isMuc || (contact.isMuc && contact.mucType == kMucTypeGroup)) && !HelperTools.isContactBlacklistedForEncryption(contact.obj) {
|
||||
Button {
|
||||
if contact.isEncrypted {
|
||||
showingShouldDisableEncryptionAlert = true
|
||||
} else {
|
||||
showingCannotEncryptAlert = !contact.obj.toggleEncryption(!contact.isEncrypted)
|
||||
}
|
||||
} label: {
|
||||
if contact.isEncrypted {
|
||||
Label {
|
||||
Text("Messages are encrypted")
|
||||
} icon: {
|
||||
Image(systemName: "lock.fill")
|
||||
}
|
||||
.foregroundStyle(Color.green)
|
||||
} else {
|
||||
Label {
|
||||
Text("Messages are NOT encrypted")
|
||||
} icon: {
|
||||
Image(systemName: "lock.open.fill")
|
||||
}
|
||||
.foregroundStyle(Color.red)
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showingCannotEncryptAlert) {
|
||||
Alert(title: Text("Encryption Not Supported"), message: Text("This contact does not appear to have any devices that support encryption, please try again later if you think this is wrong."), dismissButton: .default(Text("Close")))
|
||||
}
|
||||
.actionSheet(isPresented: $showingShouldDisableEncryptionAlert) {
|
||||
ActionSheet(
|
||||
title: Text("Disable encryption?"),
|
||||
message: Text("Do you really want to disable encryption for this contact?"),
|
||||
buttons: [
|
||||
.cancel(
|
||||
Text("No, keep encryption activated"),
|
||||
action: { }
|
||||
),
|
||||
.destructive(
|
||||
Text("Yes, deactivate encryption"),
|
||||
action: {
|
||||
showingCannotEncryptAlert = !contact.obj.toggleEncryption(!contact.isEncrypted)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
//.buttonStyle(BorderlessButtonStyle())
|
||||
}
|
||||
#endif
|
||||
|
||||
if contact.isMuc && ownAffiliation == kMucAffiliationOwner {
|
||||
let label = contact.obj.mucType == kMucTypeGroup ? NSLocalizedString("Rename Group", comment:"") : NSLocalizedString("Rename Channel", comment:"")
|
||||
TextField(label, text: $contact.fullNameView, onEditingChanged: {
|
||||
isEditingNickname = $0
|
||||
})
|
||||
.accessibilityLabel(contact.obj.mucType == kMucTypeGroup ? Text("Group name") : Text("Channel name"))
|
||||
.addClearButton(isEditing: isEditingNickname, text: $contact.fullNameView)
|
||||
} else if !contact.isMuc && !contact.isSelfChat {
|
||||
TextField(NSLocalizedString("Rename Contact", comment: "placeholder text in contact details"), text: $contact.nickNameView, onEditingChanged: {
|
||||
isEditingNickname = $0
|
||||
})
|
||||
.accessibilityLabel(Text("Nickname"))
|
||||
.addClearButton(isEditing: isEditingNickname, text: $contact.nickNameView)
|
||||
}
|
||||
|
||||
Toggle(isOn: Binding(get: {
|
||||
contact.isPinned
|
||||
}, set: {
|
||||
contact.obj.togglePinnedChat($0)
|
||||
})) {
|
||||
Text("Pin Chat")
|
||||
}
|
||||
|
||||
#if !DISABLE_OMEMO
|
||||
if !HelperTools.isContactBlacklistedForEncryption(contact.obj) && !contact.isSelfChat {
|
||||
if !contact.isMuc || contact.mucType == kMucTypeGroup {
|
||||
NavigationLink(destination: LazyClosureView(OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: contact)))) {
|
||||
Text("Encryption Keys")
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if !contact.isMuc && !contact.isSelfChat {
|
||||
NavigationLink(destination: LazyClosureView(ContactResources(contact: contact))) {
|
||||
Text("Resources")
|
||||
}
|
||||
}
|
||||
|
||||
let accountJid = account.connectionProperties.identity.jid
|
||||
let displayName = contact.contactDisplayName as String
|
||||
let sharedUrl = HelperTools.getSharedDocumentsURL(forPathComponents:[accountJid, displayName])
|
||||
if UIApplication.shared.canOpenURL(sharedUrl) && FileManager.default.fileExists(atPath:sharedUrl.path) {
|
||||
NavigationLink(destination: LazyClosureView{MediaGalleryView(contact: contact.contactJid as String, accountID: contact.accountID)}) {
|
||||
Text("Shared Media")
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
UIApplication.shared.open(sharedUrl, options:[:])
|
||||
}) {
|
||||
Text("Shared Files")
|
||||
}
|
||||
}
|
||||
|
||||
NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact))) {
|
||||
Text("Change Chat Background")
|
||||
}
|
||||
|
||||
if contact.obj.isMuc && contact.obj.mucType == kMucTypeGroup {
|
||||
NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) {
|
||||
Text("Group Members")
|
||||
}
|
||||
} else if contact.obj.isMuc && contact.obj.mucType == kMucTypeChannel {
|
||||
if [kMucAffiliationOwner, kMucAffiliationAdmin].contains(ownAffiliation) {
|
||||
NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) {
|
||||
Text("Channel Participants")
|
||||
}
|
||||
} else {
|
||||
NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) {
|
||||
Text("Channel Participants")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
|
||||
Section { // the destructive section...
|
||||
if !contact.isSelfChat {
|
||||
Button(action: {
|
||||
if !contact.isBlocked {
|
||||
showingBlockContactConfirmation = true
|
||||
} else {
|
||||
showingCannotBlockAlert = !contact.obj.toggleBlocked(!contact.isBlocked)
|
||||
}
|
||||
}) {
|
||||
if !contact.isBlocked {
|
||||
Text("Block Contact")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Text("Unblock Contact")
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showingCannotBlockAlert) {
|
||||
Alert(title: Text("Blocking/Unblocking Not Supported"), message: Text("The server does not support blocking (XEP-0191)."), dismissButton: .default(Text("Close")))
|
||||
}
|
||||
.actionSheet(isPresented: $showingBlockContactConfirmation) {
|
||||
ActionSheet(
|
||||
title: Text("Block Contact"),
|
||||
message: Text("Do you really want to block this contact? You won't receive any messages from this contact."),
|
||||
buttons: [
|
||||
.cancel(),
|
||||
.destructive(
|
||||
Text("Yes"),
|
||||
action: {
|
||||
showingCannotBlockAlert = !contact.obj.toggleBlocked(!contact.isBlocked)
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
Group {
|
||||
if contact.isInRoster {
|
||||
Button(action: {
|
||||
showingRemoveContactConfirmation = true
|
||||
}) {
|
||||
if contact.isMuc {
|
||||
if contact.mucType == kMucTypeGroup {
|
||||
Text("Leave Group")
|
||||
.foregroundColor(.red)
|
||||
} else {
|
||||
Text("Leave Channel")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
} else {
|
||||
Text("Remove from contacts")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.actionSheet(isPresented: $showingRemoveContactConfirmation) {
|
||||
ActionSheet(
|
||||
title: Text(contact.isMuc ? NSLocalizedString("Leave this conversation", comment: "") : String(format: NSLocalizedString("Remove %@ from contacts?", comment: ""), contact.contactJid)),
|
||||
message: Text(contact.isMuc ? NSLocalizedString("You will no longer receive messages from this conversation", comment: "") : NSLocalizedString("They will no longer see when you are online. They may not be able to send you encrypted messages.", comment: "")),
|
||||
buttons: [
|
||||
.cancel(),
|
||||
.destructive(
|
||||
Text("Yes"),
|
||||
action: {
|
||||
contact.obj.removeFromRoster() //this will dismiss the chatview via kMonalContactRemoved notification
|
||||
//NOTE: since we can get opened from objc through active chats,
|
||||
//NOTE: we still need to support our SheetDismisserProtocol
|
||||
if let delegate = self.delegate {
|
||||
delegate.dismiss()
|
||||
} else {
|
||||
self.presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Button(action: {
|
||||
showingAddContactConfirmation = true
|
||||
}) {
|
||||
if contact.isMuc {
|
||||
if contact.mucType == kMucTypeGroup {
|
||||
Text("Join Group")
|
||||
} else {
|
||||
Text("Join Channel")
|
||||
}
|
||||
} else {
|
||||
Text("Add to contacts")
|
||||
}
|
||||
}
|
||||
.actionSheet(isPresented: $showingAddContactConfirmation) {
|
||||
ActionSheet(
|
||||
title: Text(contact.isMuc ? (contact.mucType == kMucTypeGroup ? NSLocalizedString("Join Group", comment: "") : NSLocalizedString("Join Channel", comment: "")) : String(format: NSLocalizedString("Add %@ to your contacts?", comment: ""), contact.contactJid)),
|
||||
message: Text(contact.isMuc ? NSLocalizedString("You will receive subsequent messages from this conversation", comment: "") : NSLocalizedString("They will see when you are online. They will be able to send you encrypted messages.", comment: "")),
|
||||
buttons: [
|
||||
.cancel(),
|
||||
.default(
|
||||
Text("Yes"),
|
||||
action: {
|
||||
contact.obj.addToRoster()
|
||||
}
|
||||
),
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ownAffiliation == kMucAffiliationOwner {
|
||||
Section {
|
||||
Button(action: {
|
||||
showingDestroyConfirmation = true
|
||||
}) {
|
||||
if contact.mucType == kMucTypeGroup {
|
||||
Text("Destroy Group").foregroundColor(.red)
|
||||
} else {
|
||||
Text("Destroy Channel").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.actionSheet(isPresented: $showingDestroyConfirmation) {
|
||||
ActionSheet(
|
||||
title: contact.mucType == kMucTypeGroup ? Text("Destroy Group") : Text("Destroy Channel"),
|
||||
message: contact.mucType == kMucTypeGroup ? Text("Do you really want to destroy this group? Every member will be kicked out and it will be destroyed afterwards.") : Text("Do you really want to destroy this channel? Every member will be kicked out and it will be destroyed afterwards."),
|
||||
buttons: [
|
||||
.cancel(),
|
||||
.destructive(
|
||||
Text("Yes"),
|
||||
action: {
|
||||
showPromisingLoadingOverlay(overlay, headlineView:contact.mucType == kMucTypeGroup ? Text("Destroying group...") : Text("Destroying channel..."), descriptionView:Text("")) {
|
||||
promisifyMucAction(account:account, mucJid:contact.contactJid) {
|
||||
self.account.mucProcessor.destroyRoom(contact.contactJid as String)
|
||||
}
|
||||
}.done { callback in
|
||||
if let callback = callback {
|
||||
self.successCallback = callback
|
||||
}
|
||||
successAlert(title: Text("Success"), message: contact.mucType == kMucTypeGroup ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel."))
|
||||
}.catch { error in
|
||||
errorAlert(title: Text("Error destroying group!"), message: Text("\(String(describing:error))"))
|
||||
}
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
showingClearHistoryConfirmation = true
|
||||
}) {
|
||||
if contact.isMuc {
|
||||
if contact.obj.mucType == kMucTypeGroup {
|
||||
Text("Clear chat history of this group")
|
||||
} else {
|
||||
Text("Clear chat history of this channel")
|
||||
}
|
||||
} else {
|
||||
Text("Clear chat history of this contact")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.red)
|
||||
.actionSheet(isPresented: $showingClearHistoryConfirmation) {
|
||||
ActionSheet(
|
||||
title: Text("Clear History"),
|
||||
message: Text("Do you really want to clear all messages exchanged in this conversation? If using OMEMO you won't even be able to load them from your server again."),
|
||||
buttons: [
|
||||
.cancel(),
|
||||
.destructive(
|
||||
Text("Yes"),
|
||||
action: {
|
||||
contact.obj.clearHistory()
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#if !DISABLE_OMEMO
|
||||
//omemo debug stuff, should be removed in a few months
|
||||
Section {
|
||||
// only display omemo session reset button on 1:1 and private groups
|
||||
if contact.obj.isMuc == false || (contact.isMuc && contact.mucType == kMucTypeGroup) {
|
||||
Button(action: {
|
||||
showingResetOmemoSessionConfirmation = true
|
||||
}) {
|
||||
Text("Reset OMEMO session")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
.actionSheet(isPresented: $showingResetOmemoSessionConfirmation) {
|
||||
ActionSheet(
|
||||
title: Text("Reset OMEMO session"),
|
||||
message: Text("Do you really want to reset the OMEMO session? You should only reset the connection if you know what you are doing!"),
|
||||
buttons: [
|
||||
.cancel(),
|
||||
.destructive(
|
||||
Text("Yes"),
|
||||
action: {
|
||||
self.account.omemo.clearAllSessions(forJid:contact.contactJid);
|
||||
}
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.tint(Color.primary)
|
||||
.addLoadingOverlay(overlay)
|
||||
.navigationBarTitle(contact.contactDisplayName as String, displayMode:.inline)
|
||||
.alert(isPresented: $showAlert) {
|
||||
Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: {
|
||||
showAlert = false
|
||||
if self.success == true {
|
||||
if let callback = self.successCallback {
|
||||
callback()
|
||||
}
|
||||
//close muc ui and leave chat ui of this muc
|
||||
if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats {
|
||||
activeChats.presentChat(with:nil)
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
.sheet(isPresented: $showingSheetEditSubject) {
|
||||
LazyClosureView(EditGroupSubject(contact: contact))
|
||||
}
|
||||
.sheet(isPresented:$showingImagePicker) {
|
||||
ImagePicker(image:$inputImage)
|
||||
}
|
||||
.sheet(isPresented: $inputImage.optionalMappedToBool()) {
|
||||
ImageCropView(originalImage: inputImage!, configureBlock: { cropViewController in
|
||||
cropViewController.aspectRatioPreset = .presetSquare
|
||||
cropViewController.aspectRatioLockEnabled = true
|
||||
cropViewController.aspectRatioPickerButtonHidden = true
|
||||
cropViewController.resetAspectRatioEnabled = false
|
||||
}, onCanceled: {
|
||||
inputImage = nil
|
||||
}) { (image, cropRect, angle) in
|
||||
showPromisingLoadingOverlay(overlay, headlineView:Text("Uploading avatar..."), descriptionView:Text("")) {
|
||||
promisifyMucAction(account:account, mucJid:contact.contactJid) {
|
||||
self.account.mucProcessor.publishAvatar(image, forMuc: contact.contactJid)
|
||||
}
|
||||
}.catch { error in
|
||||
errorAlert(title: Text("Error changing avatar!"), message: Text("\(String(describing:error))"))
|
||||
hideLoadingOverlay(overlay)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of:contact.avatar as UIImage) { _ in
|
||||
hideLoadingOverlay(overlay)
|
||||
}
|
||||
.onAppear {
|
||||
self.updateRoleAndAffiliation()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in
|
||||
if let xmppAccount = notification.object as? xmpp, let notificationContact = notification.userInfo?["contact"] as? MLContact {
|
||||
DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...")
|
||||
if notificationContact == contact {
|
||||
self.updateRoleAndAffiliation()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContactDetails_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(0)))
|
||||
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(1)))
|
||||
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(2)))
|
||||
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(3)))
|
||||
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(4)))
|
||||
}
|
||||
}
|
86
Monal/Classes/ContactEntry.swift
Normal file
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// ContactEntry.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Friedrich Altheide on 28.11.23.
|
||||
// Copyright © 2023 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
struct ContactEntry<AdditionalContent: View>: View {
|
||||
let contact: ObservableKVOWrapper<MLContact>
|
||||
let selfnotesPrefix: Bool
|
||||
let fallback: String?
|
||||
@ViewBuilder let additionalContent: () -> AdditionalContent
|
||||
|
||||
init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool = true, fallback: String? = nil) where AdditionalContent == EmptyView {
|
||||
self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, fallback:fallback, additionalContent:{ EmptyView() })
|
||||
}
|
||||
|
||||
init(contact:ObservableKVOWrapper<MLContact>, fallback: String?) where AdditionalContent == EmptyView {
|
||||
self.init(contact:contact, selfnotesPrefix:true, fallback:fallback, additionalContent:{ EmptyView() })
|
||||
}
|
||||
|
||||
init(contact:ObservableKVOWrapper<MLContact>, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
|
||||
self.init(contact:contact, selfnotesPrefix:true, additionalContent:additionalContent)
|
||||
}
|
||||
|
||||
init(contact:ObservableKVOWrapper<MLContact>, fallback: String?, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
|
||||
self.init(contact:contact, selfnotesPrefix:true, fallback:fallback, additionalContent:additionalContent)
|
||||
}
|
||||
|
||||
init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
|
||||
self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, fallback:nil, additionalContent:additionalContent)
|
||||
}
|
||||
|
||||
init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool, fallback: String?, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
|
||||
self.contact = contact
|
||||
self.selfnotesPrefix = selfnotesPrefix
|
||||
self.fallback = fallback
|
||||
self.additionalContent = additionalContent
|
||||
}
|
||||
|
||||
var body:some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
HStack(alignment: .center) {
|
||||
Image(uiImage: contact.avatar)
|
||||
.resizable()
|
||||
.frame(width: 40, height: 40, alignment: .center)
|
||||
VStack(alignment: .leading) {
|
||||
if selfnotesPrefix {
|
||||
// use the if to make sure this view gets updated if the contact display name changes
|
||||
// (the condition is never false, because contactDisplayName can not be nil)
|
||||
if (contact.contactDisplayName as String?) != nil {
|
||||
Text(contact.obj.contactDisplayName(withFallback:fallback))
|
||||
}
|
||||
} else {
|
||||
// use the if to make sure this view gets updated if the contact display name changes
|
||||
// (the condition is never false, because contactDisplayNameWithoutSelfnotesPrefix can not be nil)
|
||||
if (contact.contactDisplayNameWithoutSelfnotesPrefix as String?) != nil {
|
||||
Text(contact.obj.contactDisplayName(withFallback:fallback, andSelfnotesPrefix:false))
|
||||
}
|
||||
}
|
||||
additionalContent()
|
||||
Text(contact.contactJid as String)
|
||||
.foregroundColor(Color(UIColor.secondaryLabel))
|
||||
.font(.footnote)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(0)))
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(1)))
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(2)))
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(3)))
|
||||
}
|
135
Monal/Classes/ContactPicker.swift
Normal file
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// ContactList.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Jan on 15.12.22.
|
||||
// Copyright © 2022 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
import OrderedCollections
|
||||
|
||||
struct ContactPickerEntry: View {
|
||||
let contact : ObservableKVOWrapper<MLContact>
|
||||
let isPicked: Bool
|
||||
let isExistingMember: Bool
|
||||
|
||||
var body:some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
HStack(alignment: .center) {
|
||||
if(isExistingMember) {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.foregroundColor(.gray)
|
||||
} else if(isPicked) {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.foregroundColor(.accentColor)
|
||||
} else {
|
||||
Image(systemName: "circle")
|
||||
}
|
||||
ContactEntry(contact: contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContactPicker: View {
|
||||
typealias completionType = (OrderedSet<ObservableKVOWrapper<MLContact>>)->Void
|
||||
let account: xmpp
|
||||
@Binding var returnedContacts: OrderedSet<ObservableKVOWrapper<MLContact>>
|
||||
@State var selectedContacts: OrderedSet<ObservableKVOWrapper<MLContact>>
|
||||
@State var searchText = ""
|
||||
@State var isEditingSearchInput = false
|
||||
let allowRemoval: Bool
|
||||
let completion: completionType?
|
||||
|
||||
init(_ account: xmpp, initializeFrom contacts: OrderedSet<ObservableKVOWrapper<MLContact>>, allowRemoval: Bool = true, completion:completionType?) {
|
||||
self.account = account
|
||||
self.allowRemoval = allowRemoval
|
||||
self.completion = completion
|
||||
_selectedContacts = State(wrappedValue:OrderedSet())
|
||||
//use a temporary storage because we don't have a binding to the outside world but use the completion handler
|
||||
var storage = contacts
|
||||
_returnedContacts = Binding(
|
||||
get: { storage },
|
||||
set: { storage = $0 }
|
||||
)
|
||||
buildPreselectedContacts(contacts)
|
||||
DDLogError("self.allowRemoval = \(String(describing:self.allowRemoval))")
|
||||
}
|
||||
|
||||
init(_ account: xmpp, binding returnedContacts: Binding<OrderedSet<ObservableKVOWrapper<MLContact>>>, allowRemoval: Bool = true) {
|
||||
self.account = account
|
||||
self.allowRemoval = allowRemoval
|
||||
self.completion = nil
|
||||
_selectedContacts = State(wrappedValue:OrderedSet())
|
||||
_returnedContacts = returnedContacts
|
||||
buildPreselectedContacts(returnedContacts.wrappedValue)
|
||||
}
|
||||
|
||||
private mutating func buildPreselectedContacts(_ source: OrderedSet<ObservableKVOWrapper<MLContact>>) {
|
||||
//build currently selected list of contacts
|
||||
var contactsTmp: OrderedSet<ObservableKVOWrapper<MLContact>> = OrderedSet()
|
||||
for contact in source {
|
||||
contactsTmp.append(contact)
|
||||
}
|
||||
_selectedContacts = State(wrappedValue:contactsTmp)
|
||||
}
|
||||
|
||||
private var allContacts: OrderedSet<ObservableKVOWrapper<MLContact>> {
|
||||
//build list of all possible contacts on this account (excluding selfchat and other mucs)
|
||||
var contactsTmp: OrderedSet<ObservableKVOWrapper<MLContact>> = OrderedSet()
|
||||
for contact in DataLayer.sharedInstance().possibleGroupMembers(forAccount: account.accountID) {
|
||||
contactsTmp.append(ObservableKVOWrapper(contact))
|
||||
}
|
||||
return contactsTmp
|
||||
}
|
||||
|
||||
private var searchResults : OrderedSet<ObservableKVOWrapper<MLContact>> {
|
||||
if searchText.isEmpty {
|
||||
return self.allContacts
|
||||
} else {
|
||||
var filteredContacts: OrderedSet<ObservableKVOWrapper<MLContact>> = OrderedSet()
|
||||
for contact in self.allContacts {
|
||||
if (contact.contactDisplayName as String).lowercased().contains(searchText.lowercased()) ||
|
||||
(contact.contactJid as String).contains(searchText.lowercased()) {
|
||||
filteredContacts.append(contact)
|
||||
}
|
||||
}
|
||||
return filteredContacts
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if(allContacts.isEmpty) {
|
||||
Text("No contacts to show :(")
|
||||
.navigationTitle("Contact Lists")
|
||||
} else {
|
||||
List(searchResults) { contact in
|
||||
let contactIsSelected = self.selectedContacts.contains(contact);
|
||||
let contactIsAlreadyMember = self.returnedContacts.contains(contact);
|
||||
ContactPickerEntry(contact: contact, isPicked: contactIsSelected, isExistingMember: !(!contactIsAlreadyMember || allowRemoval))
|
||||
.onTapGesture {
|
||||
// only allow changes to members that are not already part of the group
|
||||
if(!contactIsAlreadyMember || allowRemoval) {
|
||||
if(contactIsSelected) {
|
||||
self.selectedContacts.remove(contact)
|
||||
} else {
|
||||
self.selectedContacts.append(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchText, placement: .automatic, prompt: nil)
|
||||
.listStyle(.inset)
|
||||
.navigationBarTitle(Text("Contact Selection"), displayMode: .inline)
|
||||
.onDisappear {
|
||||
returnedContacts.removeAll()
|
||||
for contact in selectedContacts {
|
||||
returnedContacts.append(contact)
|
||||
}
|
||||
if let completion = completion {
|
||||
completion(returnedContacts)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
120
Monal/Classes/ContactRequestsMenu.swift
Normal file
|
@ -0,0 +1,120 @@
|
|||
//
|
||||
// ContactRequestsMenu.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Jan on 27.10.22.
|
||||
// Copyright © 2022 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
struct ContactRequestsMenuEntry: View {
|
||||
let contact : MLContact
|
||||
@State private var isDeleted = false
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(contact.contactJid)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Group {
|
||||
Button {
|
||||
let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
|
||||
appDelegate.openChat(of:contact)
|
||||
} label: {
|
||||
Image(systemName: "text.bubble")
|
||||
.foregroundStyle(Color.primary)
|
||||
}
|
||||
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
Button {
|
||||
// deny request
|
||||
MLXMPPManager.sharedInstance().remove(contact)
|
||||
} label: {
|
||||
Image(systemName: "trash.circle")
|
||||
.foregroundStyle(Color.red)
|
||||
}
|
||||
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
|
||||
Button {
|
||||
// accept request
|
||||
MLXMPPManager.sharedInstance().add(contact)
|
||||
let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
|
||||
appDelegate.openChat(of:contact)
|
||||
} label: {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.foregroundStyle(Color.green)
|
||||
}
|
||||
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
|
||||
.buttonStyle(BorderlessButtonStyle())
|
||||
}
|
||||
.font(.largeTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContactRequestsMenu: View {
|
||||
@State var pendingRequests: [xmpp:[MLContact]] = [:]
|
||||
@State var enabledAccounts: [Int:xmpp] = [:]
|
||||
|
||||
func updateRequests() {
|
||||
let requests = DataLayer.sharedInstance().allContactRequests() as! [MLContact]
|
||||
enabledAccounts.removeAll()
|
||||
for account in MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] {
|
||||
enabledAccounts[account.accountID.intValue] = account
|
||||
}
|
||||
pendingRequests.removeAll()
|
||||
for contact in requests {
|
||||
//add only requests having an enabled (dubbed connected) account
|
||||
//(should be a noop because allContactRequests() returns only enabled accounts)
|
||||
if let account = enabledAccounts[contact.accountID.intValue] {
|
||||
if pendingRequests[account] == nil {
|
||||
pendingRequests[account] = []
|
||||
}
|
||||
pendingRequests[account]!.append(contact)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Section(header: Text("Allowing someone to add you as a contact lets them see your profile picture and when you are online.")) {
|
||||
if(pendingRequests.isEmpty) {
|
||||
Text("No pending contact requests")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
List {
|
||||
ForEach(pendingRequests.sorted(by:{ $0.0.connectionProperties.identity.jid < $1.0.connectionProperties.identity.jid }), id: \.key) { account, requests in
|
||||
if enabledAccounts.count == 1 {
|
||||
ForEach(requests.indices, id: \.self) { idx in
|
||||
ContactRequestsMenuEntry(contact: requests[idx])
|
||||
}
|
||||
} else {
|
||||
Section(header: Text("Account: \(account.connectionProperties.identity.jid)")) {
|
||||
ForEach(requests.indices, id: \.self) { idx in
|
||||
ContactRequestsMenuEntry(contact: requests[idx])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh")).receive(on: RunLoop.main)) { notification in
|
||||
updateRequests()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRemoved")).receive(on: RunLoop.main)) { notification in
|
||||
updateRequests()
|
||||
}
|
||||
.onAppear {
|
||||
updateRequests()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContactRequestsMenu_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContactRequestsMenu()
|
||||
}
|
||||
}
|
159
Monal/Classes/ContactResources.swift
Normal file
|
@ -0,0 +1,159 @@
|
|||
//
|
||||
// ContactResources.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Friedrich Altheide on 24.12.21.
|
||||
// Copyright © 2021 Monal.im. All rights reserved.
|
||||
//
|
||||
import OrderedCollections
|
||||
|
||||
@ViewBuilder
|
||||
func resourceRowElement(_ k: String, _ v: some View, space: CGFloat = 5) -> some View {
|
||||
HStack {
|
||||
Text(k).font(.headline)
|
||||
Spacer()
|
||||
v.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
struct ContactResources: View {
|
||||
@StateObject var contact: ObservableKVOWrapper<MLContact>
|
||||
@State var contactVersionInfos: [String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>]
|
||||
@State private var showCaps: String?
|
||||
|
||||
init(contact: ObservableKVOWrapper<MLContact>, previewMock: [String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>]? = nil) {
|
||||
_contact = StateObject(wrappedValue: contact)
|
||||
|
||||
if previewMock != nil {
|
||||
self.contactVersionInfos = previewMock!
|
||||
} else {
|
||||
var tmpInfos:[String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>] = [:]
|
||||
for ressourceName in DataLayer.sharedInstance().resources(for: contact.obj) {
|
||||
// load already known software version info from database
|
||||
if let softwareInfo = DataLayer.sharedInstance().getSoftwareVersionInfo(forContact:contact.obj.contactJid, resource:ressourceName, andAccount:contact.obj.accountID) {
|
||||
tmpInfos[ressourceName] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(softwareInfo)
|
||||
}
|
||||
}
|
||||
self.contactVersionInfos = tmpInfos
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(self.contactVersionInfos.sorted(by:{ $0.0 < $1.0 }), id: \.key) { key, versionInfo in
|
||||
Section {
|
||||
VStack {
|
||||
resourceRowElement("Resource:", Text(versionInfo.resource as String))
|
||||
resourceRowElement("Client Name:", Text(versionInfo.appName as String? ?? ""))
|
||||
resourceRowElement("Client Version:", Text(versionInfo.appVersion as String? ?? ""))
|
||||
resourceRowElement("OS:", Text(versionInfo.platformOs as String? ?? ""))
|
||||
if let lastInteraction = versionInfo.lastInteraction as Date? {
|
||||
if lastInteraction.timeIntervalSince1970 == 0 {
|
||||
resourceRowElement("Last Interaction:", Text("Currently Online"))
|
||||
} else {
|
||||
resourceRowElement("Last Interaction:", Text(lastInteraction.formatted(date:.numeric, time:.standard)))
|
||||
}
|
||||
} else {
|
||||
resourceRowElement("Last Interaction:", Text("unsupported"))
|
||||
}
|
||||
}
|
||||
.onTapGesture(count: 2, perform: {
|
||||
showCaps = versionInfo.resource
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.richAlert(isPresented:$showCaps, title:Text("XMPP Capabilities")) { resource in
|
||||
VStack(alignment: .leading) {
|
||||
Text("The resource '\(resource)' has the following capabilities:")
|
||||
.font(Font.body.weight(.semibold))
|
||||
Spacer()
|
||||
.frame(height: 20)
|
||||
Section {
|
||||
let capsVer = DataLayer.sharedInstance().getVerForUser(self.contact.contactJid, andResource:resource, onAccountID:self.contact.accountID)
|
||||
Text("Caps hash: \(String(describing:capsVer))")
|
||||
Divider()
|
||||
if let capsSet = DataLayer.sharedInstance().getCapsforVer(capsVer, onAccountID:contact.obj.accountID) as? Set<String> {
|
||||
let caps = Array(capsSet)
|
||||
VStack(alignment: .leading) {
|
||||
ForEach(caps, id: \.self) { cap in
|
||||
Text(cap)
|
||||
.font(.system(.footnote, design:.monospaced))
|
||||
if cap != caps.last {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalXmppUserSoftWareVersionRefresh")).receive(on: RunLoop.main)) { notification in
|
||||
if let xmppAccount = notification.object as? xmpp, let softwareInfo = notification.userInfo?["versionInfo"] as? MLContactSoftwareVersionInfo {
|
||||
DDLogVerbose("Got software version info from account \(xmppAccount)...")
|
||||
if softwareInfo.fromJid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
|
||||
DispatchQueue.main.async {
|
||||
DDLogVerbose("Successfully matched software version info update to current contact: \(contact.obj)")
|
||||
self.contactVersionInfos[softwareInfo.resource ?? ""] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(softwareInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalNewPresenceNotice")).receive(on: RunLoop.main)) { notification in
|
||||
if let xmppAccount = notification.object as? xmpp, let jid = notification.userInfo?["jid"] as? String, let resource = notification.userInfo?["resource"] as? String, let available = notification.userInfo?["available"] as? NSNumber {
|
||||
DDLogVerbose("Got presence update from account \(xmppAccount)...")
|
||||
if jid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
|
||||
DispatchQueue.main.async {
|
||||
DDLogVerbose("Successfully matched presence update to current contact: \(contact.obj)")
|
||||
if available.boolValue {
|
||||
if let softwareInfo = DataLayer.sharedInstance().getSoftwareVersionInfo(forContact:contact.obj.contactJid, resource:resource, andAccount:contact.obj.accountID) {
|
||||
self.contactVersionInfos[resource] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(softwareInfo)
|
||||
}
|
||||
// query software version from contact
|
||||
MLXMPPManager.sharedInstance().getEntitySoftWareVersion(for:contact.obj, andResource:resource)
|
||||
} else {
|
||||
self.contactVersionInfos[resource] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalLastInteractionUpdatedNotice")).receive(on: RunLoop.main)) { notification in
|
||||
if let xmppAccount = notification.object as? xmpp, let jid = notification.userInfo?["jid"] as? String, let resource = notification.userInfo?["resource"] as? String, notification.userInfo?["lastInteraction"] as? NSDate != nil {
|
||||
DDLogVerbose("Got lastInteraction update from account \(xmppAccount)...")
|
||||
if jid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
|
||||
DispatchQueue.main.async {
|
||||
DDLogVerbose("Successfully matched lastInteraction update to current contact: \(contact.obj)")
|
||||
self.contactVersionInfos[resource]?.obj.lastInteraction = DataLayer.sharedInstance().lastInteraction(ofJid:self.contact.obj.contactJid, andResource:resource, forAccountID:contact.obj.accountID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
DDLogVerbose("View will appear...")
|
||||
let newTimeout = DispatchTime.now() + 1.0;
|
||||
DispatchQueue.main.asyncAfter(deadline: newTimeout) {
|
||||
DDLogVerbose("Refreshing software version info...")
|
||||
for ressourceName in DataLayer.sharedInstance().resources(for: contact.obj) {
|
||||
// query software version from contact
|
||||
MLXMPPManager.sharedInstance().getEntitySoftWareVersion(for:contact.obj, andResource:ressourceName)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitle(Text("Devices of \(contact.contactDisplayName as String)"), displayMode: .inline)
|
||||
}
|
||||
}
|
||||
|
||||
func previewMock() -> [String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>] {
|
||||
var previewMock:[String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>] = [:]
|
||||
previewMock["m1"] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(MLContactSoftwareVersionInfo.init(jid: "test1@monal.im", andRessource: "m1", andAppName: "Monal", andAppVersion: "1.1.1", andPlatformOS: "ios", andLastInteraction: Date()))
|
||||
previewMock["m2"] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(MLContactSoftwareVersionInfo.init(jid: "test1@monal.im", andRessource: "m2", andAppName: "Monal", andAppVersion: "1.1.2", andPlatformOS: "macOS", andLastInteraction: Date()))
|
||||
previewMock["m3"] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(MLContactSoftwareVersionInfo.init(jid: "test1@monal.im", andRessource: "m3", andAppName: "Monal", andAppVersion: "1.1.2", andPlatformOS: "macOS", andLastInteraction: Date()))
|
||||
return previewMock
|
||||
}
|
||||
|
||||
struct ContactResources_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContactResources(contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(0)), previewMock:previewMock())
|
||||
}
|
||||
}
|
194
Monal/Classes/ContactsView.swift
Normal file
|
@ -0,0 +1,194 @@
|
|||
//
|
||||
// ContactsView.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Matthew Fennell <matthew@fennell.dev> on 10/08/2024.
|
||||
// Copyright © 2024 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContactViewEntry: View {
|
||||
private let contact: MLContact
|
||||
@Binding private var selectedContactForContactDetails: ObservableKVOWrapper<MLContact>?
|
||||
private let dismissWithContact: (MLContact) -> Void
|
||||
|
||||
@State private var shouldPresentRemoveContactAlert: Bool = false
|
||||
|
||||
private var removeContactButtonText: String {
|
||||
if !isDeletable {
|
||||
return "Cannot delete notes to self"
|
||||
}
|
||||
return contact.isMuc ? "Remove Conversation" : "Remove Contact"
|
||||
}
|
||||
|
||||
private var removeContactConfirmationTitle: String {
|
||||
contact.isMuc ? "Leave this converstion?" : "Remove \(contact.contactJid) from contacts?"
|
||||
}
|
||||
|
||||
private var removeContactConfirmationDetail: String {
|
||||
contact.isMuc ? "" : "They will no longer see when you are online. They may not be able to access your encryption keys."
|
||||
}
|
||||
|
||||
private var isDeletable: Bool {
|
||||
!contact.isSelfChat
|
||||
}
|
||||
|
||||
init(contact: MLContact, selectedContactForContactDetails: Binding<ObservableKVOWrapper<MLContact>?>, dismissWithContact: @escaping (MLContact) -> Void) {
|
||||
self.contact = contact
|
||||
_selectedContactForContactDetails = selectedContactForContactDetails
|
||||
self.dismissWithContact = dismissWithContact
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Apple's list dividers only extend as far left as the left-most text in the view.
|
||||
// This means, by default, that the dividers on this screen would not extend all the way to the left of the view.
|
||||
// This combination of HStack with spacing of 0, and empty text at the left of the view, is a workaround to override this behaviour.
|
||||
// See https://stackoverflow.com/a/76698909
|
||||
HStack(spacing: 0) {
|
||||
Text("").frame(maxWidth: 0)
|
||||
Button(action: { dismissWithContact(contact) }) {
|
||||
HStack {
|
||||
ContactEntry(contact: ObservableKVOWrapper<MLContact>(contact))
|
||||
Spacer()
|
||||
Button {
|
||||
selectedContactForContactDetails = ObservableKVOWrapper<MLContact>(contact)
|
||||
} label: {
|
||||
Image(systemName: "info.circle")
|
||||
.imageScale(.large)
|
||||
}
|
||||
.accessibilityLabel("Open contact details")
|
||||
}
|
||||
}
|
||||
}
|
||||
.swipeActions(allowsFullSwipe: false) {
|
||||
// We do not use a Button with destructive role here as we would like to display the confirmation dialog first.
|
||||
// A destructive role would dismiss the row immediately, without waiting for the confirmation.
|
||||
Button(removeContactButtonText) {
|
||||
shouldPresentRemoveContactAlert = true
|
||||
}
|
||||
.tint(isDeletable ? .red : .gray)
|
||||
.disabled(!isDeletable)
|
||||
}
|
||||
.confirmationDialog(removeContactConfirmationTitle, isPresented: $shouldPresentRemoveContactAlert, titleVisibility: .visible) {
|
||||
Button(role: .cancel) {} label: {
|
||||
Text("No")
|
||||
}
|
||||
Button(role: .destructive) {
|
||||
MLXMPPManager.sharedInstance().remove(contact)
|
||||
} label: {
|
||||
Text("Yes")
|
||||
}
|
||||
} message: {
|
||||
Text(removeContactConfirmationDetail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContactsView: View {
|
||||
@ObservedObject private var contacts: Contacts
|
||||
private let delegate: SheetDismisserProtocol
|
||||
private let dismissWithContact: (MLContact) -> Void
|
||||
|
||||
@State private var searchText: String = ""
|
||||
@State private var selectedContactForContactDetails: ObservableKVOWrapper<MLContact>? = nil
|
||||
|
||||
init(contacts: Contacts, delegate: SheetDismisserProtocol, dismissWithContact: @escaping (MLContact) -> Void) {
|
||||
self.contacts = contacts
|
||||
self.delegate = delegate
|
||||
self.dismissWithContact = dismissWithContact
|
||||
}
|
||||
|
||||
private static func shouldDisplayContact(contact: MLContact) -> Bool {
|
||||
#if IS_QUICKSY
|
||||
return true
|
||||
#endif
|
||||
return contact.isSubscribedTo || contact.hasOutgoingContactRequest || contact.isSubscribedFrom
|
||||
}
|
||||
|
||||
private var contactList: [MLContact] {
|
||||
contacts.contacts
|
||||
.filter(ContactsView.shouldDisplayContact)
|
||||
.sorted { ContactsView.sortingCriteria($0) < ContactsView.sortingCriteria($1) }
|
||||
}
|
||||
|
||||
private var searchResults: [MLContact] {
|
||||
if searchText.isEmpty { return contactList }
|
||||
return contactList.filter { searchMatchesContact(contact: $0, search: searchText) }
|
||||
}
|
||||
|
||||
private static func sortingCriteria(_ contact: MLContact) -> (String, String) {
|
||||
(contact.contactDisplayName.lowercased(), contact.contactJid.lowercased())
|
||||
}
|
||||
|
||||
private func searchMatchesContact(contact: MLContact, search: String) -> Bool {
|
||||
let jid = contact.contactJid.lowercased()
|
||||
let name = contact.contactDisplayName.lowercased()
|
||||
let search = search.lowercased()
|
||||
|
||||
return jid.contains(search) || name.contains(search)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(searchResults, id: \.self) { contact in
|
||||
ContactViewEntry(contact: contact, selectedContactForContactDetails: $selectedContactForContactDetails, dismissWithContact: dismissWithContact)
|
||||
}
|
||||
}
|
||||
.animation(.default, value: contactList)
|
||||
.navigationTitle("Contacts")
|
||||
.listStyle(.plain)
|
||||
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
|
||||
.autocorrectionDisabled()
|
||||
.textInputAutocapitalization(.never)
|
||||
.keyboardType(.emailAddress)
|
||||
.overlay {
|
||||
if contactList.isEmpty {
|
||||
ContentUnavailableShimView("You need friends for this ride", systemImage: "figure.wave", description: Text("Add new contacts with the + button above. Your friends will pop up here when they can talk"))
|
||||
} else if searchResults.isEmpty {
|
||||
ContentUnavailableShimView.search
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: .topBarTrailing) {
|
||||
NavigationLink(destination: CreateGroupMenu(delegate: SheetDismisserProtocol())) {
|
||||
Image(systemName: "person.3.fill")
|
||||
}
|
||||
.accessibilityLabel("Create contact group")
|
||||
|
||||
NavigationLink(destination: AddContactMenu(delegate: SheetDismisserProtocol(), dismissWithNewContact: dismissWithContact)) {
|
||||
Image(systemName: "person.fill.badge.plus")
|
||||
.overlay { NumberlessBadge($contacts.requestCount) }
|
||||
}
|
||||
.accessibilityLabel(contacts.requestCount > 0 ? "Add contact (contact requests pending)" : "Add New Contact")
|
||||
}
|
||||
}
|
||||
.sheet(item: $selectedContactForContactDetails) { selectedContact in
|
||||
AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactDetails(delegate: delegate, contact: selectedContact)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Contacts: ObservableObject {
|
||||
@Published var contacts: Set<MLContact>
|
||||
@Published var requestCount: Int
|
||||
private var subscriptions: Set<AnyCancellable> = Set()
|
||||
|
||||
init() {
|
||||
contacts = Set(DataLayer.sharedInstance().contactList())
|
||||
requestCount = DataLayer.sharedInstance().allContactRequests().count
|
||||
subscriptions = [
|
||||
NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRemoved"))
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { _ in self.refreshContacts() },
|
||||
NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh"))
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { _ in self.refreshContacts() }
|
||||
]
|
||||
}
|
||||
|
||||
private func refreshContacts() {
|
||||
contacts = Set(DataLayer.sharedInstance().contactList())
|
||||
requestCount = DataLayer.sharedInstance().allContactRequests().count
|
||||
}
|
||||
}
|
49
Monal/Classes/ContentUnavailableShimView.swift
Normal file
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// ContentUnavailableShimView.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Matthew Fennell <matthew@fennell.dev> on 05/08/2024.
|
||||
// Copyright © 2024 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentUnavailableShimView: View {
|
||||
private var reason: String
|
||||
private var systemImage: String
|
||||
private var description: Text
|
||||
|
||||
init(_ reason: String, systemImage: String, description: Text) {
|
||||
self.reason = reason
|
||||
self.systemImage = systemImage
|
||||
self.description = description
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 17, *) {
|
||||
ContentUnavailableView(reason, systemImage: systemImage, description: description)
|
||||
} else {
|
||||
VStack {
|
||||
Image(systemName: systemImage)
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.largeTitle)
|
||||
.padding(.bottom, 4)
|
||||
Text(reason)
|
||||
.fontWeight(.bold)
|
||||
.font(.title3)
|
||||
description
|
||||
.foregroundStyle(.secondary)
|
||||
.font(.footnote)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ContentUnavailableShimView {
|
||||
static var search: ContentUnavailableShimView = ContentUnavailableShimView("No Results", systemImage: "magnifyingglass", description: Text("Check the spelling or try a new search."))
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentUnavailableShimView("Cannot Display", systemImage: "iphone.homebutton.slash", description: Text("Cannot display for this reason."))
|
||||
}
|
141
Monal/Classes/CreateGroupMenu.swift
Normal file
|
@ -0,0 +1,141 @@
|
|||
//
|
||||
// AddContactMenu.swift
|
||||
// Monal
|
||||
//
|
||||
// Created by Jan on 27.10.22.
|
||||
// Copyright © 2022 monal-im.org. All rights reserved.
|
||||
//
|
||||
|
||||
import OrderedCollections
|
||||
|
||||
struct CreateGroupMenu: View {
|
||||
private var appDelegate: MonalAppDelegate
|
||||
private var delegate: SheetDismisserProtocol
|
||||
@State private var enabledAccounts: [xmpp]
|
||||
@State private var selectedAccount: xmpp?
|
||||
@State private var groupName: String = ""
|
||||
@State private var showAlert = false
|
||||
// note: dismissLabel is not accessed but defined at the .alert() section
|
||||
@State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close"))
|
||||
@State private var selectedContacts: OrderedSet<ObservableKVOWrapper<MLContact>> = []
|
||||
@State private var isEditingGroupName = false
|
||||
@StateObject private var overlay = LoadingOverlayState()
|
||||
|
||||
init(delegate: SheetDismisserProtocol) {
|
||||
self.appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
|
||||
self.delegate = delegate
|
||||
|
||||
let enabledAccounts = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp]
|
||||
self.enabledAccounts = enabledAccounts
|
||||
_selectedAccount = State(wrappedValue: enabledAccounts.first)
|
||||
}
|
||||
|
||||
private func errorAlert(title: Text, message: Text = Text("")) {
|
||||
alertPrompt.title = title
|
||||
alertPrompt.message = message
|
||||
showAlert = true
|
||||
}
|
||||
|
||||
// When a Form is placed inside a Popover, and the horizontal size class is regular, the spacing chosen by SwiftUI is incorrect.
|
||||
// In particular, the spacing between the top of the first element and the navigation bar is too small, meaning the two overlap.
|
||||
// This only happens when the view is inside a popover, and the horizontal size class is regular.
|
||||
// Therefore, it is inconvenient to apply some manual spacing, as this we would have to work out in which situations it should be applied.
|
||||
// Placing a Text view inside the header causes SwiftUI to add consistent spacing in all situations.
|
||||
var popoverFormSpacingWorkaround: some View {
|
||||
Text("")
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Form {
|
||||
if enabledAccounts.isEmpty {
|
||||
Text("Please make sure at least one account has connected before trying to create new group.")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
else
|
||||
{
|
||||
Section(header: popoverFormSpacingWorkaround) {
|
||||
if enabledAccounts.count > 1 {
|
||||
Picker(selection: $selectedAccount, label: Text("Use account")) {
|
||||
ForEach(Array(self.enabledAccounts.enumerated()), id: \.element) { idx, account in
|
||||
Text(account.connectionProperties.identity.jid).tag(account as xmpp?)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
}
|
||||
|
||||
TextField(NSLocalizedString("Group Name (optional)", comment: "placeholder when creating new group"), text: $groupName, onEditingChanged: { isEditingGroupName = $0 })
|
||||
.autocorrectionDisabled()
|
||||
.autocapitalization(.none)
|
||||
.addClearButton(isEditing: isEditingGroupName, text:$groupName)
|
||||
|
||||
Button(action: {
|
||||
guard let generatedJid = self.selectedAccount!.mucProcessor.generateMucJid() else {
|
||||
errorAlert(title: Text("Error creating group!"), message: Text("Your server does not provide a MUC component."))
|
||||
return
|
||||
}
|
||||
showLoadingOverlay(overlay, headline: NSLocalizedString("Creating Group", comment: ""))
|
||||
guard let roomJid = self.selectedAccount!.mucProcessor.createGroup(generatedJid) else {
|
||||
//room already existing in our local bookmarks --> just open it
|
||||
//this should never happen since we randomly generated a jid above
|
||||
hideLoadingOverlay(overlay)
|
||||
let groupContact = MLContact.createContact(fromJid: generatedJid, andAccountID: self.selectedAccount!.accountID)
|
||||
self.delegate.dismissWithoutAnimation()
|
||||
if let activeChats = self.appDelegate.activeChats {
|
||||
activeChats.presentChat(with:groupContact)
|
||||
}
|
||||
return
|
||||
}
|
||||
self.selectedAccount!.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary
|
||||
let success : Bool = data["success"] as! Bool;
|
||||
if success {
|
||||
DataLayer.sharedInstance().setFullName(self.groupName, forContact:roomJid, andAccount:self.selectedAccount!.accountID)
|
||||
self.selectedAccount!.mucProcessor.changeName(ofMuc: roomJid, to: self.groupName)
|
||||
for user in self.selectedContacts {
|
||||
self.selectedAccount!.mucProcessor.setAffiliation(kMucAffiliationMember, ofUser: user.contactJid, inMuc: roomJid)
|
||||
self.selectedAccount!.mucProcessor.inviteUser(user.contactJid, inMuc: roomJid)
|
||||
}
|
||||
let groupContact = MLContact.createContact(fromJid: roomJid, andAccountID: self.selectedAccount!.accountID)
|
||||
hideLoadingOverlay(overlay)
|
||||
self.delegate.dismissWithoutAnimation()
|
||||
if let activeChats = self.appDelegate.activeChats {
|
||||
activeChats.presentChat(with:groupContact)
|
||||
}
|
||||
} else {
|
||||
hideLoadingOverlay(overlay)
|
||||
errorAlert(title: Text("Error creating group!"), message: Text(data["errorMessage"] as! String))
|
||||
}
|
||||
}, forMuc: roomJid)
|
||||
}, label: {
|
||||
Text("Create new group")
|
||||
})
|
||||
}
|
||||
|
||||
Section(header: Text("Selected Group Members")) {
|
||||
NavigationLink(destination: LazyClosureView(ContactPicker(self.selectedAccount!, binding: $selectedContacts))) {
|
||||
Text("Change Group Members")
|
||||
}
|
||||
ForEach(self.selectedContacts, id: \.obj.contactJid) { contact in
|
||||
ContactEntry(contact: contact)
|
||||
}
|
||||
.onDelete(perform: { indexSet in
|
||||
self.selectedContacts.remove(at: indexSet.first!)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showAlert) {
|
||||
Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: {
|
||||
showAlert = false
|
||||
}))
|
||||
}
|
||||
.addLoadingOverlay(overlay)
|
||||
.navigationBarTitle(Text("Create new group"), displayMode: .inline)
|
||||
}
|
||||
}
|
||||
|
||||
struct CreateGroupMenu_Previews: PreviewProvider {
|
||||
static var delegate = SheetDismisserProtocol()
|
||||
static var previews: some View {
|
||||
CreateGroupMenu(delegate: delegate)
|
||||
}
|
||||
}
|