Compare commits
511 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7399e8cd5c | ||
|
|
8161aa6d8c | ||
|
|
18c51d8d1b | ||
|
|
af01b97faf | ||
|
|
62ebf05a8f | ||
|
|
452e3dab06 | ||
|
|
43fedf6fcc | ||
|
|
9ab8cf29b6 | ||
|
|
e6cd5475e9 | ||
|
|
9c31b8a864 | ||
|
|
5c7e3cc5b9 | ||
|
|
d5f910ff00 | ||
|
|
d1643dbc8f | ||
|
|
dc3a5b479c | ||
|
|
b3d8dbc554 | ||
|
|
f3534e4415 | ||
|
|
061f5abd3e | ||
|
|
be047b28a6 | ||
|
|
fcdd3c7840 | ||
|
|
ca8c2cb8c5 | ||
|
|
a9f15144ed | ||
|
|
a80ac04650 | ||
|
|
25e237cfcb | ||
|
|
3227ee4c49 | ||
|
|
45ed97c255 | ||
|
|
31578e2e34 | ||
|
|
e33a7c662f | ||
|
|
8ea40a4728 | ||
|
|
29f152fcb3 | ||
|
|
5779927831 | ||
|
|
f184639c6b | ||
|
|
043fb923ae | ||
|
|
19fb333a67 | ||
|
|
35a8f90a1c | ||
|
|
c898f72847 | ||
|
|
981c868f6e | ||
|
|
15e228f287 | ||
|
|
707116a39b | ||
|
|
750740ac16 | ||
|
|
6909b6248f | ||
|
|
8bd6fe1e11 | ||
|
|
4cf705f141 | ||
|
|
99417b418a | ||
|
|
648e9b8393 | ||
|
|
69eff8fa28 | ||
|
|
3e858c652f | ||
|
|
21147969ea | ||
|
|
731de7023f | ||
|
|
810bc4572c | ||
|
|
4e8580b8f5 | ||
|
|
2e21141bc6 | ||
|
|
08a40b3516 | ||
|
|
7b9ab2488e | ||
|
|
00121bd31f | ||
|
|
ea260cb8a6 | ||
|
|
b0bd5ba906 | ||
|
|
726c4ab93b | ||
|
|
5dc91c87c6 | ||
|
|
de7a812cbd | ||
|
|
b1a3a5076f | ||
|
|
9470caf83a | ||
|
|
bdee5d17a5 | ||
|
|
85d53162c1 | ||
|
|
75814ca04d | ||
|
|
2ead48ad40 | ||
|
|
6fd7f9e1d5 | ||
|
|
7fceccc114 | ||
|
|
6bdc05f640 | ||
|
|
05e94902b8 | ||
|
|
233ef7d418 | ||
|
|
5cef975a9e | ||
|
|
39522fda58 | ||
|
|
1137189b57 | ||
|
|
32e7c1f93e | ||
|
|
7ad5df1949 | ||
|
|
77b26210ab | ||
|
|
20749c5419 | ||
|
|
14adb41ff8 | ||
|
|
8efefd19a0 | ||
|
|
bd0c72e847 | ||
|
|
5b4f9ea593 | ||
|
|
ef47d78c64 | ||
|
|
9f40223e6c | ||
|
|
6d95924145 | ||
|
|
240b74de94 | ||
|
|
4d30ef2929 | ||
|
|
415450a943 | ||
|
|
80a9132ff5 | ||
|
|
d613cc9ad7 | ||
|
|
3fb7e8b4b7 | ||
|
|
9406cfe6cc | ||
|
|
c3af9f7209 | ||
|
|
af0d239de1 | ||
|
|
ff65b15277 | ||
|
|
91d40cbeea | ||
|
|
21f628fd1e | ||
|
|
f5b124b175 | ||
|
|
10af836105 | ||
|
|
5a019e5950 | ||
|
|
c8860c6aaa | ||
|
|
fb0975f0d6 | ||
|
|
dd314a12af | ||
|
|
68ad2185dd | ||
|
|
66e5258368 | ||
|
|
fcc19ae830 | ||
|
|
6a683be907 | ||
|
|
d79002ea6b | ||
|
|
913cb788a2 | ||
|
|
45665138b2 | ||
|
|
d1c45c9ad0 | ||
|
|
d31229001f | ||
|
|
c3a83d7542 | ||
|
|
5bc2f08084 | ||
|
|
041cce64b0 | ||
|
|
74ccf1a246 | ||
|
|
a135ea236d | ||
|
|
d204ea35bd | ||
|
|
77983e73dd | ||
|
|
6aee8c84cd | ||
|
|
45757878d8 | ||
|
|
8ccd86de0f | ||
|
|
ceb66d46be | ||
|
|
0f2792fdbc | ||
|
|
eeae27cc24 | ||
|
|
7677421ef4 | ||
|
|
d17a1f3d8f | ||
|
|
7d93470738 | ||
|
|
a2b6771715 | ||
|
|
aa7ef0041f | ||
|
|
ee536f9ce5 | ||
|
|
b2ab33bcd1 | ||
|
|
48be35cbf2 | ||
|
|
fd2cf223dc | ||
|
|
8155bc5ca8 | ||
|
|
cd6de86fb1 | ||
|
|
b2568efeb1 | ||
|
|
5805b859f5 | ||
|
|
d061f56951 | ||
|
|
c3e9cfe526 | ||
|
|
3cf87afa2d | ||
|
|
ea871c7520 | ||
|
|
e1ee540d27 | ||
|
|
bb812c7f02 | ||
|
|
6ba648ed17 | ||
|
|
05a66a140e | ||
|
|
52a68e28dd | ||
|
|
4829b393c5 | ||
|
|
b11766c973 | ||
|
|
c09bfead65 | ||
|
|
7b1db2614f | ||
|
|
973238e1a4 | ||
|
|
d4e22942b8 | ||
|
|
7c934cebe1 | ||
|
|
b134c73301 | ||
|
|
fce4fc1bec | ||
|
|
977f3d8005 | ||
|
|
edab6ea432 | ||
|
|
9e437797d9 | ||
|
|
25afda3b60 | ||
|
|
836297718b | ||
|
|
da8415f4aa | ||
|
|
68c1b05313 | ||
|
|
2a4dbabae5 | ||
|
|
aafe673811 | ||
|
|
adccf8fca5 | ||
|
|
d333672b95 | ||
|
|
0df313188d | ||
|
|
fd0427d2ad | ||
|
|
394030ad43 | ||
|
|
5f2e021560 | ||
|
|
17e1bf710b | ||
|
|
baf895939d | ||
|
|
2705849cf2 | ||
|
|
79bf977904 | ||
|
|
11041ef1e9 | ||
|
|
08e870c94c | ||
|
|
9492f3165e | ||
|
|
f14bb0f890 | ||
|
|
829d127269 | ||
|
|
f1f75d8cc0 | ||
|
|
3128783105 | ||
|
|
abd7338a08 | ||
|
|
c396698ca0 | ||
|
|
cd87438fb4 | ||
|
|
eafabb7f43 | ||
|
|
de315eacf5 | ||
|
|
af6b92a441 | ||
|
|
d2215ed91e | ||
|
|
20a470ddd1 | ||
|
|
9f58aac350 | ||
|
|
931a16c544 | ||
|
|
8f7e7280f9 | ||
|
|
8888b72d29 | ||
|
|
6cfca53dc8 | ||
|
|
1bfe030dd9 | ||
|
|
98ed657f8a | ||
|
|
c22518ce23 | ||
|
|
58921cfb7f | ||
|
|
dae3164d53 | ||
|
|
5ccf209af7 | ||
|
|
bb6fbfea37 | ||
|
|
edb2af4dd5 | ||
|
|
7c3b67fbfb | ||
|
|
bf2461fae1 | ||
|
|
f316e7cca0 | ||
|
|
3f70300afb | ||
|
|
2c52821e72 | ||
|
|
4ed4b8ba06 | ||
|
|
c3c6fb32cc | ||
|
|
6f3a7501af | ||
|
|
1b859ce769 | ||
|
|
46cb7db897 | ||
|
|
a29e4e61eb | ||
|
|
6a01a74608 | ||
|
|
945ffb1ed7 | ||
|
|
0a4c927277 | ||
|
|
c67b72068a | ||
|
|
5127d5a65a | ||
|
|
c97611482e | ||
|
|
f81f6cbf3d | ||
|
|
a079e68713 | ||
|
|
66cfc272c1 | ||
|
|
1871f69aca | ||
|
|
7034b1dd8a | ||
|
|
3e0a983048 | ||
|
|
826cf7b0b1 | ||
|
|
1b874ce9c1 | ||
|
|
0ad0e752f8 | ||
|
|
d0823b1acb | ||
|
|
474cae0a5b | ||
|
|
0d5e7e97cc | ||
|
|
a58d201b1b | ||
|
|
58346140a8 | ||
|
|
cb77be106a | ||
|
|
ced5d6099e | ||
|
|
9f2f1bad77 | ||
|
|
c7329136ac | ||
|
|
a63de5db4d | ||
|
|
543e271563 | ||
|
|
ee79b6c8c9 | ||
|
|
1f3f9d4bd1 | ||
|
|
ffb6d7b02a | ||
|
|
64df7b97ab | ||
|
|
a1f80e74b5 | ||
|
|
358accbcb7 | ||
|
|
8a273b31d2 | ||
|
|
15cab0bca8 | ||
|
|
865c681df0 | ||
|
|
86c7ca3ce8 | ||
|
|
77512147ef | ||
|
|
b85230b0ff | ||
|
|
3cb3b9a10b | ||
|
|
e4404d179e | ||
|
|
9c99578206 | ||
|
|
5c9332e61e | ||
|
|
a739926e17 | ||
|
|
fd79b04ff0 | ||
|
|
75869c06fd | ||
|
|
e11e4437c5 | ||
|
|
f0d245ef8d | ||
|
|
610697be5d | ||
|
|
a9a2819b19 | ||
|
|
ec6fb6c03c | ||
|
|
1f6ad51c99 | ||
|
|
0e8c9fbc5c | ||
|
|
0b6567d98f | ||
|
|
01320b4dec | ||
|
|
77da71cd62 | ||
|
|
6f336a82a5 | ||
|
|
35fe306c12 | ||
|
|
427ec9da73 | ||
|
|
9dc823f25c | ||
|
|
d99b54f8ff | ||
|
|
38591bf374 | ||
|
|
4ba2fa8d21 | ||
|
|
8b5ac435a6 | ||
|
|
89db6b0d3a | ||
|
|
59beb243c0 | ||
|
|
0373b757f7 | ||
|
|
cd7e9652ab | ||
|
|
748f930186 | ||
|
|
6cca0f244f | ||
|
|
82bfbd027a | ||
|
|
a0f60de926 | ||
|
|
763e410ce9 | ||
|
|
4e772c077b | ||
|
|
346c794244 | ||
|
|
63a8dfd5b9 | ||
|
|
bcfbb7d7a0 | ||
|
|
963ab6a586 | ||
|
|
f47f515f90 | ||
|
|
da00488a55 | ||
|
|
6501b6523b | ||
|
|
e965309e5c | ||
|
|
63c932561e | ||
|
|
6afff591ff | ||
|
|
f560c95cc2 | ||
|
|
4aa7a100e3 | ||
|
|
d7ed26c372 | ||
|
|
5574f8668a | ||
|
|
a6b9a4567c | ||
|
|
465c7280aa | ||
|
|
5c574f5ea3 | ||
|
|
2c05e1b70f | ||
|
|
ef64a15e37 | ||
|
|
1703244da8 | ||
|
|
bb600d1e98 | ||
|
|
ce1ff171ce | ||
|
|
5337636f43 | ||
|
|
0c6f28e70a | ||
|
|
a0d07f5e40 | ||
|
|
8642fad707 | ||
|
|
ac156ab220 | ||
|
|
cca4c0bc14 | ||
|
|
9412bf0df8 | ||
|
|
86fd55b17a | ||
|
|
6b494e1113 | ||
|
|
eaae38481e | ||
|
|
1e350e1369 | ||
|
|
50c96bc2ee | ||
|
|
962d45e4f9 | ||
|
|
cec169c9b2 | ||
|
|
08c61ad66b | ||
|
|
adffbc03a2 | ||
|
|
ebf3fc6c27 | ||
|
|
6e380814aa | ||
|
|
c93a550ddd | ||
|
|
1528672e0e | ||
|
|
34a1c78199 | ||
|
|
24d6da3af2 | ||
|
|
628f942984 | ||
|
|
aa05e57afc | ||
|
|
6f0137339a | ||
|
|
46ca21a37c | ||
|
|
31d306e95f | ||
|
|
7cf89e32f7 | ||
|
|
a09a875c92 | ||
|
|
3189c502c5 | ||
|
|
e91b6bcd6b | ||
|
|
b63853d7ce | ||
|
|
ef57d882b3 | ||
|
|
f5f09f22d4 | ||
|
|
5282265fd5 | ||
|
|
3ac77f5707 | ||
|
|
7a257bc9ca | ||
|
|
6e6c7a68bd | ||
|
|
0d5a7ba19f | ||
|
|
2a81710d07 | ||
|
|
c57b2ef725 | ||
|
|
cb2caa7ef1 | ||
|
|
687d088827 | ||
|
|
42e37517b1 | ||
|
|
0e47662353 | ||
|
|
67ab6887b4 | ||
|
|
8113c4888d | ||
|
|
877cf88806 | ||
|
|
283453b387 | ||
|
|
bf435b88b3 | ||
|
|
0a8c3b2b0f | ||
|
|
c62a079de9 | ||
|
|
1b75cdf65f | ||
|
|
0046bfaf46 | ||
|
|
9b28d42814 | ||
|
|
da0ec57856 | ||
|
|
7da6a5d8a5 | ||
|
|
3e5ebc7c32 | ||
|
|
9cfcf67c72 | ||
|
|
3ecee031be | ||
|
|
bf2752496a | ||
|
|
78dc2fedeb | ||
|
|
4856b9a540 | ||
|
|
9bebfd6415 | ||
|
|
de616021e2 | ||
|
|
ca145ba681 | ||
|
|
df7a8df19a | ||
|
|
84e44f5aee | ||
|
|
72f4da6bc4 | ||
|
|
9db68aa9e4 | ||
|
|
8bc9d02e4c | ||
|
|
7464ce1412 | ||
|
|
935ad9b7c2 | ||
|
|
483ad69678 | ||
|
|
8f6b8d763a | ||
|
|
28335c8deb | ||
|
|
f4374ed54a | ||
|
|
803d180a9b | ||
|
|
43b4aeac51 | ||
|
|
6dbe908a66 | ||
|
|
3b05afd21e | ||
|
|
2c0709fa38 | ||
|
|
6b8414ccdc | ||
|
|
fe6701c82c | ||
|
|
14e2eefc42 | ||
|
|
4778d48b67 | ||
|
|
0babc81914 | ||
|
|
e63a525671 | ||
|
|
a377c1631a | ||
|
|
bec8d89acc | ||
|
|
b3a72ee2aa | ||
|
|
786450c8e9 | ||
|
|
3443fe4e4f | ||
|
|
d03cc90a29 | ||
|
|
986b329178 | ||
|
|
4f30dce4ec | ||
|
|
90c04ee62e | ||
|
|
708ebb1a7f | ||
|
|
1e144e5657 | ||
|
|
7523102982 | ||
|
|
04a6b10b39 | ||
|
|
4bbfee5e8f | ||
|
|
d446e3612f | ||
|
|
3b7cb49d89 | ||
|
|
17e20a6a7c | ||
|
|
65342e0b55 | ||
|
|
3bd0a35c61 | ||
|
|
dbe298f2a7 | ||
|
|
878506e34f | ||
|
|
86beaaaaa4 | ||
|
|
38ae10db78 | ||
|
|
bace668739 | ||
|
|
eaa8429bd8 | ||
|
|
9a186ffea9 | ||
|
|
007092f875 | ||
|
|
5c531fcbee | ||
|
|
144fd83167 | ||
|
|
f8c47fcb86 | ||
|
|
b226698760 | ||
|
|
fa4803373b | ||
|
|
c4487433cd | ||
|
|
97b456d9b7 | ||
|
|
1f298f8e79 | ||
|
|
ca4ccfc3ae | ||
|
|
5354d779c1 | ||
|
|
c1fc0d1ce3 | ||
|
|
c3301f9384 | ||
|
|
88601fb952 | ||
|
|
a3108e6c3a | ||
|
|
557529b1e7 | ||
|
|
5b64ca7c7b | ||
|
|
fb9272cd6c | ||
|
|
f858a3eb07 | ||
|
|
db9f43334d | ||
|
|
4c913d5be0 | ||
|
|
c1cfdad0a7 | ||
|
|
d3b7767374 | ||
|
|
746ed9638b | ||
|
|
8429c795a0 | ||
|
|
116cb16538 | ||
|
|
e3c4e52981 | ||
|
|
2f4171b9e2 | ||
|
|
d1596fa7c5 | ||
|
|
7b60c1e067 | ||
|
|
545c5a223f | ||
|
|
e27f1afb1a | ||
|
|
94d7edd707 | ||
|
|
52c15eeb60 | ||
|
|
4939a7da25 | ||
|
|
ba653037b1 | ||
|
|
15b23c13e7 | ||
|
|
f293a2843f | ||
|
|
a9c3e85df8 | ||
|
|
2e36b8125e | ||
|
|
d3d56d3454 | ||
|
|
5939e20399 | ||
|
|
a371bb71a9 | ||
|
|
d30f4db894 | ||
|
|
18d6bd262a | ||
|
|
892f1cab2d | ||
|
|
dff18d2b6d | ||
|
|
407b801243 | ||
|
|
a5acf30600 | ||
|
|
36a8739479 | ||
|
|
6836fb5bae | ||
|
|
bb418038e2 | ||
|
|
c0ffa10fff | ||
|
|
5aa8420513 | ||
|
|
19d438f566 | ||
|
|
48c257ac81 | ||
|
|
93ffb83e94 | ||
|
|
6cd2f4e0dc | ||
|
|
9f27d3bc0e | ||
|
|
52c39dbc4d | ||
|
|
5709320415 | ||
|
|
99d8f11c9b | ||
|
|
6f33ed5824 | ||
|
|
6d2208a0f8 | ||
|
|
a8e5e3ab09 | ||
|
|
fb11f6dec7 | ||
|
|
090c84df61 | ||
|
|
a04ad250c3 | ||
|
|
356d843a9b | ||
|
|
fb5036acf8 | ||
|
|
d709204107 | ||
|
|
df0c307be4 | ||
|
|
c16562c8ca | ||
|
|
cc007afceb | ||
|
|
a58835e3ad | ||
|
|
61440ef994 | ||
|
|
c6875c44b6 | ||
|
|
94cbdbe159 | ||
|
|
d70105e30e | ||
|
|
aa5dc88114 | ||
|
|
a592d6e2ad | ||
|
|
2da455bc2f | ||
|
|
c0b3a04110 | ||
|
|
812a773fba | ||
|
|
0c567de380 | ||
|
|
7695a800c6 | ||
|
|
a8176cb276 | ||
|
|
e5f232b7b2 | ||
|
|
f81830230f |
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,6 +8,7 @@
|
||||
## Build generated
|
||||
build/
|
||||
DerivedData
|
||||
.build
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
|
||||
21
.gitmodules
vendored
21
.gitmodules
vendored
@ -13,3 +13,24 @@
|
||||
[submodule "Cores/GBCDeltaCore"]
|
||||
path = Cores/GBCDeltaCore
|
||||
url = git@github.com:rileytestut/GBCDeltaCore.git
|
||||
[submodule "External/Harmony"]
|
||||
path = External/Harmony
|
||||
url = https://github.com/rileytestut/Harmony.git
|
||||
[submodule "Cores/NESDeltaCore"]
|
||||
path = Cores/NESDeltaCore
|
||||
url = git@github.com:rileytestut/NESDeltaCore.git
|
||||
[submodule "Cores/N64DeltaCore"]
|
||||
path = Cores/N64DeltaCore
|
||||
url = git@github.com:rileytestut/N64DeltaCore.git
|
||||
[submodule "Cores/DSDeltaCore"]
|
||||
path = Cores/DSDeltaCore
|
||||
url = https://github.com/rileytestut/DSDeltaCore.git
|
||||
[submodule "Cores/MelonDSDeltaCore"]
|
||||
path = Cores/MelonDSDeltaCore
|
||||
url = https://github.com/rileytestut/MelonDSDeltaCore.git
|
||||
[submodule "Cores/GPGXDeltaCore"]
|
||||
path = Cores/GPGXDeltaCore
|
||||
url = https://github.com/rileytestut/GPGXDeltaCore.git
|
||||
[submodule "External/CheatBase"]
|
||||
path = External/CheatBase
|
||||
url = https://github.com/rileytestut/CheatBase.git
|
||||
|
||||
5168
Artwork/Icon.ai
Normal file
5168
Artwork/Icon.ai
Normal file
File diff suppressed because one or more lines are too long
Binary file not shown.
1
Cores/DSDeltaCore
Submodule
1
Cores/DSDeltaCore
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 6c84366b3a76045782905293c9616e33f5da1a35
|
||||
@ -1 +1 @@
|
||||
Subproject commit caaf2e8e4bd847a2442382e121b07b4d5f139000
|
||||
Subproject commit c1db5f51cd455a7033801cc19dc3dbfcb6f2b42c
|
||||
@ -1 +1 @@
|
||||
Subproject commit 4eedbd481457e3c236d1f3ffab3ba2f49c6d18df
|
||||
Subproject commit 8ea36dff87bc1f787765de45fa8ccbcc1256a0e3
|
||||
@ -1 +1 @@
|
||||
Subproject commit 0e622c7887e781363e36f6c7bfcd67fba3cb00ac
|
||||
Subproject commit 81f8ffba56823637706689fb5c6bc634ee4d9b32
|
||||
1
Cores/GPGXDeltaCore
Submodule
1
Cores/GPGXDeltaCore
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 18c595887a12ef23e0d54c63f83c91c99e7f4827
|
||||
1
Cores/MelonDSDeltaCore
Submodule
1
Cores/MelonDSDeltaCore
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 697ba731981824f53460f6e0193f159f71f22ba2
|
||||
1
Cores/N64DeltaCore
Submodule
1
Cores/N64DeltaCore
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit c8816c51f82210a9c4cc62b1a7c53fa21bc705ee
|
||||
1
Cores/NESDeltaCore
Submodule
1
Cores/NESDeltaCore
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 78a092d4e795f83153e98749b5cbeb66cf812d7e
|
||||
@ -1 +1 @@
|
||||
Subproject commit bdc95aeb5e7f50263c34187cbe38ddddb4a2576e
|
||||
Subproject commit d5717291325578f64d519822aeb2be81217c67f3
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,9 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "0830"
|
||||
LastUpgradeVersion = "1020"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "NO"
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
@ -14,52 +14,10 @@
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFADAFF719AE7BB70050CF31"
|
||||
BuildableName = "Roxas.framework"
|
||||
BlueprintName = "Roxas"
|
||||
ReferencedContainer = "container:External/Roxas/Roxas.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF46895C1AACF36800A2586D"
|
||||
BuildableName = "DeltaCore.framework"
|
||||
BlueprintName = "DeltaCore"
|
||||
ReferencedContainer = "container:Cores/DeltaCore/DeltaCore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF9F4FDB1AAD8070004C9500"
|
||||
BuildableName = "SNESDeltaCore.framework"
|
||||
BlueprintName = "SNESDeltaCore"
|
||||
ReferencedContainer = "container:Cores/SNESDeltaCore/SNESDeltaCore.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFE8E9C91D010AF7009D623D"
|
||||
BuildableName = "GBADeltaCore.framework"
|
||||
BlueprintName = "GBADeltaCore"
|
||||
ReferencedContainer = "container:Cores/GBADeltaCore/GBADeltaCore.xcodeproj">
|
||||
BlueprintIdentifier = "33C94426DAF58519DC6806AF4C44C9E7"
|
||||
BuildableName = "libPods-Delta.a"
|
||||
BlueprintName = "Pods-Delta"
|
||||
ReferencedContainer = "container:Pods/Pods.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
@ -71,7 +29,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
|
||||
BuildableName = "Delta.app"
|
||||
BuildableName = "Retro Game Emulator.app"
|
||||
BlueprintName = "Delta"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
@ -83,24 +41,23 @@
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
|
||||
BuildableName = "Delta.app"
|
||||
BuildableName = "Retro Game Emulator.app"
|
||||
BlueprintName = "Delta"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
buildConfiguration = "Release"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
enableASanStackUseAfterReturn = "YES"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
@ -112,12 +69,20 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
|
||||
BuildableName = "Delta.app"
|
||||
BuildableName = "Retro Game Emulator.app"
|
||||
BlueprintName = "Delta"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.MigrationDebug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.rileytestut.Harmony.Debug 1"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
<CommandLineArgument
|
||||
argument = "-com.apple.CoreData.ConcurrencyDebug 1"
|
||||
isEnabled = "YES">
|
||||
@ -127,11 +92,9 @@
|
||||
<EnvironmentVariable
|
||||
key = "OS_ACTIVITY_MODE"
|
||||
value = "disable"
|
||||
isEnabled = "YES">
|
||||
isEnabled = "NO">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
@ -144,7 +107,7 @@
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BFFA71D61AAC406100EE9DD1"
|
||||
BuildableName = "Delta.app"
|
||||
BuildableName = "Retro Game Emulator.app"
|
||||
BlueprintName = "Delta"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1420"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D5D7C1F029E60DFF00663793"
|
||||
BuildableName = "libDeltaFeatures.a"
|
||||
BlueprintName = "DeltaFeatures"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D5D7C1F029E60DFF00663793"
|
||||
BuildableName = "libDeltaFeatures.a"
|
||||
BlueprintName = "DeltaFeatures"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D539102E29E88B6B0006B350"
|
||||
BuildableName = "DeltaPreviews.framework"
|
||||
BlueprintName = "DeltaPreviews"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "D539102E29E88B6B0006B350"
|
||||
BuildableName = "DeltaPreviews.framework"
|
||||
BlueprintName = "DeltaPreviews"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
67
Delta.xcodeproj/xcshareddata/xcschemes/Systems.xcscheme
Normal file
67
Delta.xcodeproj/xcshareddata/xcschemes/Systems.xcscheme
Normal file
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1250"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF6E70B925D2187800E41CD1"
|
||||
BuildableName = "Systems"
|
||||
BlueprintName = "Systems"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF6E70B925D2187800E41CD1"
|
||||
BuildableName = "Systems"
|
||||
BlueprintName = "Systems"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
66
Delta.xcodeproj/xcshareddata/xcschemes/mogenerator.xcscheme
Normal file
66
Delta.xcodeproj/xcshareddata/xcschemes/mogenerator.xcscheme
Normal file
@ -0,0 +1,66 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF14D8941DE7A512002CA1BE"
|
||||
BuildableName = "mogenerator"
|
||||
BlueprintName = "mogenerator"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "BF14D8941DE7A512002CA1BE"
|
||||
BuildableName = "mogenerator"
|
||||
BlueprintName = "mogenerator"
|
||||
ReferencedContainer = "container:Delta.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
20
Delta.xcworkspace/contents.xcworkspacedata
generated
20
Delta.xcworkspace/contents.xcworkspacedata
generated
@ -7,14 +7,32 @@
|
||||
<FileRef
|
||||
location = "group:Cores/DeltaCore/DeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/NESDeltaCore/NESDeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/SNESDeltaCore/SNESDeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/GBCDeltaCore/GBCDeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/GBADeltaCore/GBADeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/GBCDeltaCore/GBCDeltaCore.xcodeproj">
|
||||
location = "group:Cores/N64DeltaCore/N64DeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/MelonDSDeltaCore/MelonDSDeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/DSDeltaCore/DSDeltaCore.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Cores/GPGXDeltaCore">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/Harmony/Harmony.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:External/Roxas/Roxas.xcodeproj">
|
||||
|
||||
8
Delta.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
Normal file
8
Delta.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
Normal file
@ -0,0 +1,8 @@
|
||||
<?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>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -4,5 +4,7 @@
|
||||
<dict>
|
||||
<key>IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded</key>
|
||||
<false/>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
34
Delta.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
34
Delta.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
@ -0,0 +1,34 @@
|
||||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "AltKit",
|
||||
"repositoryURL": "https://github.com/rileytestut/AltKit.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "2fd376df1c79ec06a5c80cc8933da027f65b3148",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "DeltaCore",
|
||||
"repositoryURL": "https://github.com/rileytestut/DeltaCore.git",
|
||||
"state": {
|
||||
"branch": "ios14",
|
||||
"revision": "74d2a7a6e36035cb5730d0b0cf2456cbeb6faf0c",
|
||||
"version": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "ZIPFoundation",
|
||||
"repositoryURL": "https://github.com/weichsel/ZIPFoundation.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "02b6abe5f6eef7e3cbd5f247c5cc24e246efcfe0",
|
||||
"version": "0.9.19"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@ -9,38 +9,57 @@
|
||||
import UIKit
|
||||
|
||||
import DeltaCore
|
||||
import Harmony
|
||||
import AltKit
|
||||
|
||||
import Fabric
|
||||
import Crashlytics
|
||||
private extension CFNotificationName
|
||||
{
|
||||
static let altstoreRequestAppState: CFNotificationName = CFNotificationName("com.altstore.RequestAppState.com.rileytestut.Delta" as CFString)
|
||||
static let altstoreAppIsRunning: CFNotificationName = CFNotificationName("com.altstore.AppState.Running.com.rileytestut.Delta" as CFString)
|
||||
}
|
||||
|
||||
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void =
|
||||
{ (center, observer, name, object, userInfo) in
|
||||
guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return }
|
||||
appDelegate.receivedApplicationStateRequest()
|
||||
}
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate
|
||||
{
|
||||
var window: UIWindow?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
Fabric.with([Crashlytics.self])
|
||||
private let deepLinkController = DeepLinkController()
|
||||
private var appLaunchDeepLink: DeepLink?
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
|
||||
{
|
||||
Settings.registerDefaults()
|
||||
|
||||
System.supportedSystems.forEach { Delta.register($0.deltaCore) }
|
||||
|
||||
self.registerCores()
|
||||
self.configureAppearance()
|
||||
|
||||
// Disable system gestures that delay touches on left edge of screen
|
||||
for gestureRecognizer in self.window?.gestureRecognizers ?? [] where NSStringFromClass(type(of: gestureRecognizer)).contains("GateGesture")
|
||||
{
|
||||
gestureRecognizer.delaysTouchesBegan = false
|
||||
}
|
||||
|
||||
// Database
|
||||
|
||||
DatabaseManager.shared.loadPersistentStores { (description, error) in
|
||||
}
|
||||
|
||||
// Controllers
|
||||
ExternalControllerManager.shared.startMonitoringExternalControllers()
|
||||
ExternalGameControllerManager.shared.startMonitoring()
|
||||
|
||||
// JIT
|
||||
ServerManager.shared.prepare()
|
||||
|
||||
// Notifications
|
||||
let center = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
CFNotificationCenterAddObserver(center, nil, ReceivedApplicationState, CFNotificationName.altstoreRequestAppState.rawValue, nil, .deliverImmediately)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppDelegate.databaseManagerDidStart(_:)), name: DatabaseManager.didStartNotification, object: DatabaseManager.shared)
|
||||
|
||||
|
||||
// Deep Links
|
||||
if let shortcut = launchOptions?[.shortcutItem] as? UIApplicationShortcutItem
|
||||
{
|
||||
self.appLaunchDeepLink = .shortcut(shortcut)
|
||||
|
||||
// false = we handled the deep link, so no need to call delegate method separately.
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@ -73,27 +92,75 @@ class AppDelegate: UIResponder, UIApplicationDelegate
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
extension AppDelegate
|
||||
{
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration
|
||||
{
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
|
||||
if connectingSceneSession.role == .windowExternalDisplay
|
||||
{
|
||||
// External Display
|
||||
return UISceneConfiguration(name: "External Display", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default Scene
|
||||
return UISceneConfiguration(name: "Main", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>)
|
||||
{
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate
|
||||
{
|
||||
func registerCores()
|
||||
{
|
||||
#if LITE
|
||||
|
||||
#if BETA
|
||||
Delta.register(System.nes.deltaCore)
|
||||
Delta.register(System.gbc.deltaCore)
|
||||
#else
|
||||
Delta.register(System.nes.deltaCore)
|
||||
#endif
|
||||
|
||||
#else
|
||||
|
||||
#if BETA
|
||||
System.allCases.forEach { Delta.register($0.deltaCore) }
|
||||
#else
|
||||
System.allCases.filter { $0 != .genesis }.forEach { Delta.register($0.deltaCore) }
|
||||
#endif
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
func configureAppearance()
|
||||
{
|
||||
self.window?.tintColor = UIColor.deltaPurple
|
||||
|
||||
UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes[NSForegroundColorAttributeName] = UIColor.white
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate
|
||||
{
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool
|
||||
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool
|
||||
{
|
||||
return self.openURL(url)
|
||||
}
|
||||
|
||||
@discardableResult fileprivate func openURL(_ url: URL) -> Bool
|
||||
@discardableResult private func openURL(_ url: URL) -> Bool
|
||||
{
|
||||
if url.isFileURL
|
||||
{
|
||||
guard url.isFileURL else { return false }
|
||||
|
||||
if GameType(fileExtension: url.pathExtension) != nil || url.pathExtension.lowercased() == "zip"
|
||||
{
|
||||
return self.importGame(at: url)
|
||||
@ -102,6 +169,15 @@ extension AppDelegate
|
||||
{
|
||||
return self.importControllerSkin(at: url)
|
||||
}
|
||||
}
|
||||
else if url.scheme?.hasPrefix("db-") == true
|
||||
{
|
||||
return DropboxService.shared.handleDropboxURL(url)
|
||||
}
|
||||
else if url.scheme?.lowercased() == "delta"
|
||||
{
|
||||
return self.deepLinkController.handle(.url(url))
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@ -145,3 +221,30 @@ extension AppDelegate
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate
|
||||
{
|
||||
func application(_ application: UIApplication, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void)
|
||||
{
|
||||
let result = self.deepLinkController.handle(.shortcut(shortcutItem))
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDelegate
|
||||
{
|
||||
@objc func databaseManagerDidStart(_ notification: Notification)
|
||||
{
|
||||
guard let deepLink = self.appLaunchDeepLink else { return }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.deepLinkController.handle(deepLink)
|
||||
}
|
||||
}
|
||||
|
||||
func receivedApplicationStateRequest()
|
||||
{
|
||||
let center = CFNotificationCenterGetDarwinNotifyCenter()
|
||||
CFNotificationCenterPostNotification(center!, CFNotificationName(CFNotificationName.altstoreAppIsRunning.rawValue), nil, nil, true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,12 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="6bq-zy-UZU">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15705" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="6bq-zy-UZU">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
|
||||
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15706"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@ -18,52 +15,6 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="4cJ-4B-Kgt" customClass="GameMetadataTableViewCell" customModule="Delta" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="97"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="4cJ-4B-Kgt" id="7ze-s0-mpI">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DSH-Hk-snb" userLabel="Selected Background View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="97.5"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BoxArt" translatesAutoresizingMaskIntoConstraints="NO" id="tNY-2F-llo">
|
||||
<rect key="frame" x="15" y="8" width="80" height="80"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="tNY-2F-llo" secondAttribute="height" multiplier="1:1" id="f4E-bV-L96"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Super Mario World" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DND-Fv-FyB">
|
||||
<rect key="frame" x="110" y="38.5" width="250" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="DND-Fv-FyB" firstAttribute="leading" secondItem="tNY-2F-llo" secondAttribute="trailing" constant="15" id="71e-t3-7Av"/>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="DND-Fv-FyB" secondAttribute="bottom" constant="1" id="9No-RE-0xx"/>
|
||||
<constraint firstItem="DND-Fv-FyB" firstAttribute="top" relation="greaterThanOrEqual" secondItem="7ze-s0-mpI" secondAttribute="top" constant="1" id="F9q-6H-sqC"/>
|
||||
<constraint firstAttribute="trailing" secondItem="DND-Fv-FyB" secondAttribute="trailing" constant="15" id="KFv-7n-LrD"/>
|
||||
<constraint firstItem="DND-Fv-FyB" firstAttribute="centerY" secondItem="7ze-s0-mpI" secondAttribute="centerY" id="YBX-t4-jkR"/>
|
||||
<constraint firstItem="tNY-2F-llo" firstAttribute="top" secondItem="7ze-s0-mpI" secondAttribute="top" constant="8" id="bYX-gA-QvB"/>
|
||||
<constraint firstAttribute="bottom" secondItem="tNY-2F-llo" secondAttribute="bottom" constant="8" id="fxr-wr-I6X"/>
|
||||
<constraint firstItem="tNY-2F-llo" firstAttribute="leading" secondItem="7ze-s0-mpI" secondAttribute="leading" constant="15" id="hX2-Gr-Bnz"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="artworkImageView" destination="tNY-2F-llo" id="GqY-jv-rso"/>
|
||||
<outlet property="artworkImageViewLeadingConstraint" destination="hX2-Gr-Bnz" id="be8-dr-c8K"/>
|
||||
<outlet property="artworkImageViewTrailingConstraint" destination="71e-t3-7Av" id="y62-KO-y1r"/>
|
||||
<outlet property="nameLabel" destination="DND-Fv-FyB" id="LhN-cA-8Hy"/>
|
||||
<outlet property="selectedBackgroundView" destination="DSH-Hk-snb" id="hLY-4k-VxU"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="SB6-jW-dhZ" id="2aq-ZA-84E"/>
|
||||
<outlet property="delegate" destination="SB6-jW-dhZ" id="WgY-cp-m7K"/>
|
||||
@ -72,7 +23,7 @@
|
||||
<navigationItem key="navigationItem" title="Games Database" id="rwF-kd-avR">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="BnB-5n-Rff">
|
||||
<connections>
|
||||
<segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindFromGamesDatabaseBrowserWith:" id="zdg-Az-WwQ"/>
|
||||
<segue destination="mUU-ug-yNs" kind="unwind" unwindAction="unwindToGameCollectionViewController:" id="nzI-4n-kDg"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
@ -101,7 +52,4 @@
|
||||
<point key="canvasLocation" x="1854" y="1002"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="BoxArt" width="100" height="100"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11760" systemVersion="16B2657" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="dkK-ii-Bx4">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="dkK-ii-Bx4">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11755"/>
|
||||
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
|
||||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@ -23,21 +19,32 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="Delta" translatesAutoresizingMaskIntoConstraints="NO" id="plh-tL-LY0">
|
||||
<rect key="frame" x="94" y="250" width="187.5" height="167"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="plh-tL-LY0" secondAttribute="height" multiplier="64:57" id="8qM-L2-ASa"/>
|
||||
</constraints>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" insetsLayoutMarginsFromSafeArea="NO" image="LaunchViewC" translatesAutoresizingMaskIntoConstraints="NO" id="5XD-I3-tLg">
|
||||
<rect key="frame" x="0.0" y="50" width="375" height="378"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Retro Game Emulator" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vhb-Xd-o6a">
|
||||
<rect key="frame" x="55.5" y="448" width="264" height="33.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="28"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" alpha="0.44999998807907104" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="ENJOY CLASSIC MOMENT" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wWH-Lx-U9x">
|
||||
<rect key="frame" x="90.5" y="513.5" width="194.5" height="19.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.14728124936421713" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="plh-tL-LY0" firstAttribute="width" relation="lessThanOrEqual" secondItem="8Uu-wz-ps8" secondAttribute="width" multiplier="0.5" id="8j9-39-Y2s"/>
|
||||
<constraint firstItem="plh-tL-LY0" firstAttribute="centerY" secondItem="8Uu-wz-ps8" secondAttribute="centerY" id="COW-Co-NFK"/>
|
||||
<constraint firstItem="plh-tL-LY0" firstAttribute="height" secondItem="8Uu-wz-ps8" secondAttribute="height" multiplier="0.5" priority="900" id="G3L-7B-xVc"/>
|
||||
<constraint firstItem="plh-tL-LY0" firstAttribute="width" secondItem="8Uu-wz-ps8" secondAttribute="width" multiplier="0.5" priority="950" id="n3i-kS-7eQ"/>
|
||||
<constraint firstItem="plh-tL-LY0" firstAttribute="centerX" secondItem="8Uu-wz-ps8" secondAttribute="centerX" id="sp5-Kf-N7G"/>
|
||||
<constraint firstItem="plh-tL-LY0" firstAttribute="height" relation="lessThanOrEqual" secondItem="8Uu-wz-ps8" secondAttribute="height" multiplier="0.5" id="ubN-Qh-I5H"/>
|
||||
<constraint firstItem="5XD-I3-tLg" firstAttribute="top" secondItem="qMb-3x-uIu" secondAttribute="bottom" constant="30" id="Aiv-ac-bYx"/>
|
||||
<constraint firstItem="wWH-Lx-U9x" firstAttribute="centerX" secondItem="vhb-Xd-o6a" secondAttribute="centerX" id="DEu-U7-qVq"/>
|
||||
<constraint firstAttribute="trailing" secondItem="5XD-I3-tLg" secondAttribute="trailing" id="Gc5-y0-Vsy"/>
|
||||
<constraint firstItem="vhb-Xd-o6a" firstAttribute="top" secondItem="5XD-I3-tLg" secondAttribute="bottom" constant="20" id="Ncn-Yh-ecr"/>
|
||||
<constraint firstItem="5XD-I3-tLg" firstAttribute="leading" secondItem="8Uu-wz-ps8" secondAttribute="leading" id="SSl-CS-XOC"/>
|
||||
<constraint firstItem="5XD-I3-tLg" firstAttribute="height" secondItem="8Uu-wz-ps8" secondAttribute="height" multiplier="1.7:3" id="WGx-z3-vXf"/>
|
||||
<constraint firstItem="wWH-Lx-U9x" firstAttribute="top" secondItem="vhb-Xd-o6a" secondAttribute="bottom" constant="32" id="lVY-5z-fz7"/>
|
||||
<constraint firstItem="vhb-Xd-o6a" firstAttribute="centerX" secondItem="8Uu-wz-ps8" secondAttribute="centerX" id="oCf-dA-bJX"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
@ -47,6 +54,6 @@
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Delta" width="1280" height="1140"/>
|
||||
<image name="LaunchViewC" width="375" height="472.5"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@ -1,74 +1,104 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12120" systemVersion="16E195" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="SPq-Bk-fQl">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12088"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Games-->
|
||||
<!--Games View Controller-->
|
||||
<scene sceneID="Cd2-Pf-cua">
|
||||
<objects>
|
||||
<viewController id="jeE-WD-wXO" customClass="GamesViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController id="jeE-WD-wXO" customClass="GamesViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="WoX-O4-qy5"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="0om-QB-N5a"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="3Bk-k3-7J9">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="tmn-gd-5UN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||
<connections>
|
||||
<segue destination="tpK-ou-yEA" kind="embed" identifier="embedPageViewController" id="cjU-nW-cHY"/>
|
||||
</connections>
|
||||
</containerView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bg" translatesAutoresizingMaskIntoConstraints="NO" id="J8K-ZI-4X1">
|
||||
<rect key="frame" x="0.0" y="56" width="375" height="591"/>
|
||||
</imageView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="CxB-GP-B6S">
|
||||
<rect key="frame" x="0.0" y="538" width="375" height="109"/>
|
||||
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
||||
<state key="normal" image="home_tab"/>
|
||||
<connections>
|
||||
<segue destination="xMK-Cs-fAS" kind="presentation" modalPresentationStyle="fullScreen" id="bxS-9X-3vh"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="n4H-Kw-HPj">
|
||||
<rect key="frame" x="143.5" y="517" width="100" height="100"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="100" id="SLR-9h-t9y"/>
|
||||
<constraint firstAttribute="width" constant="100" id="Tdv-6u-nlA"/>
|
||||
</constraints>
|
||||
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
||||
<state key="normal" image="addtwo"/>
|
||||
<connections>
|
||||
<action selector="importfilesBtn:" destination="jeE-WD-wXO" eventType="touchUpInside" id="T2w-zr-j8V"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="CxB-GP-B6S" secondAttribute="trailing" id="5sO-AB-YB5"/>
|
||||
<constraint firstAttribute="trailing" secondItem="J8K-ZI-4X1" secondAttribute="trailing" id="7MY-qA-ANn"/>
|
||||
<constraint firstAttribute="trailing" secondItem="tmn-gd-5UN" secondAttribute="trailing" id="9Rq-HM-vqk"/>
|
||||
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="tmn-gd-5UN" secondAttribute="bottom" id="DV5-hh-1VN"/>
|
||||
<constraint firstItem="J8K-ZI-4X1" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="9kW-o9-BMv"/>
|
||||
<constraint firstItem="CxB-GP-B6S" firstAttribute="centerX" secondItem="3Bk-k3-7J9" secondAttribute="centerX" id="Dbp-v1-mUp"/>
|
||||
<constraint firstItem="n4H-Kw-HPj" firstAttribute="centerX" secondItem="3Bk-k3-7J9" secondAttribute="centerX" constant="6" id="H9T-X7-RK8"/>
|
||||
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="n4H-Kw-HPj" secondAttribute="bottom" constant="30" id="QHB-jA-R0s"/>
|
||||
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="CxB-GP-B6S" secondAttribute="bottom" id="QzJ-Kr-VZk"/>
|
||||
<constraint firstItem="CxB-GP-B6S" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="axc-ed-3zE"/>
|
||||
<constraint firstItem="tmn-gd-5UN" firstAttribute="leading" secondItem="3Bk-k3-7J9" secondAttribute="leading" id="f1f-sa-dBA"/>
|
||||
<constraint firstAttribute="bottom" secondItem="tmn-gd-5UN" secondAttribute="bottom" id="ifM-Wa-u9y"/>
|
||||
<constraint firstItem="J8K-ZI-4X1" firstAttribute="top" secondItem="WoX-O4-qy5" secondAttribute="bottom" id="jaI-AF-tpn"/>
|
||||
<constraint firstItem="tmn-gd-5UN" firstAttribute="top" secondItem="3Bk-k3-7J9" secondAttribute="top" id="nhS-aC-rUR"/>
|
||||
<constraint firstItem="0om-QB-N5a" firstAttribute="top" secondItem="J8K-ZI-4X1" secondAttribute="bottom" id="tvh-Sd-zA1"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" title="Games" id="pFk-as-3k4">
|
||||
<barButtonItem key="leftBarButtonItem" image="Settings_Button" id="2gg-lC-FhX">
|
||||
<connections>
|
||||
<segue destination="xMK-Cs-fAS" kind="presentation" id="uN5-PN-7FK"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="add" id="FeA-O5-xd2">
|
||||
<navigationItem key="navigationItem" id="pFk-as-3k4">
|
||||
<barButtonItem key="leftBarButtonItem" image="home" id="2gg-lC-FhX"/>
|
||||
<barButtonItem key="rightBarButtonItem" style="plain" id="FeA-O5-xd2">
|
||||
<connections>
|
||||
<action selector="importFiles" destination="jeE-WD-wXO" id="A1s-kE-NkM"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="importButton" destination="FeA-O5-xd2" id="A44-3S-Okz"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="JYx-xE-nis" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1036" y="1002"/>
|
||||
<point key="canvasLocation" x="1036" y="1001.649175412294"/>
|
||||
</scene>
|
||||
<!--Game Collection View Controller-->
|
||||
<scene sceneID="qNA-NP-TiF">
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="gameCollectionViewController" clearsSelectionOnViewWillAppear="NO" id="kqu-75-owz" customClass="GameCollectionViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="OIq-Z8-kxO">
|
||||
<collectionViewController storyboardIdentifier="gameCollectionViewController" clearsSelectionOnViewWillAppear="NO" id="kqu-75-owz" customClass="GameCollectionViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" indicatorStyle="white" dataMode="prototypes" id="OIq-Z8-kxO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="10" id="NKN-dd-bTh" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="10" id="NKN-dd-bTh" customClass="GridCollectionViewLayout" customModule="Retro_Game_Emulator" customModuleProvider="target">
|
||||
<size key="itemSize" width="100" height="100"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="20" maxX="0.0" maxY="20"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="ioT-sh-j8y" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target">
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="ioT-sh-j8y" customClass="GridCollectionViewCell" customModule="Retro_Game_Emulator" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="20" width="100" height="100"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
@ -84,7 +114,9 @@
|
||||
</collectionView>
|
||||
<connections>
|
||||
<segue destination="X2o-q6-XD5" kind="unwind" identifier="unwindFromGames" unwindAction="unwindFromGamesViewControllerWith:" id="k8C-Xn-maU"/>
|
||||
<segue destination="MPk-bF-nkj" kind="presentation" identifier="saveStates" customClass="SaveStatesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="1Xp-2J-0cq"/>
|
||||
<segue destination="MPk-bF-nkj" kind="presentation" identifier="saveStates" customClass="SaveStatesStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="1Xp-2J-0cq"/>
|
||||
<segue destination="qdE-gb-V2e" kind="presentation" identifier="preferredControllerSkins" id="i6y-cP-3WM"/>
|
||||
<segue destination="V2x-v0-jWm" kind="presentation" identifier="showDSSettings" id="kuV-tY-Y0B"/>
|
||||
</connections>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="bW1-t8-idm" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
@ -95,7 +127,7 @@
|
||||
<!--Launch View Controller-->
|
||||
<scene sceneID="p7y-IT-nlb">
|
||||
<objects>
|
||||
<viewController id="SPq-Bk-fQl" customClass="LaunchViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController id="SPq-Bk-fQl" customClass="LaunchViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Qap-U8-zpQ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="dca-QO-wba"/>
|
||||
@ -114,12 +146,38 @@
|
||||
<containerView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="vf5-Iy-lAb" userLabel="Launch Screen">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<connections>
|
||||
<segue destination="ibA-aC-X3M" kind="embed" id="fsv-uf-AOE"/>
|
||||
</connections>
|
||||
</containerView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Mgn-IF-ax7">
|
||||
<rect key="frame" x="187.5" y="448" width="0.0" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="28"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" alpha="0.44999998807907104" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="wnW-MB-udh">
|
||||
<rect key="frame" x="187.5" y="480" width="0.0" height="0.0"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="50O-wR-1hq">
|
||||
<rect key="frame" x="0.0" y="50" width="375" height="378"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="50O-wR-1hq" secondAttribute="trailing" id="2Xh-ZZ-Dbc"/>
|
||||
<constraint firstItem="50O-wR-1hq" firstAttribute="leading" secondItem="8jv-0a-ItC" secondAttribute="leading" id="6rJ-Cz-C3Z"/>
|
||||
<constraint firstItem="Mgn-IF-ax7" firstAttribute="centerX" secondItem="8jv-0a-ItC" secondAttribute="centerX" id="8kV-gZ-4TW"/>
|
||||
<constraint firstItem="50O-wR-1hq" firstAttribute="height" secondItem="8jv-0a-ItC" secondAttribute="height" multiplier="1.7:3" id="FTt-Ru-HhA"/>
|
||||
<constraint firstItem="wnW-MB-udh" firstAttribute="top" secondItem="Mgn-IF-ax7" secondAttribute="bottom" constant="32" id="IAR-im-kU7"/>
|
||||
<constraint firstItem="50O-wR-1hq" firstAttribute="top" secondItem="Qap-U8-zpQ" secondAttribute="bottom" constant="30" id="bo1-Am-BdP"/>
|
||||
<constraint firstItem="wnW-MB-udh" firstAttribute="centerX" secondItem="8jv-0a-ItC" secondAttribute="centerX" id="djF-KX-NgM"/>
|
||||
<constraint firstItem="Mgn-IF-ax7" firstAttribute="top" secondItem="50O-wR-1hq" secondAttribute="bottom" constant="20" id="igk-jf-lgM"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="gameViewContainerView" destination="oBZ-xU-jeC" id="jMI-iF-JlU"/>
|
||||
@ -140,7 +198,7 @@
|
||||
<!--Game View Controller-->
|
||||
<scene sceneID="ASV-Uk-0aP">
|
||||
<objects>
|
||||
<viewController id="yhz-fF-D91" customClass="GameViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController id="yhz-fF-D91" customClass="GameViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="ItC-Bu-WRI"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="g58-HO-6L5"/>
|
||||
@ -148,12 +206,23 @@
|
||||
<view key="view" contentMode="scaleToFill" id="skW-1S-YD4">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="bg" translatesAutoresizingMaskIntoConstraints="NO" id="0b6-zE-QS0">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="647"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.02214059979" green="0.092763282360000004" blue="0.18196243049999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="0b6-zE-QS0" secondAttribute="trailing" id="5n6-uA-WLa"/>
|
||||
<constraint firstItem="0b6-zE-QS0" firstAttribute="top" secondItem="ItC-Bu-WRI" secondAttribute="bottom" id="DTE-cg-9BW"/>
|
||||
<constraint firstItem="0b6-zE-QS0" firstAttribute="leading" secondItem="skW-1S-YD4" secondAttribute="leading" id="Ej3-6D-wcR"/>
|
||||
<constraint firstItem="g58-HO-6L5" firstAttribute="top" secondItem="0b6-zE-QS0" secondAttribute="bottom" id="ozO-Ni-qpr"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<segue destination="Yrw-9v-Pcr" kind="presentation" identifier="pause" customClass="PauseStoryboardSegue" customModule="Delta" customModuleProvider="target" id="FLq-Zt-HDv"/>
|
||||
<segue destination="wKV-3d-NIY" kind="presentation" identifier="showGamesViewController" customClass="GamesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="Tey-6Z-UHp"/>
|
||||
<segue destination="wKV-3d-NIY" kind="presentation" identifier="showInitialGamesViewController" customClass="InitialGamesStoryboardSegue" customModule="Delta" customModuleProvider="target" id="eR2-0c-0Rv"/>
|
||||
<segue destination="Yrw-9v-Pcr" kind="presentation" identifier="pause" customClass="PauseStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="FLq-Zt-HDv"/>
|
||||
<segue destination="wKV-3d-NIY" kind="presentation" identifier="showGamesViewController" customClass="GamesStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="Tey-6Z-UHp"/>
|
||||
<segue destination="wKV-3d-NIY" kind="presentation" identifier="showInitialGamesViewController" customClass="InitialGamesStoryboardSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" id="eR2-0c-0Rv"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="gxI-00-NlJ" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
@ -171,10 +240,12 @@
|
||||
<!--Settings-->
|
||||
<scene sceneID="L3X-MM-hJL">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="Settings" id="xMK-Cs-fAS" sceneMemberID="viewController"/>
|
||||
<viewControllerPlaceholder storyboardName="Settings" id="xMK-Cs-fAS" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="7yR-QM-2bX"/>
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="2N5-3k-beA" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1036" y="605"/>
|
||||
<point key="canvasLocation" x="1578" y="774"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="zJI-sC-1sm">
|
||||
@ -183,12 +254,14 @@
|
||||
<toolbarItems/>
|
||||
<nil key="simulatedBottomBarMetrics"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="wj9-1e-eev">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<color key="barTintColor" red="0.032843004910000001" green="0.076728828250000006" blue="0.13726195690000001" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="M4r-sO-G4H">
|
||||
<rect key="frame" x="0.0" y="556" width="600" height="44"/>
|
||||
<rect key="frame" x="0.0" y="-20" width="0.0" height="0.0"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</toolbar>
|
||||
<connections>
|
||||
@ -210,7 +283,7 @@
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="nR0-Va-AI1">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="saveStatesNavigationController" automaticallyAdjustsScrollViewInsets="NO" id="MPk-bF-nkj" sceneMemberID="viewController">
|
||||
<navigationController storyboardIdentifier="saveStatesNavigationController" automaticallyAdjustsScrollViewInsets="NO" modalPresentationStyle="fullScreen" id="MPk-bF-nkj" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="twH-3X-6DV">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
@ -219,7 +292,7 @@
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="Eae-Qk-9MI" kind="relationship" relationship="rootViewController" id="1Jh-Zf-ntp"/>
|
||||
<segue destination="WQV-Du-4IA" kind="unwind" identifier="unwindFromSaveStates" customClass="SaveStatesStoryboardUnwindSegue" customModule="Delta" customModuleProvider="target" unwindAction="unwindFromSaveStatesViewControllerWith:" id="dwO-iv-XDr"/>
|
||||
<segue destination="WQV-Du-4IA" kind="unwind" identifier="unwindFromSaveStates" customClass="SaveStatesStoryboardUnwindSegue" customModule="Retro_Game_Emulator" customModuleProvider="target" unwindAction="unwindToGameCollectionViewController:" id="dwO-iv-XDr"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="htj-tq-2KP" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
@ -227,6 +300,16 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2652" y="1718"/>
|
||||
</scene>
|
||||
<!--preferredControllerSkins-->
|
||||
<scene sceneID="aKY-Ld-et6">
|
||||
<objects>
|
||||
<viewControllerPlaceholder storyboardName="Settings" referencedIdentifier="preferredControllerSkins" id="dbc-pQ-iun" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="xth-MV-SHp"/>
|
||||
</viewControllerPlaceholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="za6-AO-ZFe" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3405" y="2394"/>
|
||||
</scene>
|
||||
<!--saveStatesViewController-->
|
||||
<scene sceneID="f1R-Kb-FOU">
|
||||
<objects>
|
||||
@ -237,11 +320,46 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3409" y="1716"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="eMh-8N-ZGA">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="qdE-gb-V2e" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationItem key="navigationItem" id="Dg6-He-v5H"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="35T-4Q-Mmp">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="dbc-pQ-iun" kind="relationship" relationship="rootViewController" id="oRb-B6-c0J"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="XmB-QY-yA3" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2652" y="2394"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="OW2-zT-pbF">
|
||||
<objects>
|
||||
<navigationController id="V2x-v0-jWm" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="pjb-4I-yar">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="56"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Jo9-gl-p5p" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2652" y="3085"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="Settings_Button" width="22" height="22"/>
|
||||
</resources>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="Tey-6Z-UHp"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="addtwo" width="519" height="519"/>
|
||||
<image name="bg" width="375" height="812"/>
|
||||
<image name="home" width="92" height="37"/>
|
||||
<image name="home_tab" width="384.5" height="109"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="11185.3" systemVersion="15G31" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="Dt0-nV-isV">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="11151.4"/>
|
||||
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@ -20,16 +21,19 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="p2M-dE-BJs" userLabel="Blur View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="eyD-0d-RHe" userLabel="Blur Content View">
|
||||
<frame key="frameInset"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="rqN-NB-jbb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<connections>
|
||||
<segue destination="sWv-Ky-VGs" kind="embed" identifier="embedNavigationController" id="1Ja-XW-uoT"/>
|
||||
</connections>
|
||||
</containerView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="rqN-NB-jbb" secondAttribute="bottom" id="3XJ-2M-uVD"/>
|
||||
<constraint firstAttribute="trailing" secondItem="rqN-NB-jbb" secondAttribute="trailing" id="NQ7-cS-8T5"/>
|
||||
@ -41,10 +45,10 @@
|
||||
</visualEffectView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="p2M-dE-BJs" secondAttribute="bottom" id="70W-aM-NX0"/>
|
||||
<constraint firstItem="p2M-dE-BJs" firstAttribute="top" secondItem="oOH-ea-jcb" secondAttribute="top" id="8tp-qg-fgz"/>
|
||||
<constraint firstAttribute="trailing" secondItem="p2M-dE-BJs" secondAttribute="trailing" id="Idx-Ok-WhM"/>
|
||||
<constraint firstItem="p2M-dE-BJs" firstAttribute="leading" secondItem="oOH-ea-jcb" secondAttribute="leading" id="Ppi-05-jHX"/>
|
||||
<constraint firstItem="gF0-0U-kR7" firstAttribute="top" secondItem="p2M-dE-BJs" secondAttribute="bottom" id="eFj-ha-zJQ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
@ -63,8 +67,8 @@
|
||||
<objects>
|
||||
<navigationController id="sWv-Ky-VGs" sceneMemberID="viewController">
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" id="Snh-Z0-9kC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" misplaced="YES" barStyle="black" id="Snh-Z0-9kC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
@ -78,7 +82,7 @@
|
||||
<!--Paused-->
|
||||
<scene sceneID="1md-hu-g0J">
|
||||
<objects>
|
||||
<collectionViewController id="0jA-NY-mvB" customClass="PauseMenuViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionViewController id="0jA-NY-mvB" customClass="GridMenuViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" delaysContentTouches="NO" dataMode="prototypes" id="scc-uc-vaJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
@ -91,7 +95,7 @@
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="6XS-Ne-nGZ" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target">
|
||||
<frame key="frameInset" minY="84" width="60" height="80"/>
|
||||
<rect key="frame" x="0.0" y="20" width="60" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="60" height="80"/>
|
||||
@ -128,7 +132,7 @@
|
||||
<objects>
|
||||
<collectionViewController storyboardIdentifier="saveStatesViewController" id="OOk-k7-INg" customClass="SaveStatesViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="XgF-OF-CVf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="20" id="tvW-q1-PD8" customClass="GridCollectionViewLayout" customModule="Delta" customModuleProvider="target">
|
||||
@ -139,7 +143,7 @@
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="c3N-1A-ryV" customClass="GridCollectionViewCell" customModule="Delta" customModuleProvider="target">
|
||||
<frame key="frameInset" minX="20" minY="124" width="50" height="50"/>
|
||||
<rect key="frame" x="20" y="60" width="50" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
|
||||
@ -148,7 +152,7 @@
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Header" id="YeY-W9-CC6" customClass="SaveStatesCollectionHeaderView" customModule="Delta" customModuleProvider="target">
|
||||
<frame key="frameInset" minY="64" width="375" height="50"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</collectionReusableView>
|
||||
<connections>
|
||||
@ -157,13 +161,28 @@
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="Save State" id="BoG-k2-aR2">
|
||||
<barButtonItem key="rightBarButtonItem" systemItem="add" id="lKg-Ks-hWN">
|
||||
<rightBarButtonItems>
|
||||
<barButtonItem systemItem="add" id="lKg-Ks-hWN">
|
||||
<connections>
|
||||
<action selector="addSaveState" destination="OOk-k7-INg" id="xY2-94-EOr"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem style="plain" id="has-I3-HDZ">
|
||||
<button key="customView" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" id="y2a-9f-EFz">
|
||||
<rect key="frame" x="288.5" y="13" width="30" height="30"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<state key="normal" title="▼"/>
|
||||
<connections>
|
||||
<action selector="changeSortOrder:" destination="OOk-k7-INg" eventType="primaryActionTriggered" id="qQn-uw-SN1"/>
|
||||
</connections>
|
||||
</button>
|
||||
</barButtonItem>
|
||||
</rightBarButtonItems>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
|
||||
<connections>
|
||||
<outlet property="sortButton" destination="y2a-9f-EFz" id="Zbo-Q0-bVL"/>
|
||||
</connections>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cL5-DH-K3b" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@ -174,22 +193,22 @@
|
||||
<objects>
|
||||
<tableViewController id="wb8-5o-1jE" customClass="CheatsViewController" customModule="Delta" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="44" sectionHeaderHeight="28" sectionFooterHeight="28" id="f5S-hV-1yV">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="e8g-ZW-5lQ" customClass="CheatTableViewCell" customModule="Delta" customModuleProvider="target">
|
||||
<frame key="frameInset" minY="92" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="28" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="e8g-ZW-5lQ" id="AHE-Jk-ULE">
|
||||
<frame key="frameInset" width="375" height="43.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="emc-gw-KkJ" userLabel="Selected Background View">
|
||||
<frame key="frameInset" maxY="-0.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="45"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" ambiguous="YES" id="9bA-Tg-Bko">
|
||||
<frame key="frameInset"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="9bA-Tg-Bko">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="45"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
@ -198,8 +217,9 @@
|
||||
</vibrancyEffect>
|
||||
</visualEffectView>
|
||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="R4A-9d-DGb">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" id="EdX-fU-x54">
|
||||
<frame key="frameInset"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
</view>
|
||||
<vibrancyEffect>
|
||||
@ -247,18 +267,19 @@
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" keyboardDismissMode="interactive" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="BV9-ff-x83">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.93725490199999995" green="0.93725490199999995" blue="0.95686274510000002" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="backgroundColor" red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Name" id="QT6-DZ-g70">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="ZeC-rg-QFa">
|
||||
<rect key="frame" x="0.0" y="119.5" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="ZeC-rg-QFa" id="UIF-fK-ApW">
|
||||
<frame key="frameInset" width="375" height="43.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Cheat Name" minimumFontSize="17" clearButtonMode="whileEditing" translatesAutoresizingMaskIntoConstraints="NO" id="DD1-X0-hg7">
|
||||
<rect key="frame" x="16" y="0.0" width="343" height="44"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="words" autocorrectionType="no" returnKeyType="done"/>
|
||||
<connections>
|
||||
@ -280,13 +301,14 @@
|
||||
<tableViewSection headerTitle="Type" footerTitle="Description" id="rvn-VK-2uH">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="Tst-zn-e04">
|
||||
<rect key="frame" x="0.0" y="227" width="375" height="44"/>
|
||||
<rect key="frame" x="0.0" y="163" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="Tst-zn-e04" id="gwV-zS-RWQ">
|
||||
<frame key="frameInset" width="375" height="43.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<segmentedControl opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="xrD-ue-96Q">
|
||||
<rect key="frame" x="16" y="6.5" width="343" height="32"/>
|
||||
<segments>
|
||||
<segment title="First"/>
|
||||
<segment title="Second"/>
|
||||
@ -308,14 +330,15 @@
|
||||
<tableViewSection headerTitle="Code" footerTitle="Description" id="rHC-nA-ga0">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" rowHeight="210" id="xxc-cz-sb7">
|
||||
<rect key="frame" x="0.0" y="346.5" width="375" height="210"/>
|
||||
<rect key="frame" x="0.0" y="282.5" width="375" height="210"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="xxc-cz-sb7" id="agU-SE-fNa">
|
||||
<frame key="frameInset" width="375" height="209.5"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="210"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="XXXXXXXX YYYYYYYY" translatesAutoresizingMaskIntoConstraints="NO" id="a17-LB-QXD" customClass="CheatTextView" customModule="Delta" customModuleProvider="target">
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="210"/>
|
||||
<color key="textColor" systemColor="labelColor"/>
|
||||
<fontDescription key="fontDescription" name="Menlo-Regular" family="Menlo" pointSize="26"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="allCharacters" autocorrectionType="no" spellCheckingType="no" returnKeyType="done"/>
|
||||
<connections>
|
||||
@ -376,4 +399,9 @@
|
||||
<point key="canvasLocation" x="2385" y="1377"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="labelColor">
|
||||
<color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="9531" systemVersion="15D21" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13528" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="9529"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13526"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="PausePresentationController" customModule="Delta" customModuleProvider="target">
|
||||
@ -13,14 +18,14 @@
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="qFz-hB-77X">
|
||||
<rect key="frame" x="15" y="236" width="570" height="127.5"/>
|
||||
<rect key="frame" x="15" y="270.5" width="375" height="126.5"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="500" image="Pause" translatesAutoresizingMaskIntoConstraints="NO" id="cWa-Ht-i5m">
|
||||
<rect key="frame" x="0.0" y="0.0" width="570" height="80"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" relation="greaterThanOrEqual" priority="900" constant="44" id="xki-9P-sHi">
|
||||
<variation key="heightClass=compact" constant="25"/>
|
||||
@ -28,9 +33,9 @@
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="800" text="Super Mario World" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="ZcT-qE-aES">
|
||||
<rect key="frame" x="0.0" y="95" width="570" height="32.5"/>
|
||||
<rect key="frame" x="0.0" y="95" width="375" height="31.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle1"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
846
Delta/Base.lproj/Settings.storyboard
Normal file
846
Delta/Base.lproj/Settings.storyboard
Normal file
@ -0,0 +1,846 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="32700.99.1234" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="ssH-mM-uG6">
|
||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22684"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Settings-->
|
||||
<scene sceneID="N30-Sa-dHo">
|
||||
<objects>
|
||||
<tableViewController title="Settings" id="eHi-aO-uGS" customClass="SettingsViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="hRw-wV-lch">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="separatorColor" systemColor="separatorColor"/>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Controllers" id="c6K-sJ-0vW">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="oDc-U1-cH3" detailTextLabel="czc-3c-hwp" style="IBUITableViewCellStyleValue1" id="jvV-ZB-Rq1">
|
||||
<rect key="frame" x="16" y="38" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="jvV-ZB-Rq1" id="AVi-6C-eIS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Player 1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="oDc-U1-cH3">
|
||||
<rect key="frame" x="16" y="13" width="56" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" insetsLayoutMarginsFromSafeArea="NO" text="Detail" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="czc-3c-hwp">
|
||||
<rect key="frame" x="266.5" y="13" width="42" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" systemColor="lightTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="e3u-x9-IEC" detailTextLabel="2OP-A1-VYo" style="IBUITableViewCellStyleValue1" id="1Fv-H5-0oH">
|
||||
<rect key="frame" x="16" y="82" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="1Fv-H5-0oH" id="kFJ-zK-MLZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 2" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="e3u-x9-IEC">
|
||||
<rect key="frame" x="16" y="13" width="58" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="SteelSeries Stratus" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2OP-A1-VYo">
|
||||
<rect key="frame" x="170" y="13" width="138.5" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" systemColor="lightTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.074800990519999999" green="0.081782229240000004" blue="0.1058854684" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="Cdn-11-xZe" detailTextLabel="wWc-NY-Bsd" style="IBUITableViewCellStyleValue1" id="EcC-Be-jV5">
|
||||
<rect key="frame" x="16" y="126" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="EcC-Be-jV5" id="9ZS-um-scR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 3" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Cdn-11-xZe">
|
||||
<rect key="frame" x="16" y="13" width="58.5" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="MOGA Gamepad" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="wWc-NY-Bsd">
|
||||
<rect key="frame" x="186.5" y="13" width="122" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" systemColor="lightTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.074800990519999999" green="0.081782229240000004" blue="0.1058854684" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="DetailCell" textLabel="Hls-3b-EaS" detailTextLabel="hNf-uc-PLR" style="IBUITableViewCellStyleValue1" id="hO9-Ov-vsA">
|
||||
<rect key="frame" x="16" y="170" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="hO9-Ov-vsA" id="MRi-re-XI7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 4" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Hls-3b-EaS">
|
||||
<rect key="frame" x="16" y="13" width="59" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Jayce's iPhone" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="hNf-uc-PLR">
|
||||
<rect key="frame" x="200" y="13" width="108.5" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" systemColor="lightTextColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.070923872289999995" green="0.081780366600000001" blue="0.10588551309999999" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Controller Skins" id="Nch-k1-6pR">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" id="ICf-ug-NwS">
|
||||
<rect key="frame" x="16" y="270" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="ICf-ug-NwS" id="7se-sE-x9e">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="game" translatesAutoresizingMaskIntoConstraints="NO" id="pZJ-Xw-wbK">
|
||||
<rect key="frame" x="20" y="12" width="20" height="20"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Game Boy Color" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="L97-UB-99v">
|
||||
<rect key="frame" x="48" y="13.5" width="103" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="baockgo" translatesAutoresizingMaskIntoConstraints="NO" id="f28-XT-Xqv">
|
||||
<rect key="frame" x="303" y="12" width="20" height="20"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="pZJ-Xw-wbK" firstAttribute="leading" secondItem="7se-sE-x9e" secondAttribute="leading" constant="20" id="8Ju-vf-sg8"/>
|
||||
<constraint firstItem="pZJ-Xw-wbK" firstAttribute="centerY" secondItem="7se-sE-x9e" secondAttribute="centerY" id="Buh-qm-B9i"/>
|
||||
<constraint firstItem="L97-UB-99v" firstAttribute="leading" secondItem="pZJ-Xw-wbK" secondAttribute="trailing" constant="8" id="T6f-Mp-Etv"/>
|
||||
<constraint firstAttribute="trailing" secondItem="f28-XT-Xqv" secondAttribute="trailing" constant="20" id="Y6B-0R-HYa"/>
|
||||
<constraint firstItem="L97-UB-99v" firstAttribute="centerY" secondItem="7se-sE-x9e" secondAttribute="centerY" id="aRg-pc-aaY"/>
|
||||
<constraint firstItem="f28-XT-Xqv" firstAttribute="centerY" secondItem="7se-sE-x9e" secondAttribute="centerY" id="i7Y-Js-8KD"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="Dxs-Me-IVU" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="Hqy-yc-Jef">
|
||||
<rect key="frame" x="16" y="314" width="343" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Hqy-yc-Jef" id="wJL-kh-qW0">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Dxs-Me-IVU">
|
||||
<rect key="frame" x="16" y="0.0" width="292.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="o9x-Kn-6bC" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="jFa-Qk-1cj">
|
||||
<rect key="frame" x="16" y="314.00000011920929" width="343" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="jFa-Qk-1cj" id="rFR-qL-fNQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="o9x-Kn-6bC">
|
||||
<rect key="frame" x="16" y="0.0" width="292.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="XVO-TO-ncw" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="vIu-iy-kRM">
|
||||
<rect key="frame" x="16" y="314.00000023841858" width="343" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="vIu-iy-kRM" id="FIZ-uw-fR7">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="XVO-TO-ncw">
|
||||
<rect key="frame" x="16" y="0.0" width="292.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="vV9-Fk-zd5" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="Dfy-MJ-39n">
|
||||
<rect key="frame" x="16" y="314.00000035762787" width="343" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="Dfy-MJ-39n" id="dgi-73-brN">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="vV9-Fk-zd5">
|
||||
<rect key="frame" x="16" y="0.0" width="292.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="Etp-g5-W9R" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="p69-Xz-VoS">
|
||||
<rect key="frame" x="16" y="314.00000047683716" width="343" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="p69-Xz-VoS" id="pwB-9y-EUf">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="Etp-g5-W9R">
|
||||
<rect key="frame" x="16" y="0.0" width="292.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="Cell" textLabel="sF6-5e-HL2" rowHeight="0.0" style="IBUITableViewCellStyleDefault" id="WVd-aL-SWy">
|
||||
<rect key="frame" x="16" y="314.00000059604645" width="343" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="WVd-aL-SWy" id="uD0-3G-npJ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="System Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="sF6-5e-HL2">
|
||||
<rect key="frame" x="16" y="0.0" width="292.5" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="other" id="SwK-m9-8gt">
|
||||
<cells>
|
||||
<tableViewCell contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="44" id="Xxk-vo-eu4">
|
||||
<rect key="frame" x="16" y="370.00000071525574" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="Xxk-vo-eu4" id="vxt-Ex-b4b">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="about" translatesAutoresizingMaskIntoConstraints="NO" id="Bvn-hc-pwv">
|
||||
<rect key="frame" x="20" y="12" width="20" height="20"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="About" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="JQ1-q7-Gxs">
|
||||
<rect key="frame" x="48" y="13.5" width="39" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="baockgo" translatesAutoresizingMaskIntoConstraints="NO" id="deP-m5-YEL">
|
||||
<rect key="frame" x="303" y="12" width="20" height="20"/>
|
||||
</imageView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="hwG-bd-Nvz">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
||||
<connections>
|
||||
<action selector="aboutbtn:" destination="eHi-aO-uGS" eventType="touchUpInside" id="3Zx-do-hZW"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="hwG-bd-Nvz" firstAttribute="top" secondItem="vxt-Ex-b4b" secondAttribute="top" id="0er-Nl-2S2"/>
|
||||
<constraint firstAttribute="bottom" secondItem="hwG-bd-Nvz" secondAttribute="bottom" id="3d6-cf-80e"/>
|
||||
<constraint firstItem="Bvn-hc-pwv" firstAttribute="leading" secondItem="vxt-Ex-b4b" secondAttribute="leading" constant="20" id="7Ot-0B-bZJ"/>
|
||||
<constraint firstItem="JQ1-q7-Gxs" firstAttribute="centerY" secondItem="Bvn-hc-pwv" secondAttribute="centerY" id="Ggp-5n-s6x"/>
|
||||
<constraint firstItem="deP-m5-YEL" firstAttribute="centerY" secondItem="vxt-Ex-b4b" secondAttribute="centerY" id="SMi-0Z-bS0"/>
|
||||
<constraint firstItem="JQ1-q7-Gxs" firstAttribute="leading" secondItem="Bvn-hc-pwv" secondAttribute="trailing" constant="8" id="TGU-7v-ZAl"/>
|
||||
<constraint firstAttribute="trailing" secondItem="deP-m5-YEL" secondAttribute="trailing" constant="20" id="g70-Mf-HKo"/>
|
||||
<constraint firstAttribute="trailing" secondItem="hwG-bd-Nvz" secondAttribute="trailing" id="hma-YZ-euX"/>
|
||||
<constraint firstItem="hwG-bd-Nvz" firstAttribute="leading" secondItem="vxt-Ex-b4b" secondAttribute="leading" id="ieP-nx-1SH"/>
|
||||
<constraint firstItem="Bvn-hc-pwv" firstAttribute="centerY" secondItem="vxt-Ex-b4b" secondAttribute="centerY" id="oRX-Bk-F9q"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="44" id="acf-m6-fmX">
|
||||
<rect key="frame" x="16" y="414.00000071525574" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="acf-m6-fmX" id="X3f-9y-xTV">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="privacy" translatesAutoresizingMaskIntoConstraints="NO" id="siI-Gj-0gv">
|
||||
<rect key="frame" x="20" y="12" width="20" height="20"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Privacy Policy" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="L8X-tT-vJW">
|
||||
<rect key="frame" x="48" y="13.5" width="88" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="baockgo" translatesAutoresizingMaskIntoConstraints="NO" id="NoK-Re-DcB">
|
||||
<rect key="frame" x="303" y="12" width="20" height="20"/>
|
||||
</imageView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1xF-JV-zaQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
||||
<connections>
|
||||
<action selector="privacybtn:" destination="eHi-aO-uGS" eventType="touchUpInside" id="sDx-xb-tmz"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="1xF-JV-zaQ" secondAttribute="bottom" id="6lW-J7-pcp"/>
|
||||
<constraint firstItem="L8X-tT-vJW" firstAttribute="centerY" secondItem="X3f-9y-xTV" secondAttribute="centerY" id="EcD-Mb-AY8"/>
|
||||
<constraint firstAttribute="trailing" secondItem="1xF-JV-zaQ" secondAttribute="trailing" id="M6D-4J-8BW"/>
|
||||
<constraint firstItem="1xF-JV-zaQ" firstAttribute="leading" secondItem="X3f-9y-xTV" secondAttribute="leading" id="NyC-vL-YuI"/>
|
||||
<constraint firstItem="L8X-tT-vJW" firstAttribute="leading" secondItem="siI-Gj-0gv" secondAttribute="trailing" constant="8" id="ZOi-xq-zNB"/>
|
||||
<constraint firstAttribute="trailing" secondItem="NoK-Re-DcB" secondAttribute="trailing" constant="20" id="aXe-iE-zuL"/>
|
||||
<constraint firstItem="siI-Gj-0gv" firstAttribute="leading" secondItem="X3f-9y-xTV" secondAttribute="leading" constant="20" id="arU-V7-rjZ"/>
|
||||
<constraint firstItem="NoK-Re-DcB" firstAttribute="centerY" secondItem="X3f-9y-xTV" secondAttribute="centerY" id="jk6-yF-Z5S"/>
|
||||
<constraint firstItem="1xF-JV-zaQ" firstAttribute="top" secondItem="X3f-9y-xTV" secondAttribute="top" id="kNk-0u-VE0"/>
|
||||
<constraint firstItem="siI-Gj-0gv" firstAttribute="centerY" secondItem="X3f-9y-xTV" secondAttribute="centerY" id="rpD-ru-yjt"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</tableViewCell>
|
||||
<tableViewCell contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="44" id="MrG-s8-yXA">
|
||||
<rect key="frame" x="16" y="458.00000071525574" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" tableViewCell="MrG-s8-yXA" id="6T0-pz-Vma">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="useragreement" translatesAutoresizingMaskIntoConstraints="NO" id="CSn-hu-wMa">
|
||||
<rect key="frame" x="20" y="12" width="20" height="20"/>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="User Agreement" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OmR-EO-UWK">
|
||||
<rect key="frame" x="48" y="13.5" width="104" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="baockgo" translatesAutoresizingMaskIntoConstraints="NO" id="O9E-CI-aBo">
|
||||
<rect key="frame" x="303" y="12" width="20" height="20"/>
|
||||
</imageView>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TgQ-rC-R47">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
||||
<connections>
|
||||
<action selector="useragreemBtn:" destination="eHi-aO-uGS" eventType="touchUpInside" id="Rxb-QU-uBw"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="CSn-hu-wMa" firstAttribute="leading" secondItem="6T0-pz-Vma" secondAttribute="leading" constant="20" id="8aS-xw-48c"/>
|
||||
<constraint firstItem="TgQ-rC-R47" firstAttribute="top" secondItem="6T0-pz-Vma" secondAttribute="top" id="9rp-bv-CGE"/>
|
||||
<constraint firstItem="CSn-hu-wMa" firstAttribute="centerY" secondItem="6T0-pz-Vma" secondAttribute="centerY" id="FTm-wm-JMk"/>
|
||||
<constraint firstItem="O9E-CI-aBo" firstAttribute="centerY" secondItem="6T0-pz-Vma" secondAttribute="centerY" id="TIz-3R-YbC"/>
|
||||
<constraint firstAttribute="trailing" secondItem="O9E-CI-aBo" secondAttribute="trailing" constant="20" id="TyR-DE-Un5"/>
|
||||
<constraint firstItem="TgQ-rC-R47" firstAttribute="leading" secondItem="6T0-pz-Vma" secondAttribute="leading" id="YFV-PF-z69"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TgQ-rC-R47" secondAttribute="bottom" id="dze-O5-QfT"/>
|
||||
<constraint firstItem="OmR-EO-UWK" firstAttribute="centerY" secondItem="6T0-pz-Vma" secondAttribute="centerY" id="iYL-9B-bcl"/>
|
||||
<constraint firstAttribute="trailing" secondItem="TgQ-rC-R47" secondAttribute="trailing" id="isn-Gv-lJy"/>
|
||||
<constraint firstItem="OmR-EO-UWK" firstAttribute="leading" secondItem="CSn-hu-wMa" secondAttribute="trailing" constant="8" id="yu7-ce-uSe"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="aJ4-xT-aZL">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="none" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" rowHeight="0.0" id="bza-Nl-H39">
|
||||
<rect key="frame" x="16" y="538.00000071525574" width="343" height="1.1920928955078125e-07"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="bza-Nl-H39" id="g7q-9D-XYR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="0.0"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="QWm-yw-YNv">
|
||||
<rect key="frame" x="0.0" y="-17" width="343" height="34"/>
|
||||
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
|
||||
<connections>
|
||||
<action selector="didmisiigame:" destination="eHi-aO-uGS" eventType="touchUpInside" id="4yk-aq-NQN"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="QWm-yw-YNv" firstAttribute="leading" secondItem="g7q-9D-XYR" secondAttribute="leading" id="XjX-7g-4Kw"/>
|
||||
<constraint firstItem="QWm-yw-YNv" firstAttribute="centerY" secondItem="g7q-9D-XYR" secondAttribute="centerY" id="Zgp-AP-dw3"/>
|
||||
<constraint firstAttribute="trailing" secondItem="QWm-yw-YNv" secondAttribute="trailing" id="gPG-We-VJt"/>
|
||||
<constraint firstItem="QWm-yw-YNv" firstAttribute="centerX" secondItem="g7q-9D-XYR" secondAttribute="centerX" id="gzN-im-JAL"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="eHi-aO-uGS" id="rsh-0y-8Ex"/>
|
||||
<outlet property="delegate" destination="eHi-aO-uGS" id="7cP-Sn-ONN"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Settings" largeTitleDisplayMode="always" id="DYU-tO-Lmw">
|
||||
<barButtonItem key="rightBarButtonItem" title="back" style="done" id="UdW-1j-fSz">
|
||||
<connections>
|
||||
<segue destination="slQ-xv-zI1" kind="unwind" identifier="unwindSettingsSegue" unwindAction="unwindFromSettingsViewController:" id="7gb-Bj-XU3"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<size key="freeformSize" width="375" height="600"/>
|
||||
<connections>
|
||||
<segue destination="uBz-mm-mXr" kind="show" identifier="controllersSegue" id="MLY-hF-UB8"/>
|
||||
<segue destination="56e-ul-z6v" kind="show" identifier="controllerSkinsSegue" id="GNM-Gt-YFf"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="19F-9T-esM" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<exit id="slQ-xv-zI1" userLabel="Exit" sceneMemberID="exit"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1498" y="794"/>
|
||||
</scene>
|
||||
<!--Controls-->
|
||||
<scene sceneID="Gi9-m1-y9x">
|
||||
<objects>
|
||||
<viewController title="Controls" id="x1g-pH-DnF" customClass="ControllerInputsViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="cH1-gu-g2u"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="Z6c-bc-h6l"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="cPg-qa-ERT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<containerView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="6Wl-el-X30" userLabel="GameViewController">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<connections>
|
||||
<segue destination="LIv-AL-s86" kind="embed" identifier="embedGameViewController" id="2Qg-Jw-0mM"/>
|
||||
</connections>
|
||||
</containerView>
|
||||
<containerView opaque="NO" contentMode="scaleToFill" placeholderIntrinsicWidth="375" placeholderIntrinsicHeight="200" translatesAutoresizingMaskIntoConstraints="NO" id="KkE-ji-6Y8" userLabel="GridMenuViewController">
|
||||
<rect key="frame" x="0.0" y="233.5" width="375" height="200"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="200" id="MWA-T4-ROi"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<segue destination="Jpj-e9-6XW" kind="embed" identifier="embedActionsMenuViewController" id="kfu-fO-l6Z"/>
|
||||
</connections>
|
||||
</containerView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<gestureRecognizers/>
|
||||
<constraints>
|
||||
<constraint firstItem="KkE-ji-6Y8" firstAttribute="centerY" secondItem="cPg-qa-ERT" secondAttribute="centerY" placeholder="YES" id="4wi-cL-aCQ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="6Wl-el-X30" secondAttribute="bottom" id="Bmp-yB-Yf1"/>
|
||||
<constraint firstAttribute="trailing" secondItem="KkE-ji-6Y8" secondAttribute="trailing" id="Jeb-8K-VYw"/>
|
||||
<constraint firstItem="6Wl-el-X30" firstAttribute="top" secondItem="cPg-qa-ERT" secondAttribute="top" id="TD2-bx-DJC"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6Wl-el-X30" secondAttribute="trailing" id="Xph-DL-tBk"/>
|
||||
<constraint firstItem="6Wl-el-X30" firstAttribute="leading" secondItem="cPg-qa-ERT" secondAttribute="leading" id="gcd-77-5wR"/>
|
||||
<constraint firstItem="KkE-ji-6Y8" firstAttribute="leading" secondItem="cPg-qa-ERT" secondAttribute="leading" id="z7N-Cn-hGs"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outletCollection property="gestureRecognizers" destination="4p8-OB-LsR" appends="YES" id="4k4-Oj-XtP"/>
|
||||
</connections>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" id="UeP-Yr-9jA">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="QfC-sf-WbP">
|
||||
<connections>
|
||||
<segue destination="8l5-7I-Z7e" kind="unwind" identifier="cancelControllerInputs" unwindAction="unwindFromControllerInputsViewController:" id="8m5-qE-zxs"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<rightBarButtonItems>
|
||||
<barButtonItem style="done" systemItem="save" id="WHh-7W-jpl">
|
||||
<connections>
|
||||
<segue destination="8l5-7I-Z7e" kind="unwind" identifier="saveControllerInputs" unwindAction="unwindFromControllerInputsViewController:" id="b6G-wn-Tim"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem title="Reset" style="done" id="4jy-hy-YS3">
|
||||
<color key="tintColor" systemColor="systemRedColor"/>
|
||||
<connections>
|
||||
<action selector="resetInputMapping:" destination="x1g-pH-DnF" id="flE-B2-TMu"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</rightBarButtonItems>
|
||||
</navigationItem>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
|
||||
<connections>
|
||||
<outlet property="actionsMenuViewControllerHeightConstraint" destination="MWA-T4-ROi" id="itx-dZ-m76"/>
|
||||
<outlet property="cancelTapGestureRecognizer" destination="4p8-OB-LsR" id="coO-FL-pbp"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="DqP-Jn-rth" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<exit id="8l5-7I-Z7e" userLabel="Exit" sceneMemberID="exit"/>
|
||||
<tapGestureRecognizer delaysTouchesBegan="YES" id="4p8-OB-LsR">
|
||||
<connections>
|
||||
<action selector="handleTapGesture:" destination="x1g-pH-DnF" id="8KO-75-4Iy"/>
|
||||
<outlet property="delegate" destination="x1g-pH-DnF" id="GDY-v6-naf"/>
|
||||
</connections>
|
||||
</tapGestureRecognizer>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4102" y="-244"/>
|
||||
</scene>
|
||||
<!--Controllers-->
|
||||
<scene sceneID="swa-DT-VKS">
|
||||
<objects>
|
||||
<tableViewController id="uBz-mm-mXr" customClass="ControllersSettingsViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="insetGrouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="hLd-Z5-I3b">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" textLabel="VBO-V1-Wfu" detailTextLabel="tqn-1q-p53" style="IBUITableViewCellStyleValue1" id="lzU-uS-el2">
|
||||
<rect key="frame" x="16" y="55.5" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="lzU-uS-el2" id="o56-OW-cxE">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Controller Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="VBO-V1-Wfu">
|
||||
<rect key="frame" x="16" y="13" width="118.5" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="Player 1" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="tqn-1q-p53">
|
||||
<rect key="frame" x="271" y="13" width="56" height="19.5"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="none" indentationWidth="10" reuseIdentifier="NoControllers" textLabel="OeA-dF-xlk" style="IBUITableViewCellStyleDefault" id="rCu-Pd-J3y">
|
||||
<rect key="frame" x="16" y="99.5" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="rCu-Pd-J3y" id="JTP-bU-BBn">
|
||||
<rect key="frame" x="0.0" y="0.0" width="343" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" multipleTouchEnabled="YES" contentMode="left" text="No Connected Controllers" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="OeA-dF-xlk">
|
||||
<rect key="frame" x="16" y="0.0" width="311" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="uBz-mm-mXr" id="XB4-jw-1fr"/>
|
||||
<outlet property="delegate" destination="uBz-mm-mXr" id="tJX-mX-6j8"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Controllers" largeTitleDisplayMode="never" id="QK7-oi-2jJ"/>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/>
|
||||
<connections>
|
||||
<segue destination="0QR-U9-gtx" kind="presentation" identifier="controllerInputsSegue" modalPresentationStyle="fullScreen" id="E3Y-yV-zT5"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="owG-Kh-rfn" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2513" y="-244"/>
|
||||
</scene>
|
||||
<!--Game Boy Advance-->
|
||||
<scene sceneID="pkL-Te-puh">
|
||||
<objects>
|
||||
<tableViewController storyboardIdentifier="preferredControllerSkins" id="56e-ul-z6v" customClass="PreferredControllerSkinsViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="grouped" separatorStyle="default" rowHeight="44" sectionHeaderHeight="18" sectionFooterHeight="18" id="fdQ-n7-kUL">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<sections>
|
||||
<tableViewSection headerTitle="Portrait" id="jGW-i7-nK1">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="8Vp-a7-RvI">
|
||||
<rect key="frame" x="0.0" y="38" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="8Vp-a7-RvI" id="KCG-fx-fax">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="5r7-OQ-i7w">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="5r7-OQ-i7w" firstAttribute="top" secondItem="KCG-fx-fax" secondAttribute="top" id="2uo-BN-g4M"/>
|
||||
<constraint firstAttribute="trailing" secondItem="5r7-OQ-i7w" secondAttribute="trailing" id="AWh-tS-ECE"/>
|
||||
<constraint firstItem="5r7-OQ-i7w" firstAttribute="leading" secondItem="KCG-fx-fax" secondAttribute="leading" id="WLG-Wh-Pq2"/>
|
||||
<constraint firstAttribute="bottom" secondItem="5r7-OQ-i7w" secondAttribute="bottom" id="X0H-WI-dQX"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection headerTitle="Landscape" id="PqP-JS-vGE">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="HaE-e5-fux">
|
||||
<rect key="frame" x="0.0" y="138" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="HaE-e5-fux" id="XwS-Kw-Fe6">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="XY1-es-oZe">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="XY1-es-oZe" firstAttribute="top" secondItem="XwS-Kw-Fe6" secondAttribute="top" id="BLb-gp-vUV"/>
|
||||
<constraint firstAttribute="bottom" secondItem="XY1-es-oZe" secondAttribute="bottom" id="EmB-8V-hFE"/>
|
||||
<constraint firstItem="XY1-es-oZe" firstAttribute="leading" secondItem="XwS-Kw-Fe6" secondAttribute="leading" id="ONa-4a-dnw"/>
|
||||
<constraint firstAttribute="trailing" secondItem="XY1-es-oZe" secondAttribute="trailing" id="avH-RJ-zDf"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="56e-ul-z6v" id="oBb-23-5bM"/>
|
||||
<outlet property="delegate" destination="56e-ul-z6v" id="08A-GG-8bs"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Game Boy Advance" id="2GJ-l7-DgY">
|
||||
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="3iE-lg-bkA">
|
||||
<connections>
|
||||
<segue destination="eMT-K0-PiM" kind="unwind" unwindAction="unwindToGameCollectionViewController:" id="fFx-zM-kfa"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="landscapeImageView" destination="XY1-es-oZe" id="d6S-ez-jh2"/>
|
||||
<outlet property="portraitImageView" destination="5r7-OQ-i7w" id="C5r-uX-SlN"/>
|
||||
<segue destination="pG9-hI-tRE" kind="show" identifier="showControllerSkins" id="kiq-86-5B7"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Nx1-Ly-oRu" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
<exit id="eMT-K0-PiM" userLabel="Exit" sceneMemberID="exit"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2513" y="466"/>
|
||||
</scene>
|
||||
<!--Controller Skins-->
|
||||
<scene sceneID="IN0-an-SWm">
|
||||
<objects>
|
||||
<tableViewController title="Controller Skins" id="pG9-hI-tRE" customClass="ControllerSkinsViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="150" sectionHeaderHeight="18" sectionFooterHeight="18" id="WiB-mC-9xS">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.071399740870000006" green="0.082175157959999995" blue="0.10832635309999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="kK3-bl-qxv" customClass="ControllerSkinTableViewCell" customModule="Retro_Game_Emulator" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="55.5" width="375" height="150"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="kK3-bl-qxv" id="7Vt-Nl-Sfx">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="150"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<activityIndicatorView hidden="YES" opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" style="gray" translatesAutoresizingMaskIntoConstraints="NO" id="o9T-qo-UoF">
|
||||
<rect key="frame" x="177.5" y="65" width="20" height="20"/>
|
||||
</activityIndicatorView>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="zbw-fi-eau">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="150"/>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="zbw-fi-eau" firstAttribute="top" secondItem="7Vt-Nl-Sfx" secondAttribute="top" id="1KW-et-nu2"/>
|
||||
<constraint firstAttribute="trailing" secondItem="zbw-fi-eau" secondAttribute="trailing" id="51I-HE-J4C"/>
|
||||
<constraint firstAttribute="bottom" secondItem="zbw-fi-eau" secondAttribute="bottom" id="ZpW-sc-XRY"/>
|
||||
<constraint firstItem="o9T-qo-UoF" firstAttribute="centerX" secondItem="7Vt-Nl-Sfx" secondAttribute="centerX" id="rmp-c3-xsc"/>
|
||||
<constraint firstItem="o9T-qo-UoF" firstAttribute="centerY" secondItem="7Vt-Nl-Sfx" secondAttribute="centerY" id="y0I-Bf-s60"/>
|
||||
<constraint firstItem="zbw-fi-eau" firstAttribute="leading" secondItem="7Vt-Nl-Sfx" secondAttribute="leading" id="zQU-50-6Od"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="activityIndicatorView" destination="o9T-qo-UoF" id="dZl-54-Q22"/>
|
||||
<outlet property="controllerSkinImageView" destination="zbw-fi-eau" id="VXC-1B-ZLE"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="pG9-hI-tRE" id="FkO-on-Ylv"/>
|
||||
<outlet property="delegate" destination="pG9-hI-tRE" id="8NO-ha-J5E"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Controller Skins" largeTitleDisplayMode="never" id="Ful-dk-I0G">
|
||||
<rightBarButtonItems>
|
||||
<barButtonItem style="plain" systemItem="add" id="FI4-LI-ozM">
|
||||
<connections>
|
||||
<action selector="importControllerSkin" destination="pG9-hI-tRE" id="tYx-5l-Yrn"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem title="Reset" style="done" id="XU0-TI-SPi">
|
||||
<color key="tintColor" systemColor="systemRedColor"/>
|
||||
<connections>
|
||||
<action selector="resetControllerSkin:" destination="pG9-hI-tRE" id="EOa-FJ-zOp"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</rightBarButtonItems>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="importControllerSkinButton" destination="FI4-LI-ozM" id="t6D-mk-Tc7"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="54U-JB-wBG" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3270" y="466"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="8qd-VB-Uy5">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="ssH-mM-uG6" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" largeTitles="YES" id="Ckw-ES-lkE">
|
||||
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<textAttributes key="titleTextAttributes">
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</textAttributes>
|
||||
<textAttributes key="largeTitleTextAttributes">
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</textAttributes>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="eHi-aO-uGS" kind="relationship" relationship="rootViewController" id="wkb-nX-9mw"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="HMI-Ep-MdI" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="622" y="1182"/>
|
||||
</scene>
|
||||
<!--Grid Menu View Controller-->
|
||||
<scene sceneID="Lgi-Ii-M1W">
|
||||
<objects>
|
||||
<collectionViewController id="Jpj-e9-6XW" customClass="GridMenuViewController" customModule="Retro_Game_Emulator" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="yGk-jU-wZQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="10" minimumInteritemSpacing="10" id="tLr-UM-1BH" customClass="GridCollectionViewLayout" customModule="Retro_Game_Emulator" customModuleProvider="target">
|
||||
<size key="itemSize" width="50" height="50"/>
|
||||
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="Hef-IR-nMO" customClass="GridCollectionViewCell" customModule="Retro_Game_Emulator" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center">
|
||||
<rect key="frame" x="0.0" y="0.0" width="50" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</view>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="Jpj-e9-6XW" id="iAK-8A-KXA"/>
|
||||
<outlet property="delegate" destination="Jpj-e9-6XW" id="sbi-az-9kr"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<size key="freeformSize" width="375" height="667"/>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="pRg-BA-3KK" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4858" y="-931"/>
|
||||
</scene>
|
||||
<!--Game View Controller-->
|
||||
<scene sceneID="qAz-yz-iOc">
|
||||
<objects>
|
||||
<viewController id="LIv-AL-s86" customClass="GameViewController" customModule="DeltaCore" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="9u3-RP-Qcj"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="XGZ-ro-kQv"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="57g-cn-rbZ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="uQK-ch-9AG" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="4858" y="-244"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="bwW-s2-fcE">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="0QR-U9-gtx" customClass="RSTNavigationController" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<value key="contentSizeForViewInPopover" type="size" width="375" height="667"/>
|
||||
<simulatedNavigationBarMetrics key="simulatedTopBarMetrics" barStyle="black" prompted="NO"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" barStyle="black" id="Y5H-O6-CQ5">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="x1g-pH-DnF" kind="relationship" relationship="rootViewController" id="EOa-ao-vBI"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="D4f-Fb-zfa" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3270" y="-244"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="about" width="20" height="20"/>
|
||||
<image name="baockgo" width="20" height="20"/>
|
||||
<image name="game" width="20" height="20"/>
|
||||
<image name="privacy" width="20" height="20"/>
|
||||
<image name="useragreement" width="20" height="20"/>
|
||||
<systemColor name="lightTextColor">
|
||||
<color white="1" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
<systemColor name="secondaryLabelColor">
|
||||
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="separatorColor">
|
||||
<color red="0.23529411759999999" green="0.23529411759999999" blue="0.26274509800000001" alpha="0.28999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
<systemColor name="systemRedColor">
|
||||
<color red="1" green="0.23137254900000001" blue="0.18823529410000001" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@ -17,8 +17,7 @@ extension Action
|
||||
case destructive
|
||||
case selected
|
||||
|
||||
var alertActionStyle: UIAlertActionStyle
|
||||
{
|
||||
var alertActionStyle: UIAlertAction.Style {
|
||||
switch self
|
||||
{
|
||||
case .default, .selected: return .default
|
||||
@ -27,8 +26,7 @@ extension Action
|
||||
}
|
||||
}
|
||||
|
||||
var previewActionStyle: UIPreviewActionStyle?
|
||||
{
|
||||
var previewActionStyle: UIPreviewAction.Style? {
|
||||
switch self
|
||||
{
|
||||
case .default: return .default
|
||||
@ -40,11 +38,40 @@ extension Action
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, *)
|
||||
extension Action.Style
|
||||
{
|
||||
var menuAttributes: UIMenuElement.Attributes {
|
||||
switch self
|
||||
{
|
||||
case .default, .cancel, .selected: return []
|
||||
case .destructive: return .destructive
|
||||
}
|
||||
}
|
||||
|
||||
var menuState: UIMenuElement.State {
|
||||
switch self
|
||||
{
|
||||
case .default, .cancel, .destructive: return .off
|
||||
case .selected: return .on
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Action
|
||||
{
|
||||
let title: String
|
||||
let style: Style
|
||||
let action: ((Action) -> Void)?
|
||||
var title: String
|
||||
var style: Style
|
||||
var image: UIImage? = nil
|
||||
var action: ((Action) -> Void)?
|
||||
|
||||
init(title: String, style: Style = .default, image: UIImage? = nil, action: ((Action) -> Void)? = nil)
|
||||
{
|
||||
self.title = title
|
||||
self.style = style
|
||||
self.image = image
|
||||
self.action = action
|
||||
}
|
||||
}
|
||||
|
||||
extension UIAlertAction
|
||||
@ -82,6 +109,19 @@ extension UIAlertController
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
extension UIAction
|
||||
{
|
||||
convenience init?(_ action: Action)
|
||||
{
|
||||
guard action.style != .cancel else { return nil }
|
||||
|
||||
self.init(title: action.title, image: action.image, attributes: action.style.menuAttributes, state: action.style.menuState) { _ in
|
||||
action.action?(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension RangeReplaceableCollection where Iterator.Element == Action
|
||||
{
|
||||
var alertActions: [UIAlertAction] {
|
||||
@ -90,7 +130,13 @@ extension RangeReplaceableCollection where Iterator.Element == Action
|
||||
}
|
||||
|
||||
var previewActions: [UIPreviewAction] {
|
||||
let actions = self.flatMap { UIPreviewAction($0) }
|
||||
let actions = self.compactMap { UIPreviewAction($0) }
|
||||
return actions
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
var menuActions: [UIAction] {
|
||||
let actions = self.compactMap { UIAction($0) }
|
||||
return actions
|
||||
}
|
||||
}
|
||||
|
||||
19
Delta/Components/Box.swift
Normal file
19
Delta/Components/Box.swift
Normal file
@ -0,0 +1,19 @@
|
||||
//
|
||||
// Box.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 11/28/18.
|
||||
// Copyright © 2018 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Box<T>
|
||||
{
|
||||
let value: T
|
||||
|
||||
init(_ value: T)
|
||||
{
|
||||
self.value = value
|
||||
}
|
||||
}
|
||||
@ -39,21 +39,21 @@ class GridCollectionViewCell: UICollectionViewCell
|
||||
}
|
||||
}
|
||||
|
||||
var maximumImageSize: CGSize = CGSize(width: 100, height: 100) {
|
||||
var maximumImageSize: CGSize = CGSize(width: 150, height: 120) {
|
||||
didSet {
|
||||
self.updateMaximumImageSize()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
|
||||
private var vibrancyView = UIVisualEffectView(effect: UIVibrancyEffect(blurEffect: UIBlurEffect(style: .dark)))
|
||||
|
||||
fileprivate var imageViewWidthConstraint: NSLayoutConstraint!
|
||||
fileprivate var imageViewHeightConstraint: NSLayoutConstraint!
|
||||
private var imageViewWidthConstraint: NSLayoutConstraint!
|
||||
private var imageViewHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
fileprivate var textLabelBottomAnchorConstraint: NSLayoutConstraint!
|
||||
private var textLabelBottomAnchorConstraint: NSLayoutConstraint!
|
||||
|
||||
fileprivate var textLabelVerticalSpacingConstraint: NSLayoutConstraint!
|
||||
fileprivate var textLabelFocusedVerticalSpacingConstraint: NSLayoutConstraint?
|
||||
private var textLabelVerticalSpacingConstraint: NSLayoutConstraint!
|
||||
private var textLabelFocusedVerticalSpacingConstraint: NSLayoutConstraint?
|
||||
|
||||
override init(frame: CGRect)
|
||||
{
|
||||
@ -109,13 +109,15 @@ class GridCollectionViewCell: UICollectionViewCell
|
||||
// Image View
|
||||
self.imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
self.imageView.topAnchor.constraint(equalTo: self.contentView.topAnchor).isActive = true
|
||||
self.imageView.centerXAnchor.constraint(equalTo: self.contentView.centerXAnchor).isActive = true
|
||||
self.imageView.topAnchor.constraint(equalTo: self.vibrancyView.topAnchor).isActive = true
|
||||
self.imageView.centerXAnchor.constraint(equalTo: self.vibrancyView.centerXAnchor).isActive = true
|
||||
|
||||
self.imageViewWidthConstraint = self.imageView.widthAnchor.constraint(equalToConstant: self.maximumImageSize.width)
|
||||
self.imageViewWidthConstraint = self.imageView.widthAnchor.constraint(equalToConstant: 150)
|
||||
self.imageViewWidthConstraint.isActive = true
|
||||
|
||||
self.imageViewHeightConstraint = self.imageView.heightAnchor.constraint(equalToConstant: self.maximumImageSize.height)
|
||||
self.imageViewHeightConstraint = self.imageView.heightAnchor.constraint(equalToConstant: 120)
|
||||
|
||||
self.imageViewHeightConstraint.priority = UILayoutPriority(999) // Fixes "Unable to simultaneously satisfy constraints" runtime error when inserting new grid row.
|
||||
self.imageViewHeightConstraint.isActive = true
|
||||
|
||||
|
||||
@ -181,10 +183,15 @@ private extension GridCollectionViewCell
|
||||
{
|
||||
func updateMaximumImageSize()
|
||||
{
|
||||
self.imageViewWidthConstraint.constant = self.maximumImageSize.width
|
||||
self.imageViewHeightConstraint.constant = self.maximumImageSize.height
|
||||
self.imageViewWidthConstraint.constant = 150
|
||||
self.imageViewHeightConstraint.constant = 120
|
||||
|
||||
self.textLabelVerticalSpacingConstraint.constant = 8
|
||||
self.textLabelFocusedVerticalSpacingConstraint?.constant = self.maximumImageSize.height / 10.0
|
||||
// 修改 textLabel 的固定高度为 30
|
||||
// let textLabelFixedHeight: CGFloat = 30
|
||||
//
|
||||
// self.textLabelVerticalSpacingConstraint.constant = textLabelFixedHeight
|
||||
// self.textLabelFocusedVerticalSpacingConstraint?.constant = textLabelFixedHeight / 50
|
||||
}
|
||||
}
|
||||
|
||||
@ -23,22 +23,63 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
|
||||
// If only one row, distribute the items equally horizontally
|
||||
var usesEqualHorizontalSpacingDistributionForSingleRow = false
|
||||
|
||||
private var contentInset: UIEdgeInsets {
|
||||
guard let collectionView = self.collectionView else { return .zero }
|
||||
|
||||
var contentInset = collectionView.contentInset
|
||||
contentInset.left += collectionView.safeAreaInsets.left
|
||||
contentInset.right += collectionView.safeAreaInsets.right
|
||||
|
||||
return contentInset
|
||||
}
|
||||
|
||||
private var contentWidth: CGFloat {
|
||||
guard let collectionView = self.collectionView else { return 0.0 }
|
||||
|
||||
let contentWidth = collectionView.bounds.width - (self.contentInset.left + self.contentInset.right)
|
||||
return contentWidth
|
||||
}
|
||||
|
||||
private var maximumItemsPerRow: Int {
|
||||
let maximumItemsPerRow = Int(floor((self.contentWidth - self.minimumInteritemSpacing) / (self.itemWidth + self.minimumInteritemSpacing)))
|
||||
return maximumItemsPerRow
|
||||
}
|
||||
|
||||
private var interitemSpacing: CGFloat {
|
||||
let interitemSpacing = (self.contentWidth - CGFloat(self.maximumItemsPerRow) * self.itemWidth) / CGFloat(self.maximumItemsPerRow + 1)
|
||||
return interitemSpacing
|
||||
}
|
||||
|
||||
private var cachedCellLayoutAttributes = [IndexPath: UICollectionViewLayoutAttributes]()
|
||||
|
||||
override var estimatedItemSize: CGSize {
|
||||
didSet {
|
||||
fatalError("GridCollectionViewLayout does not support self-sizing cells.")
|
||||
}
|
||||
}
|
||||
|
||||
override func prepare()
|
||||
{
|
||||
super.prepare()
|
||||
|
||||
self.sectionInset.left = self.interitemSpacing + self.contentInset.left
|
||||
self.sectionInset.right = self.interitemSpacing + self.contentInset.right
|
||||
}
|
||||
|
||||
override func invalidateLayout(with context: UICollectionViewLayoutInvalidationContext)
|
||||
{
|
||||
super.invalidateLayout(with: context)
|
||||
|
||||
if let context = context as? UICollectionViewFlowLayoutInvalidationContext,
|
||||
context.invalidateFlowLayoutAttributes || context.invalidateFlowLayoutDelegateMetrics || context.invalidateEverything
|
||||
{
|
||||
// Clear layout cache to prevent crashing due to returning outdated layout attributes.
|
||||
self.cachedCellLayoutAttributes = [:]
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]?
|
||||
{
|
||||
guard let collectionView = self.collectionView else { return nil }
|
||||
|
||||
let maximumItemsPerRow = floor((collectionView.bounds.width - self.minimumInteritemSpacing) / (self.itemWidth + self.minimumInteritemSpacing))
|
||||
let interitemSpacing = (collectionView.bounds.width - maximumItemsPerRow * self.itemWidth) / (maximumItemsPerRow + 1)
|
||||
|
||||
self.sectionInset.left = interitemSpacing
|
||||
self.sectionInset.right = interitemSpacing
|
||||
|
||||
let layoutAttributes = super.layoutAttributesForElements(in: rect)?.map({ $0.copy() }) as! [UICollectionViewLayoutAttributes]
|
||||
|
||||
var minimumY: CGFloat? = nil
|
||||
@ -58,7 +99,7 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
|
||||
|
||||
if abs(attributes.frame.minX - self.sectionInset.left) > 1
|
||||
{
|
||||
attributes.frame.origin.x = previousLayoutAttributes.frame.maxX + interitemSpacing
|
||||
attributes.frame.origin.x = previousLayoutAttributes.frame.maxX + self.interitemSpacing
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,7 +111,7 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
|
||||
{
|
||||
isSingleRow = false
|
||||
|
||||
self.alignLayoutAttributes(tempLayoutAttributes, toMinimumY: minY)
|
||||
self.align(tempLayoutAttributes, toMinimumY: minY)
|
||||
|
||||
// Reset tempLayoutAttributes
|
||||
tempLayoutAttributes.removeAll()
|
||||
@ -97,27 +138,42 @@ class GridCollectionViewLayout: UICollectionViewFlowLayout
|
||||
// Handle the remaining tempLayoutAttributes
|
||||
if let minimumY = minimumY
|
||||
{
|
||||
self.alignLayoutAttributes(tempLayoutAttributes, toMinimumY: minimumY)
|
||||
self.align(tempLayoutAttributes, toMinimumY: minimumY)
|
||||
|
||||
if isSingleRow && self.usesEqualHorizontalSpacingDistributionForSingleRow
|
||||
{
|
||||
let spacing = (collectionView.bounds.width - (self.itemWidth * CGFloat(tempLayoutAttributes.count))) / (CGFloat(tempLayoutAttributes.count) + 1.0)
|
||||
let spacing = (self.contentWidth - (self.itemWidth * CGFloat(tempLayoutAttributes.count))) / (CGFloat(tempLayoutAttributes.count) + 1.0)
|
||||
|
||||
for (index, layoutAttributes) in tempLayoutAttributes.enumerated()
|
||||
{
|
||||
layoutAttributes.frame.origin.x = spacing + (spacing + self.itemWidth) * CGFloat(index)
|
||||
layoutAttributes.frame.origin.x = spacing + (spacing + self.itemWidth) * CGFloat(index) + self.contentInset.left
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for attributes in layoutAttributes where attributes.representedElementCategory == .cell
|
||||
{
|
||||
// Update cached attributes for layoutAttributesForItem(at:)
|
||||
self.cachedCellLayoutAttributes[attributes.indexPath] = attributes
|
||||
}
|
||||
|
||||
return layoutAttributes
|
||||
}
|
||||
|
||||
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes?
|
||||
{
|
||||
if let cachedAttributes = self.cachedCellLayoutAttributes[indexPath]
|
||||
{
|
||||
return cachedAttributes
|
||||
}
|
||||
|
||||
return super.layoutAttributesForItem(at: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
private extension GridCollectionViewLayout
|
||||
{
|
||||
func alignLayoutAttributes(_ layoutAttributes: [UICollectionViewLayoutAttributes], toMinimumY minimumY: CGFloat)
|
||||
func align(_ layoutAttributes: [UICollectionViewLayoutAttributes], toMinimumY minimumY: CGFloat)
|
||||
{
|
||||
for attributes in layoutAttributes
|
||||
{
|
||||
|
||||
@ -65,12 +65,12 @@ class LoadControllerSkinImageOperation: RSTLoadOperation<UIImage, ControllerSkin
|
||||
|
||||
override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void)
|
||||
{
|
||||
guard self.controllerSkin.supports(self.traits) else {
|
||||
guard let traits = self.controllerSkin.supportedTraits(for: self.traits) else {
|
||||
completion(nil, Error.unsupportedTraits)
|
||||
return
|
||||
}
|
||||
|
||||
guard let image = self.controllerSkin.image(for: self.traits, preferredSize: self.size) else {
|
||||
guard let image = self.controllerSkin.image(for: traits, preferredSize: self.size) else {
|
||||
completion(nil, Error.doesNotExist)
|
||||
return
|
||||
}
|
||||
|
||||
@ -45,6 +45,11 @@ class LoadImageURLOperation: RSTLoadOperation<UIImage, NSURL>
|
||||
super.cancel()
|
||||
|
||||
self.downloadOperation?.cancel()
|
||||
|
||||
if self.isAsynchronous
|
||||
{
|
||||
self.finish()
|
||||
}
|
||||
}
|
||||
|
||||
override func loadResult(completion: @escaping (UIImage?, Swift.Error?) -> Void)
|
||||
|
||||
65
Delta/Components/Popover Menu/ListMenuViewController.swift
Normal file
65
Delta/Components/Popover Menu/ListMenuViewController.swift
Normal file
@ -0,0 +1,65 @@
|
||||
//
|
||||
// PopoverMenuViewController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 9/2/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import Roxas
|
||||
|
||||
class ListMenuViewController: UITableViewController
|
||||
{
|
||||
var items: [MenuItem] {
|
||||
get { return self.dataSource.items }
|
||||
set { self.dataSource.items = newValue }
|
||||
}
|
||||
|
||||
private let dataSource = RSTArrayTableViewDataSource<MenuItem>(items: [])
|
||||
|
||||
override var preferredContentSize: CGSize {
|
||||
get {
|
||||
// Don't include navigation bar height in calculation (as of iOS 13).
|
||||
let navigationBarHeight = 0.0 // self.navigationController?.navigationBar.bounds.height ?? 0.0
|
||||
return CGSize(width: 0, height: (self.tableView.rowHeight * CGFloat(self.items.count)) + navigationBarHeight)
|
||||
}
|
||||
set {}
|
||||
}
|
||||
|
||||
init()
|
||||
{
|
||||
super.init(style: .plain)
|
||||
|
||||
self.dataSource.cellConfigurationHandler = { (cell, item, indexPath) in
|
||||
cell.textLabel?.text = item.text
|
||||
cell.accessoryType = item.isSelected ? .checkmark : .none
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
self.tableView.rowHeight = 44
|
||||
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
extension ListMenuViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
let item = self.dataSource.item(at: indexPath)
|
||||
item.isSelected = !item.isSelected
|
||||
item.action(item)
|
||||
|
||||
self.tableView.reloadData()
|
||||
}
|
||||
}
|
||||
160
Delta/Components/Popover Menu/PopoverMenuButton.swift
Normal file
160
Delta/Components/Popover Menu/PopoverMenuButton.swift
Normal file
@ -0,0 +1,160 @@
|
||||
//
|
||||
// PopoverMenuButton.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 9/2/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UINavigationBar
|
||||
{
|
||||
fileprivate var defaultTitleTextAttributes: [NSAttributedString.Key: Any]? {
|
||||
if let textAttributes = self._defaultTitleTextAttributes
|
||||
{
|
||||
return textAttributes
|
||||
}
|
||||
|
||||
// Make "copy" of self.
|
||||
let navigationBar = UINavigationBar(frame: self.bounds) // Use self.bounds to avoid "Unable to simultaneously satisfy constraints" runtime error.
|
||||
navigationBar.barStyle = self.barStyle
|
||||
|
||||
// Set item with title so we can retrieve default text attributes.
|
||||
let navigationItem = UINavigationItem(title: "Testut")
|
||||
navigationBar.items = [navigationItem]
|
||||
navigationBar.isHidden = true
|
||||
|
||||
// Must be added to window hierarchy for it to create title UILabel.
|
||||
self.addSubview(navigationBar)
|
||||
defer { navigationBar.removeFromSuperview() }
|
||||
|
||||
navigationBar.layoutIfNeeded()
|
||||
|
||||
let textAttributes = navigationBar._defaultTitleTextAttributes
|
||||
return textAttributes
|
||||
}
|
||||
|
||||
private var _defaultTitleTextAttributes: [NSAttributedString.Key: Any]? {
|
||||
guard self.titleTextAttributes == nil else { return self.titleTextAttributes }
|
||||
|
||||
guard let contentView = self.subviews.first(where: { NSStringFromClass(type(of: $0)).contains("ContentView") || NSStringFromClass(type(of: $0)).contains("ItemView") })
|
||||
else { return nil }
|
||||
|
||||
let containerView: UIView
|
||||
|
||||
//TODO: Recursively search all subviews for title UILabel instead of hardcoded OS version-specific hierarchy traversals...
|
||||
if #available(iOS 16, *)
|
||||
{
|
||||
guard let titleControl = contentView.subviews.first(where: { NSStringFromClass(type(of: $0)).contains("Title") }) else { return nil }
|
||||
|
||||
if #available(iOS 17, *)
|
||||
{
|
||||
guard let view = titleControl.subviews.first else { return nil }
|
||||
containerView = view
|
||||
}
|
||||
else
|
||||
{
|
||||
containerView = titleControl
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
containerView = contentView
|
||||
}
|
||||
|
||||
guard let titleLabel = containerView.subviews.first(where: { $0 is UILabel }) as? UILabel else { return nil }
|
||||
|
||||
let textAttributes = titleLabel.attributedText?.attributes(at: 0, effectiveRange: nil)
|
||||
return textAttributes
|
||||
}
|
||||
}
|
||||
|
||||
class PopoverMenuButton: UIControl
|
||||
{
|
||||
var title: String {
|
||||
get { return self.textLabel.text ?? "" }
|
||||
set {
|
||||
self.textLabel.text = newValue
|
||||
self.updateTextAttributes()
|
||||
self.invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
private let textLabel: UILabel
|
||||
private let arrowLabel: UILabel
|
||||
private let stackView: UIStackView
|
||||
|
||||
private var _didLayoutSubviews = false
|
||||
|
||||
private var parentNavigationBar: UINavigationBar? {
|
||||
guard let navigationController = self.parentViewController as? UINavigationController ?? self.parentViewController?.navigationController else { return nil }
|
||||
guard self.isDescendant(of: navigationController.navigationBar) else { return nil }
|
||||
|
||||
return navigationController.navigationBar
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
}
|
||||
|
||||
init()
|
||||
{
|
||||
self.textLabel = UILabel()
|
||||
self.textLabel.textColor = .black
|
||||
|
||||
self.arrowLabel = UILabel()
|
||||
self.arrowLabel.text = "▾"
|
||||
self.arrowLabel.textColor = .black
|
||||
|
||||
self.stackView = UIStackView(arrangedSubviews: [self.textLabel, self.arrowLabel])
|
||||
self.stackView.axis = .horizontal
|
||||
self.stackView.distribution = .fillProportionally
|
||||
self.stackView.alignment = .center
|
||||
self.stackView.spacing = 4.0
|
||||
self.stackView.isUserInteractionEnabled = false
|
||||
|
||||
let intrinsicContentSize = self.stackView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
super.init(frame: CGRect(origin: .zero, size: intrinsicContentSize))
|
||||
|
||||
self.addSubview(self.stackView, pinningEdgesWith: .zero)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func didMoveToSuperview()
|
||||
{
|
||||
self.updateTextAttributes()
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
if !_didLayoutSubviews
|
||||
{
|
||||
_didLayoutSubviews = true
|
||||
|
||||
// didMoveToSuperview() can be too early to accurately
|
||||
// update text attributes, so ensure we also update
|
||||
// during first layoutSubviews() call.
|
||||
self.updateTextAttributes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension PopoverMenuButton
|
||||
{
|
||||
func updateTextAttributes()
|
||||
{
|
||||
guard let parentNavigationBar = self.parentNavigationBar else { return }
|
||||
guard let textAttributes = parentNavigationBar.defaultTitleTextAttributes else { return }
|
||||
|
||||
for label in [self.textLabel, self.arrowLabel]
|
||||
{
|
||||
label.attributedText = NSAttributedString(string: label.text ?? "", attributes: textAttributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
102
Delta/Components/Popover Menu/PopoverMenuController.swift
Normal file
102
Delta/Components/Popover Menu/PopoverMenuController.swift
Normal file
@ -0,0 +1,102 @@
|
||||
//
|
||||
// PopoverMenuController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 9/5/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
private var popoverMenuControllerKey: UInt8 = 0
|
||||
|
||||
extension UINavigationItem
|
||||
{
|
||||
var popoverMenuController: PopoverMenuController? {
|
||||
get { return objc_getAssociatedObject(self, &popoverMenuControllerKey) as? PopoverMenuController }
|
||||
set {
|
||||
self.titleView = newValue?.popoverMenuButton
|
||||
objc_setAssociatedObject(self, &popoverMenuControllerKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PopoverMenuController: NSObject
|
||||
{
|
||||
let popoverViewController: UIViewController
|
||||
|
||||
let popoverMenuButton: PopoverMenuButton
|
||||
|
||||
var isActive: Bool = false {
|
||||
willSet {
|
||||
guard newValue != self.isActive else { return }
|
||||
|
||||
if newValue
|
||||
{
|
||||
self.presentPopoverViewController()
|
||||
}
|
||||
else
|
||||
{
|
||||
self.dismissPopoverViewController()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init(popoverViewController: UIViewController)
|
||||
{
|
||||
self.popoverViewController = popoverViewController
|
||||
|
||||
self.popoverMenuButton = PopoverMenuButton()
|
||||
|
||||
super.init()
|
||||
|
||||
self.popoverMenuButton.addTarget(self, action: #selector(PopoverMenuController.pressedPopoverMenuButton(_:)), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
private extension PopoverMenuController
|
||||
{
|
||||
@objc func pressedPopoverMenuButton(_ button: PopoverMenuButton)
|
||||
{
|
||||
self.isActive = !self.isActive
|
||||
}
|
||||
|
||||
func presentPopoverViewController()
|
||||
{
|
||||
guard !self.isActive else { return }
|
||||
|
||||
guard let presentingViewController = self.popoverMenuButton.parentViewController else { return }
|
||||
|
||||
self.popoverViewController.modalPresentationStyle = .popover
|
||||
self.popoverViewController.popoverPresentationController?.delegate = self
|
||||
self.popoverViewController.popoverPresentationController?.sourceView = self.popoverMenuButton.superview
|
||||
self.popoverViewController.popoverPresentationController?.sourceRect = self.popoverMenuButton.frame
|
||||
|
||||
presentingViewController.present(self.popoverViewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
func dismissPopoverViewController()
|
||||
{
|
||||
guard self.isActive else { return }
|
||||
|
||||
self.popoverViewController.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
extension PopoverMenuController: UIPopoverPresentationControllerDelegate
|
||||
{
|
||||
func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
|
||||
{
|
||||
// Force popover presentation, regardless of trait collection.
|
||||
return .none
|
||||
}
|
||||
|
||||
func popoverPresentationControllerDidDismissPopover(_ popoverPresentationController: UIPopoverPresentationController)
|
||||
{
|
||||
self.isActive = false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,68 @@
|
||||
//
|
||||
// PopoverMenuViewController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 9/2/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import Roxas
|
||||
|
||||
class ListMenuViewController: UITableViewController
|
||||
{
|
||||
var items: [MenuItem] {
|
||||
get { return self.dataSource.items }
|
||||
set { self.dataSource.items = newValue; }
|
||||
}
|
||||
|
||||
fileprivate let dataSource = RSTArrayTableViewDataSource<MenuItem>(items: [])
|
||||
|
||||
override var preferredContentSize: CGSize {
|
||||
get {
|
||||
let navigationBarHeight = self.navigationController?.navigationBar.bounds.height ?? 0.0
|
||||
return CGSize(width: 0, height: (self.tableView.rowHeight * CGFloat(self.items.count)) + navigationBarHeight)
|
||||
}
|
||||
set {}
|
||||
}
|
||||
|
||||
init()
|
||||
{
|
||||
super.init(style: .plain)
|
||||
|
||||
self.dataSource.cellConfigurationHandler = { (cell, item, indexPath) in
|
||||
cell.textLabel?.text = item.text
|
||||
cell.accessoryType = item.isSelected ? .checkmark : .none
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
self.tableView.rowHeight = 44
|
||||
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
}
|
||||
}
|
||||
|
||||
extension ListMenuViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
self.items.forEach { $0.isSelected = false }
|
||||
|
||||
let item = self.dataSource.item(at: indexPath)
|
||||
item.isSelected = true
|
||||
item.action(item)
|
||||
|
||||
self.tableView.reloadData()
|
||||
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
73
Delta/Components/Table View/BadgedTableViewCell.swift
Normal file
73
Delta/Components/Table View/BadgedTableViewCell.swift
Normal file
@ -0,0 +1,73 @@
|
||||
//
|
||||
// BadgedTableViewCell.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 11/20/18.
|
||||
// Copyright © 2018 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class BadgedTableViewCell: UITableViewCell
|
||||
{
|
||||
let badgeLabel = UILabel()
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
self.initialize()
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?)
|
||||
{
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
|
||||
self.initialize()
|
||||
}
|
||||
|
||||
private func initialize()
|
||||
{
|
||||
self.badgeLabel.clipsToBounds = true
|
||||
self.badgeLabel.textAlignment = .center
|
||||
self.badgeLabel.backgroundColor = .red
|
||||
self.badgeLabel.font = UIFont.boldSystemFont(ofSize: 14)
|
||||
self.badgeLabel.textColor = .white
|
||||
self.contentView.addSubview(self.badgeLabel)
|
||||
}
|
||||
|
||||
override func layoutSubviews()
|
||||
{
|
||||
super.layoutSubviews()
|
||||
|
||||
guard let textLabel = self.textLabel else { return }
|
||||
|
||||
let spacing = 8 as CGFloat
|
||||
|
||||
var contentSize = self.badgeLabel.intrinsicContentSize
|
||||
contentSize.width += 10
|
||||
contentSize.height += 10
|
||||
contentSize.width = max(contentSize.width, contentSize.height)
|
||||
|
||||
var frame = CGRect(x: self.contentView.bounds.maxX - contentSize.width,
|
||||
y: self.contentView.bounds.midY - contentSize.height / 2,
|
||||
width: contentSize.width,
|
||||
height: contentSize.height)
|
||||
|
||||
if self.accessoryType == .none
|
||||
{
|
||||
frame.origin.x -= spacing
|
||||
}
|
||||
|
||||
self.badgeLabel.frame = frame
|
||||
self.badgeLabel.layer.cornerRadius = frame.height / 2
|
||||
|
||||
self.badgeLabel.backgroundColor = .red
|
||||
|
||||
let overlap = textLabel.frame.maxX - (frame.minX - spacing)
|
||||
if overlap > 0 && !self.badgeLabel.isHidden
|
||||
{
|
||||
textLabel.frame.size.width -= overlap
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// GameMetadataTableViewCell.swift
|
||||
// GameTableViewCell.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 3/27/17.
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class GameMetadataTableViewCell: UITableViewCell
|
||||
class GameTableViewCell: UITableViewCell
|
||||
{
|
||||
@IBOutlet private(set) var nameLabel: UILabel!
|
||||
@IBOutlet private(set) var artworkImageView: UIImageView!
|
||||
63
Delta/Components/Table View/GameTableViewCell.xib
Normal file
63
Delta/Components/Table View/GameTableViewCell.xib
Normal file
@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="Aspect ratio constraints" minToolsVersion="5.1"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="Cell" id="FL0-zT-qa3" customClass="GameTableViewCell" customModule="Delta" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="97"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="FL0-zT-qa3" id="zSi-4a-DaH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5dT-zd-huQ" userLabel="Selected Background View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="96.5"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
<imageView userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BoxArt" translatesAutoresizingMaskIntoConstraints="NO" id="68X-vf-MNx">
|
||||
<rect key="frame" x="15" y="8" width="80" height="80"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="68X-vf-MNx" secondAttribute="height" multiplier="1:1" id="P6f-Lc-8B3"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Super Mario World" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hKz-BX-p8h">
|
||||
<rect key="frame" x="110" y="38.5" width="250" height="20.5"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<color key="textColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="hKz-BX-p8h" secondAttribute="bottom" constant="1" id="1JB-It-w1s"/>
|
||||
<constraint firstItem="68X-vf-MNx" firstAttribute="leading" secondItem="zSi-4a-DaH" secondAttribute="leading" constant="15" id="CD9-Qz-0UL"/>
|
||||
<constraint firstAttribute="trailing" secondItem="hKz-BX-p8h" secondAttribute="trailing" constant="15" id="RxT-jB-cvp"/>
|
||||
<constraint firstItem="68X-vf-MNx" firstAttribute="top" secondItem="zSi-4a-DaH" secondAttribute="top" constant="8" id="T5j-O5-aTX"/>
|
||||
<constraint firstItem="hKz-BX-p8h" firstAttribute="leading" secondItem="68X-vf-MNx" secondAttribute="trailing" constant="15" id="jks-s2-5ZX"/>
|
||||
<constraint firstItem="hKz-BX-p8h" firstAttribute="top" relation="greaterThanOrEqual" secondItem="zSi-4a-DaH" secondAttribute="top" constant="1" id="swc-Ib-2wh"/>
|
||||
<constraint firstItem="hKz-BX-p8h" firstAttribute="centerY" secondItem="zSi-4a-DaH" secondAttribute="centerY" id="uyb-vA-Qtb"/>
|
||||
<constraint firstAttribute="bottom" secondItem="68X-vf-MNx" secondAttribute="bottom" constant="8" id="wGX-lV-ACr"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="artworkImageView" destination="68X-vf-MNx" id="Sda-Tl-WEd"/>
|
||||
<outlet property="artworkImageViewLeadingConstraint" destination="CD9-Qz-0UL" id="jhw-i7-9ak"/>
|
||||
<outlet property="artworkImageViewTrailingConstraint" destination="jks-s2-5ZX" id="vrM-OV-rsa"/>
|
||||
<outlet property="nameLabel" destination="hKz-BX-p8h" id="gdI-v9-dj3"/>
|
||||
<outlet property="selectedBackgroundView" destination="5dT-zd-huQ" id="jOr-DK-8bp"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="BoxArt" width="100" height="100"/>
|
||||
</resources>
|
||||
</document>
|
||||
143
Delta/Database/Cheats/CheatBase.swift
Normal file
143
Delta/Database/Cheats/CheatBase.swift
Normal file
@ -0,0 +1,143 @@
|
||||
//
|
||||
// CheatBase.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 1/17/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
import Roxas
|
||||
|
||||
private extension UserDefaults
|
||||
{
|
||||
@NSManaged var previousCheatBaseVersion: Int
|
||||
}
|
||||
|
||||
extension ExpressionType
|
||||
{
|
||||
static var cheatID: SQLite.Expression<Int> {
|
||||
return SQLite.Expression<Int>("cheatID")
|
||||
}
|
||||
|
||||
static var cheatName: SQLite.Expression<String> {
|
||||
return SQLite.Expression<String>("cheatName")
|
||||
}
|
||||
|
||||
static var cheatDescription: SQLite.Expression<String?> {
|
||||
return SQLite.Expression<String?>("cheatDescription")
|
||||
}
|
||||
|
||||
static var cheatCode: SQLite.Expression<String> {
|
||||
return SQLite.Expression<String>("cheatCode")
|
||||
}
|
||||
|
||||
static var cheatDeviceID: SQLite.Expression<Int> {
|
||||
return SQLite.Expression<Int>("cheatDeviceID")
|
||||
}
|
||||
|
||||
static var cheatActivation: SQLite.Expression<String?> {
|
||||
return SQLite.Expression<String?>("cheatActivation")
|
||||
}
|
||||
|
||||
static var cheatCategoryID: SQLite.Expression<Int> {
|
||||
return SQLite.Expression<Int>("cheatCategoryID")
|
||||
}
|
||||
|
||||
static var cheatCategoryName: SQLite.Expression<String> {
|
||||
return SQLite.Expression<String>("cheatCategory")
|
||||
}
|
||||
|
||||
static var cheatCategoryDescription: SQLite.Expression<String> {
|
||||
return SQLite.Expression<String>("cheatCategoryDescription")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Table
|
||||
{
|
||||
static var cheats: Table {
|
||||
return Table("CHEATS")
|
||||
}
|
||||
|
||||
static var cheatCategories: Table {
|
||||
return Table("CHEAT_CATEGORIES")
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
class CheatBase: GamesDatabase
|
||||
{
|
||||
static let cheatsVersion = 1
|
||||
static var previousCheatsVersion: Int? {
|
||||
return UserDefaults.standard.previousCheatBaseVersion
|
||||
}
|
||||
|
||||
private let connection: Connection
|
||||
|
||||
override init() throws
|
||||
{
|
||||
let fileURL = DatabaseManager.cheatBaseURL
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path) else { throw GamesDatabase.Error.doesNotExist }
|
||||
|
||||
self.connection = try Connection(fileURL.path)
|
||||
|
||||
try super.init()
|
||||
|
||||
UserDefaults.standard.previousCheatBaseVersion = CheatBase.cheatsVersion
|
||||
}
|
||||
|
||||
func cheats(for game: Game) async throws -> [CheatMetadata]?
|
||||
{
|
||||
let metadata = await withCheckedContinuation { continuation in
|
||||
if let context = game.managedObjectContext
|
||||
{
|
||||
context.perform {
|
||||
let metadata = self.metadata(for: game)
|
||||
continuation.resume(returning: metadata)
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
let metadata = self.metadata(for: game)
|
||||
continuation.resume(returning: metadata)
|
||||
}
|
||||
}
|
||||
|
||||
guard let romIDValue = metadata?.romID else { return nil }
|
||||
|
||||
let cheatID = Expression<Any>.cheatID
|
||||
let cheatName = Expression<Any>.cheatName
|
||||
let cheatCode = Expression<Any>.cheatCode
|
||||
let cheatDescription = Expression<Any>.cheatDescription
|
||||
let cheatActivation = Expression<Any>.cheatActivation
|
||||
let cheatDeviceID = Expression<Any>.cheatDeviceID
|
||||
|
||||
let categoryID = Expression<Any>.cheatCategoryID
|
||||
let categoryName = Expression<Any>.cheatCategoryName
|
||||
let categoryDescription = Expression<Any>.cheatCategoryDescription
|
||||
|
||||
let romID = Expression<Any>.romID
|
||||
|
||||
let query = Table.cheats.select(cheatID, cheatName, cheatCode, cheatDescription, cheatActivation, cheatDeviceID, Table.cheats[categoryID], categoryName, categoryDescription)
|
||||
.filter(romID == romIDValue)
|
||||
.join(Table.cheatCategories, on: Table.cheats[categoryID] == Table.cheatCategories[categoryID])
|
||||
.order(cheatName)
|
||||
|
||||
let rows = try self.connection.prepare(query)
|
||||
|
||||
let results = rows.compactMap { (row) -> CheatMetadata? in
|
||||
guard case let deviceID = Int16(row[cheatDeviceID]), let device = CheatDevice(rawValue: deviceID) else { return nil }
|
||||
|
||||
let id = row[Table.cheats[categoryID]]
|
||||
|
||||
let category = CheatCategory(id: id, name: row[categoryName], categoryDescription: row[categoryDescription])
|
||||
let metadata = CheatMetadata(id: row[cheatID], name: row[cheatName], code: row[cheatCode], description: row[cheatDescription], activationHint: row[cheatActivation], device: device, category: category)
|
||||
return metadata
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
}
|
||||
273
Delta/Database/Cheats/CheatBaseView.swift
Normal file
273
Delta/Database/Cheats/CheatBaseView.swift
Normal file
@ -0,0 +1,273 @@
|
||||
//
|
||||
// CheatBaseView.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 1/17/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import enum SQLite.Result
|
||||
|
||||
@available(iOS 14, *)
|
||||
extension CheatBaseView
|
||||
{
|
||||
private class ViewModel: ObservableObject
|
||||
{
|
||||
@Published
|
||||
var allCheats: [CheatMetadata]? {
|
||||
didSet {
|
||||
guard let cheats = allCheats else {
|
||||
self.cheatsByCategory = nil
|
||||
return
|
||||
}
|
||||
|
||||
let cheatsByCategory = Dictionary(grouping: cheats, by: { $0.category }).sorted { $0.key.id < $1.key.id }
|
||||
self.cheatsByCategory = cheatsByCategory
|
||||
}
|
||||
}
|
||||
|
||||
@Published
|
||||
private(set) var cheatsByCategory: [(CheatCategory, [CheatMetadata])]?
|
||||
|
||||
@Published
|
||||
private(set) var error: Error?
|
||||
|
||||
@Published
|
||||
var searchText: String = "" {
|
||||
didSet {
|
||||
self.searchCheats()
|
||||
}
|
||||
}
|
||||
|
||||
@Published
|
||||
private(set) var filteredCheats: [CheatMetadata]?
|
||||
|
||||
@MainActor
|
||||
func fetchCheats(for game: Game) async
|
||||
{
|
||||
guard self.allCheats == nil else { return }
|
||||
|
||||
do
|
||||
{
|
||||
let database = try CheatBase()
|
||||
let cheats = try await database.cheats(for: game) ?? []
|
||||
self.allCheats = cheats
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.error = error
|
||||
}
|
||||
}
|
||||
|
||||
private func searchCheats()
|
||||
{
|
||||
if let cheats = self.allCheats, !self.searchText.isEmpty
|
||||
{
|
||||
let predicate = NSPredicate(forSearchingForText: self.searchText, inValuesForKeyPaths: [#keyPath(CheatMetadata.name), #keyPath(CheatMetadata.cheatDescription)])
|
||||
|
||||
let filteredCheats = cheats.filter { predicate.evaluate(with: $0) }
|
||||
self.filteredCheats = filteredCheats
|
||||
}
|
||||
else
|
||||
{
|
||||
self.filteredCheats = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
struct CheatBaseView: View
|
||||
{
|
||||
let game: Game?
|
||||
|
||||
var cancellationHandler: (() -> Void)?
|
||||
var selectionHandler: ((CheatMetadata) -> Void)?
|
||||
|
||||
@StateObject
|
||||
private var viewModel = ViewModel()
|
||||
|
||||
@State
|
||||
private var activationHintCheat: CheatMetadata?
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
if let cheats = viewModel.allCheats, !cheats.isEmpty
|
||||
{
|
||||
// Only show List if there is at least one cheat for this game.
|
||||
cheatList()
|
||||
}
|
||||
|
||||
// Place above List
|
||||
placeholderView()
|
||||
}
|
||||
.alert(item: $activationHintCheat) { cheat in
|
||||
Alert(title: Text("How to Activate"),
|
||||
message: Text(cheat.activationHint ?? ""),
|
||||
dismissButton: .default(Text("OK")))
|
||||
}
|
||||
.navigationTitle(Text(game?.name ?? "CheatBase"))
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Cancel") {
|
||||
cancellationHandler?()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
Task {
|
||||
guard let game = self.game else { return }
|
||||
await viewModel.fetchCheats(for: game)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func cheatList() -> some View
|
||||
{
|
||||
VStack {
|
||||
if #unavailable(iOS 15)
|
||||
{
|
||||
LegacySearchBar(text: $viewModel.searchText)
|
||||
}
|
||||
|
||||
let listView = List {
|
||||
if let filteredCheats = viewModel.filteredCheats
|
||||
{
|
||||
ForEach(filteredCheats) { cheat in
|
||||
cell(for: cheat)
|
||||
}
|
||||
}
|
||||
else if let cheats = viewModel.cheatsByCategory
|
||||
{
|
||||
ForEach(cheats, id: \.0.id) { (category, cheats) in
|
||||
Section {
|
||||
DisclosureGroup {
|
||||
ForEach(cheats) { cheat in
|
||||
cell(for: cheat)
|
||||
}
|
||||
} label: {
|
||||
Text(category.name)
|
||||
}
|
||||
} footer: {
|
||||
Text(category.categoryDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
listView.searchable(text: $viewModel.searchText)
|
||||
}
|
||||
else
|
||||
{
|
||||
listView
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .top)
|
||||
}
|
||||
|
||||
|
||||
private func cell(for cheat: CheatMetadata) -> some View
|
||||
{
|
||||
ZStack(alignment: .leading) {
|
||||
Button(action: { choose(cheat) }) {}
|
||||
|
||||
HStack {
|
||||
// Name + Description
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(cheat.name)
|
||||
|
||||
if let description = cheat.cheatDescription
|
||||
{
|
||||
Text(description)
|
||||
.font(.caption)
|
||||
}
|
||||
}
|
||||
|
||||
// Activation Hint
|
||||
if cheat.activationHint != nil
|
||||
{
|
||||
Spacer()
|
||||
|
||||
Button(action: { activationHintCheat = cheat }) {
|
||||
Image(systemName: "info.circle")
|
||||
}
|
||||
.buttonStyle(.borderless)
|
||||
}
|
||||
}
|
||||
.multilineTextAlignment(.leading)
|
||||
}
|
||||
}
|
||||
|
||||
private func placeholderView() -> some View
|
||||
{
|
||||
VStack(spacing: 8) {
|
||||
if let error = viewModel.error
|
||||
{
|
||||
Text("Unable to Load Cheats")
|
||||
.font(.title)
|
||||
|
||||
if let error = error as? SQLite.Result
|
||||
{
|
||||
// SQLite.Result implements CustomStringConvertible.description, but not localizedDescription.
|
||||
Text(String(describing: error))
|
||||
.font(.callout)
|
||||
}
|
||||
else
|
||||
{
|
||||
Text(error.localizedDescription)
|
||||
.font(.callout)
|
||||
}
|
||||
}
|
||||
else if let filteredCheats = viewModel.filteredCheats, filteredCheats.isEmpty
|
||||
{
|
||||
Text("Cheat Not Found")
|
||||
.font(.title)
|
||||
|
||||
Text("Please make sure the name is correct, or try searching for another cheat.")
|
||||
.font(.callout)
|
||||
}
|
||||
else if let cheats = viewModel.allCheats, cheats.isEmpty
|
||||
{
|
||||
Text("No Cheats")
|
||||
.font(.title)
|
||||
|
||||
Text("There are no cheats for this game in Delta's CheatBase. Please try a different game.")
|
||||
.font(.callout)
|
||||
}
|
||||
else if viewModel.allCheats == nil
|
||||
{
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
}
|
||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity, alignment: .center)
|
||||
.foregroundColor(.gray)
|
||||
.padding()
|
||||
}
|
||||
|
||||
init(game: Game, cheats: [CheatMetadata]? = nil)
|
||||
{
|
||||
self.game = game
|
||||
|
||||
let viewModel = ViewModel()
|
||||
viewModel.allCheats = cheats
|
||||
self._viewModel = StateObject(wrappedValue: viewModel)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14, *)
|
||||
private extension CheatBaseView
|
||||
{
|
||||
func choose(_ cheatMetadata: CheatMetadata)
|
||||
{
|
||||
self.selectionHandler?(cheatMetadata)
|
||||
}
|
||||
}
|
||||
111
Delta/Database/Cheats/CheatDevice.swift
Normal file
111
Delta/Database/Cheats/CheatDevice.swift
Normal file
@ -0,0 +1,111 @@
|
||||
//
|
||||
// CheatDevice.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 1/30/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import DeltaCore
|
||||
import NESDeltaCore
|
||||
|
||||
@objc
|
||||
enum CheatDevice: Int16
|
||||
{
|
||||
case famicomGameGenie = 1
|
||||
case famicomRaw = 2
|
||||
case famicomRawCompare = 3
|
||||
|
||||
case gbGameGenie = 4
|
||||
|
||||
case gbaActionReplayMax = 5
|
||||
case gbaCodeBreaker = 6
|
||||
case gbaGameShark = 7
|
||||
|
||||
case gbcGameShark = 8
|
||||
|
||||
case n64GameShark = 9
|
||||
|
||||
case dsActionReplay = 10
|
||||
case dsCodeBreaker = 11
|
||||
|
||||
case nesGameGenie = 12
|
||||
case nesRaw = 13
|
||||
case nesRawCompare = 14
|
||||
|
||||
case snesActionReplay = 15
|
||||
case snesGameGenie = 16
|
||||
|
||||
case gameGearActionReplay = 17
|
||||
case gameGearGameGenie = 18
|
||||
|
||||
case masterSystemActionReplay = 19
|
||||
case masterSystemGameGenie = 20
|
||||
|
||||
case cdActionReplay10 = 21
|
||||
case cdActionReplay8 = 22
|
||||
|
||||
case genesisActionReplay10 = 23
|
||||
case genesisActionReplay8 = 24
|
||||
}
|
||||
|
||||
extension CheatDevice
|
||||
{
|
||||
var cheatType: CheatType? {
|
||||
switch self
|
||||
{
|
||||
case .snesActionReplay, .gbaActionReplayMax, .dsActionReplay, .gameGearActionReplay, .masterSystemActionReplay, .genesisActionReplay8, .genesisActionReplay10, .cdActionReplay8, .cdActionReplay10:
|
||||
return .actionReplay
|
||||
|
||||
case .n64GameShark, .gbcGameShark, .gbaGameShark:
|
||||
return .gameShark
|
||||
|
||||
case .famicomGameGenie, .snesGameGenie, .gbGameGenie, .gameGearGameGenie, .masterSystemGameGenie:
|
||||
return .gameGenie
|
||||
|
||||
case .nesGameGenie:
|
||||
return CheatType(rawValue: DeltaCore.CheatType.gameGenie8.rawValue)
|
||||
|
||||
case .gbaCodeBreaker, .dsCodeBreaker:
|
||||
return .codeBreaker
|
||||
|
||||
case .famicomRaw, .famicomRawCompare:
|
||||
return nil
|
||||
|
||||
case .nesRaw, .nesRawCompare:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var gameType: GameType? {
|
||||
switch self
|
||||
{
|
||||
case .famicomGameGenie, .famicomRaw, .famicomRawCompare: return .nes
|
||||
case .nesGameGenie, .nesRaw, .nesRawCompare: return .nes
|
||||
case .snesActionReplay, .snesGameGenie: return .snes
|
||||
case .n64GameShark: return .n64
|
||||
case .gbGameGenie, .gbcGameShark: return .gbc
|
||||
case .gbaActionReplayMax, .gbaGameShark, .gbaCodeBreaker: return .gba
|
||||
case .dsActionReplay, .dsCodeBreaker: return .ds
|
||||
case .genesisActionReplay8, .genesisActionReplay10: return .genesis
|
||||
case .cdActionReplay8, .cdActionReplay10: return .genesis
|
||||
|
||||
// Not yet supported
|
||||
case .gameGearActionReplay, .gameGearGameGenie: return nil
|
||||
case .masterSystemActionReplay, .masterSystemGameGenie: return nil
|
||||
}
|
||||
}
|
||||
|
||||
var cheatFormat: CheatFormat? {
|
||||
guard
|
||||
let cheatType = self.cheatType,
|
||||
let gameType = self.gameType,
|
||||
let deltaCore = Delta.core(for: gameType)
|
||||
else { return nil }
|
||||
|
||||
let cheatFormat = deltaCore.supportedCheatFormats.first { $0.type == cheatType }
|
||||
return cheatFormat
|
||||
}
|
||||
}
|
||||
45
Delta/Database/Cheats/CheatMetadata.swift
Normal file
45
Delta/Database/Cheats/CheatMetadata.swift
Normal file
@ -0,0 +1,45 @@
|
||||
//
|
||||
// CheatMetadata.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 1/17/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import DeltaCore
|
||||
|
||||
struct CheatCategory: Identifiable, Hashable
|
||||
{
|
||||
var id: Int
|
||||
|
||||
var name: String
|
||||
var categoryDescription: String
|
||||
}
|
||||
|
||||
@objcMembers // @objcMembers required for NSPredicate-based filtering.
|
||||
final class CheatMetadata: NSObject, Identifiable
|
||||
{
|
||||
let id: Int
|
||||
|
||||
let name: String
|
||||
let code: String
|
||||
|
||||
let cheatDescription: String?
|
||||
let activationHint: String?
|
||||
|
||||
let device: CheatDevice
|
||||
let category: CheatCategory
|
||||
|
||||
init(id: Int, name: String, code: String, description: String?, activationHint: String?, device: CheatDevice, category: CheatCategory)
|
||||
{
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.code = code
|
||||
self.cheatDescription = description
|
||||
self.activationHint = activationHint
|
||||
self.device = device
|
||||
self.category = category
|
||||
}
|
||||
}
|
||||
51
Delta/Database/Cheats/LegacySearchBar.swift
Normal file
51
Delta/Database/Cheats/LegacySearchBar.swift
Normal file
@ -0,0 +1,51 @@
|
||||
//
|
||||
// LegacySearchBar.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 1/25/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
@available(iOS 13, *)
|
||||
struct LegacySearchBar: UIViewRepresentable
|
||||
{
|
||||
class Coordinator: NSObject, UISearchBarDelegate
|
||||
{
|
||||
@Binding
|
||||
var text: String
|
||||
|
||||
init(text: Binding<String>)
|
||||
{
|
||||
self._text = text
|
||||
}
|
||||
|
||||
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String)
|
||||
{
|
||||
self.text = searchText
|
||||
}
|
||||
}
|
||||
|
||||
@Binding
|
||||
var text: String
|
||||
|
||||
func makeUIView(context: Context) -> UISearchBar
|
||||
{
|
||||
let searchBar = UISearchBar(frame: .zero)
|
||||
searchBar.delegate = context.coordinator
|
||||
searchBar.placeholder = NSLocalizedString("Search", comment: "")
|
||||
return searchBar
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UISearchBar, context: Context)
|
||||
{
|
||||
uiView.text = self.text
|
||||
}
|
||||
|
||||
func makeCoordinator() -> Coordinator
|
||||
{
|
||||
return Coordinator(text: $text)
|
||||
}
|
||||
}
|
||||
@ -11,14 +11,19 @@ import CoreData
|
||||
|
||||
// Workspace
|
||||
import DeltaCore
|
||||
import ZipZap
|
||||
|
||||
// Pods
|
||||
import FileMD5Hash
|
||||
import Harmony
|
||||
import Roxas
|
||||
import ZIPFoundation
|
||||
import MelonDSDeltaCore
|
||||
|
||||
extension DatabaseManager
|
||||
{
|
||||
enum ImportError: Error, Hashable
|
||||
static let didStartNotification = Notification.Name("databaseManagerDidStartNotification")
|
||||
}
|
||||
|
||||
extension DatabaseManager
|
||||
{
|
||||
enum ImportError: LocalizedError, Hashable, Equatable
|
||||
{
|
||||
case doesNotExist(URL)
|
||||
case invalid(URL)
|
||||
@ -26,91 +31,217 @@ extension DatabaseManager
|
||||
case unknown(URL, NSError)
|
||||
case saveFailed(Set<URL>, NSError)
|
||||
|
||||
var hashValue: Int {
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .doesNotExist: return 0
|
||||
case .invalid: return 1
|
||||
case .unsupported: return 2
|
||||
case .unknown: return 3
|
||||
case .saveFailed: return 4
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: ImportError, rhs: ImportError) -> Bool
|
||||
{
|
||||
switch (lhs, rhs)
|
||||
{
|
||||
case (let .doesNotExist(url1), let .doesNotExist(url2)) where url1 == url2: return true
|
||||
case (let .invalid(url1), let .invalid(url2)) where url1 == url2: return true
|
||||
case (let .unsupported(url1), let .unsupported(url2)) where url1 == url2: return true
|
||||
case (let .unknown(url1, error1), let .unknown(url2, error2)) where url1 == url2 && error1 == error2: return true
|
||||
case (let .saveFailed(urls1, error1), let .saveFailed(urls2, error2)) where urls1 == urls2 && error1 == error2: return true
|
||||
case (.doesNotExist, _): return false
|
||||
case (.invalid, _): return false
|
||||
case (.unsupported, _): return false
|
||||
case (.unknown, _): return false
|
||||
case (.saveFailed, _): return false
|
||||
case .doesNotExist: return NSLocalizedString("The file does not exist.", comment: "")
|
||||
case .invalid: return NSLocalizedString("The file is invalid.", comment: "")
|
||||
case .unsupported: return NSLocalizedString("This file is not supported.", comment: "")
|
||||
case .unknown(_, let error): return error.localizedDescription
|
||||
case .saveFailed(_, let error): return error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class DatabaseManager: NSPersistentContainer
|
||||
final class DatabaseManager: RSTPersistentContainer
|
||||
{
|
||||
static let shared = DatabaseManager()
|
||||
|
||||
fileprivate var gamesDatabase: GamesDatabase? = nil
|
||||
private(set) var isStarted = false
|
||||
|
||||
private var gamesDatabase: GamesDatabase? = nil
|
||||
|
||||
private var validationManagedObjectContext: NSManagedObjectContext?
|
||||
|
||||
private let importController = ImportController(documentTypes: [])
|
||||
|
||||
private init()
|
||||
{
|
||||
guard
|
||||
let modelURL = Bundle(for: DatabaseManager.self).url(forResource: "Model", withExtension: "mom"),
|
||||
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL)
|
||||
let modelURL = Bundle(for: DatabaseManager.self).url(forResource: "Delta", withExtension: "momd"),
|
||||
let managedObjectModel = NSManagedObjectModel(contentsOf: modelURL),
|
||||
let harmonyModel = NSManagedObjectModel.harmonyModel(byMergingWith: [managedObjectModel])
|
||||
else { fatalError("Core Data model cannot be found. Aborting.") }
|
||||
|
||||
super.init(name: "Delta", managedObjectModel: harmonyModel)
|
||||
|
||||
super.init(name: "Delta", managedObjectModel: managedObjectModel)
|
||||
|
||||
self.viewContext.automaticallyMergesChangesFromParent = true
|
||||
self.shouldAddStoresAsynchronously = true
|
||||
}
|
||||
}
|
||||
|
||||
extension DatabaseManager
|
||||
{
|
||||
override func newBackgroundContext() -> NSManagedObjectContext
|
||||
func start(completionHandler: @escaping (Error?) -> Void)
|
||||
{
|
||||
let context = super.newBackgroundContext()
|
||||
context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
return context
|
||||
guard !self.isStarted else { return }
|
||||
|
||||
for description in self.persistentStoreDescriptions
|
||||
{
|
||||
// Set configuration so RSTPersistentContainer can determine how to migrate this and Harmony's database independently.
|
||||
description.configuration = NSManagedObjectModel.Configuration.external.rawValue
|
||||
}
|
||||
|
||||
override func loadPersistentStores(completionHandler block: @escaping (NSPersistentStoreDescription, Error?) -> Void)
|
||||
{
|
||||
super.loadPersistentStores { (description, error) in
|
||||
self.loadPersistentStores { (description, error) in
|
||||
guard error == nil else { return completionHandler(error) }
|
||||
|
||||
self.prepareDatabase {
|
||||
block(description, error)
|
||||
self.isStarted = true
|
||||
|
||||
NotificationCenter.default.post(name: DatabaseManager.didStartNotification, object: self)
|
||||
|
||||
completionHandler(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func prepare(_ core: DeltaCoreProtocol, in context: NSManagedObjectContext)
|
||||
{
|
||||
guard let system = System(gameType: core.gameType) else { return }
|
||||
|
||||
if let skin = ControllerSkin(system: system, context: context)
|
||||
{
|
||||
print("Updated default skin (\(skin.identifier)) for system:", system)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Failed to update default skin for system:", system)
|
||||
}
|
||||
|
||||
switch system
|
||||
{
|
||||
case .ds where core == MelonDS.core:
|
||||
|
||||
// Returns nil if game already exists.
|
||||
func makeBIOS(name: String, identifier: String) -> Game?
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), identifier)
|
||||
if let _ = Game.instancesWithPredicate(predicate, inManagedObjectContext: context, type: Game.self).first
|
||||
{
|
||||
// BIOS already exists, so don't do anything.
|
||||
return nil
|
||||
}
|
||||
|
||||
let filename: String
|
||||
|
||||
switch identifier
|
||||
{
|
||||
case Game.melonDSBIOSIdentifier:
|
||||
guard
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios7URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.bios9URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.firmwareURL.path)
|
||||
else { return nil }
|
||||
|
||||
filename = "nds.bios"
|
||||
|
||||
case Game.melonDSDSiBIOSIdentifier:
|
||||
#if BETA
|
||||
|
||||
guard
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS7URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiBIOS9URL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiFirmwareURL.path) &&
|
||||
FileManager.default.fileExists(atPath: MelonDSEmulatorBridge.shared.dsiNANDURL.path)
|
||||
else { return nil }
|
||||
|
||||
filename = "dsi.bios"
|
||||
|
||||
#else
|
||||
return nil
|
||||
#endif
|
||||
|
||||
default: filename = "system.bios"
|
||||
}
|
||||
|
||||
let bios = Game(context: context)
|
||||
bios.name = name
|
||||
bios.identifier = identifier
|
||||
bios.type = .ds
|
||||
bios.filename = filename
|
||||
|
||||
if let artwork = UIImage(named: "DS Home Screen"), let artworkData = artwork.pngData()
|
||||
{
|
||||
do
|
||||
{
|
||||
let destinationURL = DatabaseManager.artworkURL(for: bios)
|
||||
try artworkData.write(to: destinationURL, options: .atomic)
|
||||
bios.artworkURL = destinationURL
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to copy default DS home screen artwork.", error)
|
||||
}
|
||||
}
|
||||
|
||||
return bios
|
||||
}
|
||||
|
||||
let insertedGames = [
|
||||
(name: NSLocalizedString("Home Screen", comment: ""), identifier: Game.melonDSBIOSIdentifier),
|
||||
(name: NSLocalizedString("Home Screen (DSi)", comment: ""), identifier: Game.melonDSDSiBIOSIdentifier)
|
||||
].compactMap(makeBIOS)
|
||||
|
||||
// Break if we didn't create any new Games.
|
||||
guard !insertedGames.isEmpty else { break }
|
||||
|
||||
let gameCollection = GameCollection(context: context)
|
||||
gameCollection.identifier = GameType.ds.rawValue
|
||||
gameCollection.index = Int16(System.ds.year)
|
||||
gameCollection.games.formUnion(insertedGames)
|
||||
|
||||
case .ds:
|
||||
let predicate = NSPredicate(format: "%K IN %@", #keyPath(Game.identifier), [Game.melonDSBIOSIdentifier, Game.melonDSDSiBIOSIdentifier])
|
||||
|
||||
let games = Game.instancesWithPredicate(predicate, inManagedObjectContext: context, type: Game.self)
|
||||
for game in games
|
||||
{
|
||||
context.delete(game)
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Update -
|
||||
private extension DatabaseManager
|
||||
{
|
||||
func updateRecentGameShortcuts()
|
||||
{
|
||||
guard let managedObjectContext = self.validationManagedObjectContext else { return }
|
||||
|
||||
guard Settings.gameShortcutsMode == .recent else { return }
|
||||
|
||||
let fetchRequest = Game.recentlyPlayedFetchRequest
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
|
||||
do
|
||||
{
|
||||
let games = try managedObjectContext.fetch(fetchRequest)
|
||||
Settings.gameShortcuts = games
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Preparation -
|
||||
private extension DatabaseManager
|
||||
{
|
||||
func prepareDatabase(completion: @escaping (Void) -> Void)
|
||||
func prepareDatabase(completion: @escaping () -> Void)
|
||||
{
|
||||
self.validationManagedObjectContext = self.newBackgroundContext()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(DatabaseManager.validateManagedObjectContextSave(with:)), name: .NSManagedObjectContextDidSave, object: nil)
|
||||
|
||||
self.performBackgroundTask { (context) in
|
||||
|
||||
for system in System.supportedSystems
|
||||
for system in System.allCases
|
||||
{
|
||||
guard let deltaControllerSkin = DeltaCore.ControllerSkin.standardControllerSkin(for: system.gameType) else { continue }
|
||||
|
||||
let controllerSkin = ControllerSkin(context: context)
|
||||
controllerSkin.isStandard = true
|
||||
controllerSkin.filename = deltaControllerSkin.fileURL.lastPathComponent
|
||||
|
||||
controllerSkin.configure(with: deltaControllerSkin)
|
||||
self.prepare(system.deltaCore, in: context)
|
||||
}
|
||||
|
||||
do
|
||||
@ -124,10 +255,27 @@ private extension DatabaseManager
|
||||
|
||||
do
|
||||
{
|
||||
if !FileManager.default.fileExists(atPath: DatabaseManager.gamesDatabaseURL.path)
|
||||
if !FileManager.default.fileExists(atPath: DatabaseManager.gamesDatabaseURL.path) || GamesDatabase.version != GamesDatabase.previousVersion
|
||||
{
|
||||
guard let bundleURL = Bundle.main.url(forResource: "openvgdb", withExtension: "sqlite") else { throw GamesDatabase.Error.doesNotExist }
|
||||
try FileManager.default.copyItem(at: bundleURL, to: DatabaseManager.gamesDatabaseURL)
|
||||
try FileManager.default.copyItem(at: bundleURL, to: DatabaseManager.gamesDatabaseURL, shouldReplace: true)
|
||||
}
|
||||
|
||||
if #available(iOS 14, *), !FileManager.default.fileExists(atPath: DatabaseManager.cheatBaseURL.path) || CheatBase.cheatsVersion != CheatBase.previousCheatsVersion
|
||||
{
|
||||
guard let archiveURL = Bundle.main.url(forResource: "cheatbase", withExtension: "zip") else { throw GamesDatabase.Error.doesNotExist }
|
||||
|
||||
let temporaryDirectoryURL = FileManager.default.uniqueTemporaryURL()
|
||||
try FileManager.default.createDirectory(at: temporaryDirectoryURL, withIntermediateDirectories: true)
|
||||
defer {
|
||||
try? FileManager.default.removeItem(at: temporaryDirectoryURL)
|
||||
}
|
||||
|
||||
// Unzip to temporaryDirectoryURL first to ensure we don't accidentally unzip other items into DatabaseManager.cheatBaseURL directory (e.g. __MACOSX directory).
|
||||
try FileManager.default.unzipItem(at: archiveURL, to: temporaryDirectoryURL, skipCRC32: true) // skipCRC32 to avoid ~10 second extraction.
|
||||
|
||||
let extractedDatabaseURL = temporaryDirectoryURL.appendingPathComponent("cheatbase.sqlite")
|
||||
try FileManager.default.copyItem(at: extractedDatabaseURL, to: DatabaseManager.cheatBaseURL, shouldReplace: true)
|
||||
}
|
||||
|
||||
self.gamesDatabase = try GamesDatabase()
|
||||
@ -138,7 +286,6 @@ private extension DatabaseManager
|
||||
}
|
||||
|
||||
completion()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -149,7 +296,20 @@ extension DatabaseManager
|
||||
{
|
||||
func importGames(at urls: Set<URL>, completion: ((Set<Game>, Set<ImportError>) -> Void)?)
|
||||
{
|
||||
var errors = Set<ImportError>()
|
||||
let externalFileURLs = urls.filter { !FileManager.default.isReadableFile(atPath: $0.path) }
|
||||
guard externalFileURLs.isEmpty else {
|
||||
self.importExternalFiles(at: externalFileURLs) { (importedURLs, externalImportErrors) in
|
||||
var availableFileURLs = urls.filter { !externalFileURLs.contains($0) }
|
||||
availableFileURLs.formUnion(importedURLs)
|
||||
|
||||
self.importGames(at: Set(availableFileURLs)) { (importedGames, importErrors) in
|
||||
let allErrors = importErrors.union(externalImportErrors)
|
||||
completion?(importedGames, allErrors)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let zipFileURLs = urls.filter { $0.pathExtension.lowercased() == "zip" }
|
||||
if zipFileURLs.count > 0
|
||||
@ -167,6 +327,7 @@ extension DatabaseManager
|
||||
|
||||
self.performBackgroundTask { (context) in
|
||||
|
||||
var errors = Set<ImportError>()
|
||||
var identifiers = Set<String>()
|
||||
|
||||
for url in urls
|
||||
@ -181,7 +342,23 @@ extension DatabaseManager
|
||||
continue
|
||||
}
|
||||
|
||||
let identifier = FileHash.sha1HashOfFile(atPath: url.path) as String
|
||||
guard System.registeredSystems.contains(system) else {
|
||||
errors.insert(.unsupported(url))
|
||||
continue
|
||||
}
|
||||
|
||||
let identifier: String
|
||||
|
||||
do
|
||||
{
|
||||
identifier = try RSTHasher.sha1HashOfFile(at: url)
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
errors.insert(.unknown(url, error))
|
||||
continue
|
||||
}
|
||||
|
||||
let filename = identifier + "." + url.pathExtension
|
||||
|
||||
let game = Game(context: context)
|
||||
@ -246,10 +423,24 @@ extension DatabaseManager
|
||||
|
||||
func importControllerSkins(at urls: Set<URL>, completion: ((Set<ControllerSkin>, Set<ImportError>) -> Void)?)
|
||||
{
|
||||
var errors = Set<ImportError>()
|
||||
let externalFileURLs = urls.filter { !FileManager.default.isReadableFile(atPath: $0.path) }
|
||||
guard externalFileURLs.isEmpty else {
|
||||
self.importExternalFiles(at: externalFileURLs) { (importedURLs, externalImportErrors) in
|
||||
var availableFileURLs = urls.filter { !externalFileURLs.contains($0) }
|
||||
availableFileURLs.formUnion(importedURLs)
|
||||
|
||||
self.importControllerSkins(at: Set(availableFileURLs)) { (importedSkins, importErrors) in
|
||||
let allErrors = importErrors.union(externalImportErrors)
|
||||
completion?(importedSkins, allErrors)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
self.performBackgroundTask { (context) in
|
||||
|
||||
var errors = Set<ImportError>()
|
||||
var identifiers = Set<String>()
|
||||
|
||||
for url in urls
|
||||
@ -318,7 +509,6 @@ extension DatabaseManager
|
||||
{
|
||||
DispatchQueue.global().async {
|
||||
|
||||
var semaphores = Set<DispatchSemaphore>()
|
||||
var outputURLs = Set<URL>()
|
||||
var errors = Set<ImportError>()
|
||||
|
||||
@ -326,16 +516,19 @@ extension DatabaseManager
|
||||
{
|
||||
var archiveContainsValidGameFile = false
|
||||
|
||||
guard let archive = Archive(url: url, accessMode: .read) else {
|
||||
errors.insert(.invalid(url))
|
||||
continue
|
||||
}
|
||||
|
||||
for entry in archive
|
||||
{
|
||||
do
|
||||
{
|
||||
let archive = try ZZArchive(url: url)
|
||||
|
||||
for entry in archive.entries
|
||||
{
|
||||
// Ensure entry is not in a subdirectory
|
||||
guard !entry.fileName.contains("/") else { continue }
|
||||
guard !entry.path.contains("/") else { continue }
|
||||
|
||||
let fileExtension = (entry.fileName as NSString).pathExtension
|
||||
let fileExtension = (entry.path as NSString).pathExtension
|
||||
|
||||
guard GameType(fileExtension: fileExtension) != nil else { continue }
|
||||
|
||||
@ -344,55 +537,23 @@ extension DatabaseManager
|
||||
// However, if this game file does turn out to be invalid when extracting, we'll return an ImportError.invalid error specific to this game file
|
||||
archiveContainsValidGameFile = true
|
||||
|
||||
// ROMs may potentially be very large, so we extract using file streams and not raw Data
|
||||
let inputStream = try entry.newStream()
|
||||
|
||||
// Must use temporary directory, and not the directory containing zip file, since the latter might be read-only (such as when importing from Safari)
|
||||
let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent(entry.fileName)
|
||||
let outputURL = FileManager.default.temporaryDirectory.appendingPathComponent(entry.path)
|
||||
|
||||
if FileManager.default.fileExists(atPath: outputURL.path)
|
||||
{
|
||||
try FileManager.default.removeItem(at: outputURL)
|
||||
}
|
||||
|
||||
guard let outputStream = OutputStream(url: outputURL, append: false) else { continue }
|
||||
_ = try archive.extract(entry, to: outputURL, skipCRC32: true)
|
||||
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
semaphores.insert(semaphore)
|
||||
|
||||
let outputWriter = InputStreamOutputWriter(inputStream: inputStream, outputStream: outputStream)
|
||||
outputWriter.start { (error) in
|
||||
if let error = error
|
||||
{
|
||||
if FileManager.default.fileExists(atPath: outputURL.path)
|
||||
{
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: outputURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
print(error)
|
||||
|
||||
errors.insert(.invalid(outputURL))
|
||||
}
|
||||
else
|
||||
{
|
||||
outputURLs.insert(outputURL)
|
||||
}
|
||||
|
||||
semaphore.signal()
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
if !archiveContainsValidGameFile
|
||||
{
|
||||
@ -400,11 +561,6 @@ extension DatabaseManager
|
||||
}
|
||||
}
|
||||
|
||||
for semaphore in semaphores
|
||||
{
|
||||
semaphore.wait()
|
||||
}
|
||||
|
||||
for url in urls
|
||||
{
|
||||
if FileManager.default.fileExists(atPath: url.path)
|
||||
@ -423,6 +579,32 @@ extension DatabaseManager
|
||||
completion(outputURLs, errors)
|
||||
}
|
||||
}
|
||||
|
||||
private func importExternalFiles(at urls: Set<URL>, completion: @escaping ((Set<URL>, Set<ImportError>) -> Void))
|
||||
{
|
||||
var outputURLs = Set<URL>()
|
||||
var errors = Set<ImportError>()
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
for url in urls
|
||||
{
|
||||
dispatchGroup.enter()
|
||||
|
||||
self.importController.importExternalFile(at: url) { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): errors.insert(.unknown(url, error as NSError))
|
||||
case .success(let fileURL): outputURLs.insert(fileURL)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
}
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
completion(outputURLs, errors)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - File URLs -
|
||||
@ -454,6 +636,12 @@ extension DatabaseManager
|
||||
return gamesDatabaseURL
|
||||
}
|
||||
|
||||
class var cheatBaseURL: URL
|
||||
{
|
||||
let gamesDatabaseURL = self.defaultDirectoryURL().appendingPathComponent("cheatbase.sqlite")
|
||||
return gamesDatabaseURL
|
||||
}
|
||||
|
||||
class var gamesDirectoryURL: URL
|
||||
{
|
||||
let gamesDirectoryURL = DatabaseManager.defaultDirectoryURL().appendingPathComponent("Games")
|
||||
@ -498,11 +686,33 @@ extension DatabaseManager
|
||||
{
|
||||
let gameURL = game.fileURL
|
||||
|
||||
let artworkURL = gameURL.deletingPathExtension().appendingPathExtension("jpg")
|
||||
let artworkURL = gameURL.deletingPathExtension().appendingPathExtension("png")
|
||||
return artworkURL
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Notifications -
|
||||
private extension DatabaseManager
|
||||
{
|
||||
@objc func validateManagedObjectContextSave(with notification: Notification)
|
||||
{
|
||||
guard (notification.object as? NSManagedObjectContext) != self.validationManagedObjectContext else { return }
|
||||
|
||||
let insertedObjects = (notification.userInfo?[NSInsertedObjectsKey] as? Set<NSManagedObject>) ?? []
|
||||
let updatedObjects = (notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject>) ?? []
|
||||
let deletedObjects = (notification.userInfo?[NSDeletedObjectsKey] as? Set<NSManagedObject>) ?? []
|
||||
|
||||
let allObjects = insertedObjects.union(updatedObjects).union(deletedObjects)
|
||||
|
||||
if allObjects.contains(where: { $0 is Game })
|
||||
{
|
||||
self.validationManagedObjectContext?.perform {
|
||||
self.updateRecentGameShortcuts()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//MARK: - Private -
|
||||
private extension DatabaseManager
|
||||
{
|
||||
|
||||
@ -1,148 +0,0 @@
|
||||
//
|
||||
// InputStreamOutputWriter.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 12/25/16.
|
||||
// Copyright © 2016 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
private let MaximumBufferLength = 4 * 1024 // 4 KB
|
||||
|
||||
class InputStreamOutputWriter: NSObject
|
||||
{
|
||||
let inputStream: InputStream
|
||||
let outputStream: OutputStream
|
||||
|
||||
fileprivate var completion: ((Error?) -> Void)?
|
||||
|
||||
fileprivate var dataBuffer = Data(capacity: MaximumBufferLength * 2)
|
||||
|
||||
init(inputStream: InputStream, outputStream: OutputStream)
|
||||
{
|
||||
self.inputStream = inputStream
|
||||
self.outputStream = outputStream
|
||||
|
||||
super.init()
|
||||
|
||||
self.inputStream.delegate = self
|
||||
self.outputStream.delegate = self
|
||||
}
|
||||
|
||||
func start(with completion: @escaping ((Error?) -> Void))
|
||||
{
|
||||
guard self.completion == nil else { return }
|
||||
|
||||
self.completion = completion
|
||||
|
||||
let writingQueue = DispatchQueue(label: "com.rileytestut.InputStreamOutputWriter.writingQueue", qos: .userInitiated)
|
||||
writingQueue.async {
|
||||
self.inputStream.schedule(in: .current, forMode: .defaultRunLoopMode)
|
||||
self.outputStream.schedule(in: .current, forMode: .defaultRunLoopMode)
|
||||
|
||||
self.outputStream.open()
|
||||
self.inputStream.open()
|
||||
|
||||
RunLoop.current.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension InputStreamOutputWriter
|
||||
{
|
||||
func writeDataBuffer()
|
||||
{
|
||||
while self.outputStream.hasSpaceAvailable && self.dataBuffer.count > 0
|
||||
{
|
||||
self.dataBuffer.withUnsafeMutableBytes { (buffer: UnsafeMutablePointer<UInt8>) -> Void in
|
||||
let writtenBytesCount = self.outputStream.write(buffer, maxLength: self.dataBuffer.count)
|
||||
if writtenBytesCount >= 0
|
||||
{
|
||||
self.dataBuffer.removeSubrange(0 ..< writtenBytesCount)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func finishWriting()
|
||||
{
|
||||
self.inputStream.close()
|
||||
self.outputStream.close()
|
||||
|
||||
self.inputStream.remove(from: .current, forMode: .commonModes)
|
||||
self.outputStream.remove(from: .current, forMode: .commonModes)
|
||||
|
||||
self.completion?(self.inputStream.streamError ?? self.outputStream.streamError)
|
||||
|
||||
CFRunLoopStop(CFRunLoopGetCurrent())
|
||||
}
|
||||
}
|
||||
|
||||
extension InputStreamOutputWriter: StreamDelegate
|
||||
{
|
||||
func stream(_ aStream: Stream, handle eventCode: Stream.Event)
|
||||
{
|
||||
if let inputStream = aStream as? InputStream
|
||||
{
|
||||
self.inputStream(inputStream, handle: eventCode)
|
||||
}
|
||||
else if let outputStream = aStream as? OutputStream
|
||||
{
|
||||
self.outputStream(outputStream, handle: eventCode)
|
||||
}
|
||||
}
|
||||
|
||||
private func inputStream(_ inputStream: InputStream, handle eventCode: Stream.Event)
|
||||
{
|
||||
switch eventCode
|
||||
{
|
||||
case Stream.Event.hasBytesAvailable:
|
||||
|
||||
guard inputStream.streamError == nil else { return }
|
||||
|
||||
while inputStream.hasBytesAvailable
|
||||
{
|
||||
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: MaximumBufferLength)
|
||||
|
||||
let readBytesCount = inputStream.read(buffer, maxLength: MaximumBufferLength)
|
||||
|
||||
guard readBytesCount >= 0 else { break }
|
||||
|
||||
self.dataBuffer.append(buffer, count: readBytesCount)
|
||||
|
||||
buffer.deallocate(capacity: MaximumBufferLength)
|
||||
|
||||
self.writeDataBuffer()
|
||||
}
|
||||
|
||||
case Stream.Event.endEncountered:
|
||||
if self.dataBuffer.count == 0
|
||||
{
|
||||
self.finishWriting()
|
||||
}
|
||||
|
||||
case Stream.Event.errorOccurred: self.finishWriting()
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
|
||||
private func outputStream(_ outputStream: OutputStream, handle eventCode: Stream.Event)
|
||||
{
|
||||
switch eventCode
|
||||
{
|
||||
case Stream.Event.hasSpaceAvailable:
|
||||
self.writeDataBuffer()
|
||||
|
||||
if self.inputStream.streamStatus == .atEnd
|
||||
{
|
||||
self.finishWriting()
|
||||
}
|
||||
|
||||
case Stream.Event.errorOccurred: self.finishWriting()
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
<?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>_XCCurrentVersionName</key>
|
||||
<string>Delta 7.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,138 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13240" systemVersion="16G29" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
|
||||
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
|
||||
<attribute name="code" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="CheatType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
|
||||
<attribute name="filename" attributeType="String" syncable="YES"/>
|
||||
<attribute name="gameType" attributeType="Transformable" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="gameType"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Game" representedClassName="Game" syncable="YES">
|
||||
<attribute name="artworkURL" optional="YES" attributeType="Transformable" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="URL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
|
||||
<relationship name="gameCollections" toMany="YES" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
|
||||
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
|
||||
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollections" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
|
||||
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="Any"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameControllerInputType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameType" attributeType="Transformable" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="gameControllerInputType"/>
|
||||
<constraint value="gameType"/>
|
||||
<constraint value="playerIndex"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="SaveStateType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
|
||||
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="135"/>
|
||||
<element name="Game" positionX="-378" positionY="-54" width="128" height="180"/>
|
||||
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
|
||||
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="105"/>
|
||||
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
|
||||
</elements>
|
||||
</model>
|
||||
@ -0,0 +1,139 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13772" systemVersion="16G1114" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
|
||||
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
|
||||
<attribute name="code" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="CheatType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
|
||||
<attribute name="filename" attributeType="String" syncable="YES"/>
|
||||
<attribute name="gameType" attributeType="Transformable" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="gameType"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Game" representedClassName="Game" syncable="YES">
|
||||
<attribute name="artworkURL" optional="YES" attributeType="Transformable" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="URL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
|
||||
<relationship name="gameCollections" toMany="YES" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
|
||||
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
|
||||
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollections" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
|
||||
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="Any"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameControllerInputType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameType" attributeType="Transformable" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="gameControllerInputType"/>
|
||||
<constraint value="gameType"/>
|
||||
<constraint value="playerIndex"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="SaveStateType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
|
||||
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="135"/>
|
||||
<element name="Game" positionX="-378" positionY="-54" width="128" height="195"/>
|
||||
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
|
||||
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="105"/>
|
||||
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
|
||||
</elements>
|
||||
</model>
|
||||
@ -0,0 +1,155 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14492.1" systemVersion="18G95" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
|
||||
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
|
||||
<attribute name="code" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="CheatType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
|
||||
<attribute name="filename" attributeType="String" syncable="YES"/>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="gameType"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Game" representedClassName="Game" syncable="YES">
|
||||
<attribute name="artworkURL" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="URL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
|
||||
<relationship name="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
|
||||
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
|
||||
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
|
||||
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollection" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
|
||||
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="Any"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameControllerInputType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="gameControllerInputType"/>
|
||||
<constraint value="gameType"/>
|
||||
<constraint value="playerIndex"/>
|
||||
</uniquenessConstraint>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="SaveStateType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
|
||||
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="135"/>
|
||||
<element name="Game" positionX="-378" positionY="-54" width="128" height="210"/>
|
||||
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
|
||||
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
|
||||
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
|
||||
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
|
||||
</elements>
|
||||
</model>
|
||||
@ -0,0 +1,159 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="15702" systemVersion="19C57" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
|
||||
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
|
||||
<attribute name="code" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="CheatType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
|
||||
<attribute name="filename" attributeType="String" syncable="YES"/>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="gameType"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Game" representedClassName="Game" syncable="YES">
|
||||
<attribute name="artworkURL" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="URL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
|
||||
<relationship name="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
|
||||
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
|
||||
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
|
||||
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
|
||||
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
|
||||
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollection" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
|
||||
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="Any"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameControllerInputType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="gameControllerInputType"/>
|
||||
<constraint value="gameType"/>
|
||||
<constraint value="playerIndex"/>
|
||||
</uniquenessConstraint>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="SaveStateType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
|
||||
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="163"/>
|
||||
<element name="Game" positionX="-378" positionY="-54" width="128" height="238"/>
|
||||
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
|
||||
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
|
||||
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
|
||||
<element name="SaveState" positionX="-198" positionY="113" width="128" height="165"/>
|
||||
</elements>
|
||||
</model>
|
||||
@ -0,0 +1,160 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E287" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
|
||||
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
|
||||
<attribute name="code" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="CheatType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
|
||||
<attribute name="filename" attributeType="String" syncable="YES"/>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="gameType"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Game" representedClassName="Game" syncable="YES">
|
||||
<attribute name="artworkURL" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="URL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
|
||||
<relationship name="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
|
||||
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
|
||||
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
|
||||
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
|
||||
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
|
||||
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollection" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
|
||||
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="Any"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameControllerInputType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="gameControllerInputType"/>
|
||||
<constraint value="gameType"/>
|
||||
<constraint value="playerIndex"/>
|
||||
</uniquenessConstraint>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
|
||||
<attribute name="coreIdentifier" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="SaveStateType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Cheat" positionX="-198" positionY="-63" width="128" height="165"/>
|
||||
<element name="ControllerSkin" positionX="-387" positionY="90" width="128" height="163"/>
|
||||
<element name="Game" positionX="-378" positionY="-54" width="128" height="238"/>
|
||||
<element name="GameCollection" positionX="-585" positionY="-27" width="128" height="90"/>
|
||||
<element name="GameControllerInputMapping" positionX="-387" positionY="90" width="128" height="120"/>
|
||||
<element name="GameSave" positionX="-387" positionY="90" width="128" height="90"/>
|
||||
<element name="SaveState" positionX="-198" positionY="113" width="128" height="178"/>
|
||||
</elements>
|
||||
</model>
|
||||
@ -0,0 +1,152 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22158.8" systemVersion="22F66" minimumToolsVersion="Xcode 7.0" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="1.0">
|
||||
<entity name="Cheat" representedClassName="Cheat" syncable="YES">
|
||||
<attribute name="code" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isEnabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="CheatType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="cheats" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ControllerSkin" representedClassName="ControllerSkin" syncable="YES">
|
||||
<attribute name="filename" attributeType="String" syncable="YES"/>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isStandard" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="supportedConfigurations" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="ControllerSkinConfigurations"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="preferredLandscapeSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredLandscapeSkin" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="preferredPortraitSkinByGames" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="preferredPortraitSkin" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="gameType"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Game" representedClassName="Game" syncable="YES">
|
||||
<attribute name="artworkURL" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="URL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playedDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="cheats" toMany="YES" deletionRule="Cascade" destinationEntity="Cheat" inverseName="game" inverseEntity="Cheat" syncable="YES"/>
|
||||
<relationship name="gameCollection" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="GameCollection" inverseName="games" inverseEntity="GameCollection" syncable="YES"/>
|
||||
<relationship name="gameSave" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="GameSave" inverseName="game" inverseEntity="GameSave" syncable="YES"/>
|
||||
<relationship name="preferredLandscapeSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredLandscapeSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
|
||||
<relationship name="preferredPortraitSkin" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ControllerSkin" inverseName="preferredPortraitSkinByGames" inverseEntity="ControllerSkin" syncable="YES"/>
|
||||
<relationship name="previewSaveState" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SaveState" inverseName="previewGame" inverseEntity="SaveState" syncable="YES"/>
|
||||
<relationship name="saveStates" toMany="YES" deletionRule="Cascade" destinationEntity="SaveState" inverseName="game" inverseEntity="SaveState" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameCollection" representedClassName="GameCollection" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="index" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<relationship name="games" toMany="YES" deletionRule="Nullify" destinationEntity="Game" inverseName="gameCollection" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameControllerInputMapping" representedClassName="GameControllerInputMapping" syncable="YES">
|
||||
<attribute name="deltaCoreInputMapping" attributeType="Transformable" valueTransformerName="GameControllerInputMappingTransformer" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="Any"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameControllerInputType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameControllerInputType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="gameType" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="GameType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="playerIndex" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="gameControllerInputType"/>
|
||||
<constraint value="gameType"/>
|
||||
<constraint value="playerIndex"/>
|
||||
</uniquenessConstraint>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="GameSave" representedClassName="GameSave" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="sha1" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="gameSave" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="SaveState" representedClassName="SaveState" versionHashModifier="quick" syncable="YES">
|
||||
<attribute name="coreIdentifier" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="creationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="filename" attributeType="String" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueClassName" value="NSURL"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modifiedDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="type" attributeType="Integer 16" defaultValueString="2" usesScalarValueType="NO" syncable="YES">
|
||||
<userInfo>
|
||||
<entry key="attributeValueScalarType" value="SaveStateType"/>
|
||||
</userInfo>
|
||||
</attribute>
|
||||
<relationship name="game" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="saveStates" inverseEntity="Game" syncable="YES"/>
|
||||
<relationship name="previewGame" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Game" inverseName="previewSaveState" inverseEntity="Game" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
</model>
|
||||
@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
|
||||
import DeltaCore
|
||||
import Harmony
|
||||
|
||||
@objc(Cheat)
|
||||
public class Cheat: _Cheat, CheatProtocol
|
||||
@ -29,3 +30,32 @@ public class Cheat: _Cheat, CheatProtocol
|
||||
self.primitiveModifiedDate = date
|
||||
}
|
||||
}
|
||||
|
||||
extension Cheat: Syncable
|
||||
{
|
||||
public static var syncablePrimaryKey: AnyKeyPath {
|
||||
return \Cheat.identifier
|
||||
}
|
||||
|
||||
public var syncableKeys: Set<AnyKeyPath> {
|
||||
return [\Cheat.code, \Cheat.creationDate, \Cheat.modifiedDate, \Cheat.name, \Cheat.type]
|
||||
}
|
||||
|
||||
public var syncableRelationships: Set<AnyKeyPath> {
|
||||
return [\Cheat.game as AnyKeyPath]
|
||||
}
|
||||
|
||||
public var syncableMetadata: [HarmonyMetadataKey : String] {
|
||||
guard let game = self.game else { return [:] }
|
||||
return [.gameID: game.identifier, .gameName: game.name]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.name
|
||||
}
|
||||
|
||||
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
|
||||
{
|
||||
return .newest
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,42 +9,31 @@
|
||||
import Foundation
|
||||
|
||||
import DeltaCore
|
||||
import Harmony
|
||||
|
||||
extension ControllerSkinConfigurations
|
||||
{
|
||||
init(traits: DeltaCore.ControllerSkin.Traits)
|
||||
init?(traits: DeltaCore.ControllerSkin.Traits)
|
||||
{
|
||||
switch traits.deviceType
|
||||
switch (traits.device, traits.displayType, traits.orientation)
|
||||
{
|
||||
case .iphone:
|
||||
case (.iphone, .standard, .portrait): self = .iphoneStandardPortrait
|
||||
case (.iphone, .standard, .landscape): self = .iphoneStandardLandscape
|
||||
case (.iphone, .edgeToEdge, .portrait): self = .iphoneEdgeToEdgePortrait
|
||||
case (.iphone, .edgeToEdge, .landscape): self = .iphoneEdgeToEdgeLandscape
|
||||
case (.iphone, .splitView, _): return nil
|
||||
|
||||
switch traits.orientation
|
||||
{
|
||||
case .portrait: self = .fullScreenPortrait
|
||||
case .landscape: self = .fullScreenLandscape
|
||||
}
|
||||
|
||||
case .ipad:
|
||||
|
||||
switch traits.displayMode
|
||||
{
|
||||
case .fullScreen:
|
||||
|
||||
switch traits.orientation
|
||||
{
|
||||
case .portrait: self = .fullScreenPortrait
|
||||
case .landscape: self = .fullScreenLandscape
|
||||
}
|
||||
|
||||
case .splitView:
|
||||
|
||||
switch traits.orientation
|
||||
{
|
||||
case .portrait: self = .splitViewPortrait
|
||||
case .landscape: self = .splitViewLandscape
|
||||
}
|
||||
}
|
||||
case (.ipad, .standard, .portrait): self = .ipadStandardPortrait
|
||||
case (.ipad, .standard, .landscape): self = .ipadStandardLandscape
|
||||
case (.ipad, .edgeToEdge, .portrait): self = .ipadEdgeToEdgePortrait
|
||||
case (.ipad, .edgeToEdge, .landscape): self = .ipadEdgeToEdgeLandscape
|
||||
case (.ipad, .splitView, .portrait): self = .ipadSplitViewPortrait
|
||||
case (.ipad, .splitView, .landscape): self = .ipadSplitViewLandscape
|
||||
|
||||
case (.tv, .standard, .portrait): self = .tvStandardPortrait
|
||||
case (.tv, .standard, .landscape): self = .tvStandardLandscape
|
||||
case (.tv, .edgeToEdge, _): return nil
|
||||
case (.tv, .splitView, _): return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -61,7 +50,7 @@ public class ControllerSkin: _ControllerSkin
|
||||
return self.controllerSkin?.isDebugModeEnabled ?? false
|
||||
}
|
||||
|
||||
fileprivate lazy var controllerSkin: DeltaCore.ControllerSkin? = {
|
||||
private lazy var controllerSkin: DeltaCore.ControllerSkin? = {
|
||||
let controllerSkin = self.isStandard ? DeltaCore.ControllerSkin.standardControllerSkin(for: self.gameType) : DeltaCore.ControllerSkin(fileURL: self.fileURL)
|
||||
return controllerSkin
|
||||
}()
|
||||
@ -88,9 +77,9 @@ extension ControllerSkin: ControllerSkinProtocol
|
||||
return self.controllerSkin?.image(for: traits, preferredSize: preferredSize)
|
||||
}
|
||||
|
||||
public func inputs(for traits: DeltaCore.ControllerSkin.Traits, point: CGPoint) -> [Input]?
|
||||
public func thumbstick(for item: DeltaCore.ControllerSkin.Item, traits: DeltaCore.ControllerSkin.Traits, preferredSize: DeltaCore.ControllerSkin.Size) -> (UIImage, CGSize)?
|
||||
{
|
||||
return self.controllerSkin?.inputs(for: traits, point: point)
|
||||
return self.controllerSkin?.thumbstick(for: item, traits: traits, preferredSize: preferredSize)
|
||||
}
|
||||
|
||||
public func items(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Item]?
|
||||
@ -103,13 +92,46 @@ extension ControllerSkin: ControllerSkinProtocol
|
||||
return self.controllerSkin?.isTranslucent(for: traits)
|
||||
}
|
||||
|
||||
public func gameScreenFrame(for traits: DeltaCore.ControllerSkin.Traits) -> CGRect?
|
||||
public func screens(for traits: DeltaCore.ControllerSkin.Traits) -> [DeltaCore.ControllerSkin.Screen]?
|
||||
{
|
||||
return self.controllerSkin?.gameScreenFrame(for: traits)
|
||||
return self.controllerSkin?.screens(for: traits)
|
||||
}
|
||||
|
||||
public func aspectRatio(for traits: DeltaCore.ControllerSkin.Traits) -> CGSize?
|
||||
{
|
||||
return self.controllerSkin?.aspectRatio(for: traits)
|
||||
}
|
||||
|
||||
public func contentSize(for traits: DeltaCore.ControllerSkin.Traits) -> CGSize?
|
||||
{
|
||||
return self.controllerSkin?.contentSize(for: traits)
|
||||
}
|
||||
}
|
||||
|
||||
extension ControllerSkin: Syncable
|
||||
{
|
||||
public static var syncablePrimaryKey: AnyKeyPath {
|
||||
return \ControllerSkin.identifier
|
||||
}
|
||||
|
||||
public var syncableKeys: Set<AnyKeyPath> {
|
||||
return [\ControllerSkin.filename, \ControllerSkin.gameType, \ControllerSkin.name, \ControllerSkin.supportedConfigurations]
|
||||
}
|
||||
|
||||
public var syncableFiles: Set<File> {
|
||||
return [File(identifier: "skin", fileURL: self.fileURL)]
|
||||
}
|
||||
|
||||
public var isSyncingEnabled: Bool {
|
||||
return !self.isStandard
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.name
|
||||
}
|
||||
|
||||
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
|
||||
{
|
||||
return .newest
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,13 +9,22 @@
|
||||
import Foundation
|
||||
|
||||
import DeltaCore
|
||||
import MelonDSDeltaCore
|
||||
|
||||
import Harmony
|
||||
|
||||
public extension Game
|
||||
{
|
||||
static let melonDSBIOSIdentifier = "com.rileytestut.MelonDSDeltaCore.BIOS"
|
||||
static let melonDSDSiBIOSIdentifier = "com.rileytestut.MelonDSDeltaCore.DSiBIOS"
|
||||
}
|
||||
|
||||
@objc(Game)
|
||||
public class Game: _Game, GameProtocol
|
||||
{
|
||||
public var fileURL: URL {
|
||||
var fileURL: URL!
|
||||
|
||||
//通过将指定的路径组件附加到 self 来返回 URL
|
||||
self.managedObjectContext?.performAndWait {
|
||||
fileURL = DatabaseManager.gamesDirectoryURL.appendingPathComponent(self.filename)
|
||||
}
|
||||
@ -29,11 +38,29 @@ public class Game: _Game, GameProtocol
|
||||
var artworkURL = self.primitiveValue(forKey: #keyPath(Game.artworkURL)) as? URL
|
||||
self.didAccessValue(forKey: #keyPath(Game.artworkURL))
|
||||
|
||||
if let unwrappedArtworkURL = artworkURL, unwrappedArtworkURL.isFileURL
|
||||
if let unwrappedArtworkURL = artworkURL
|
||||
{
|
||||
if unwrappedArtworkURL.isFileURL
|
||||
{
|
||||
// Recreate the stored URL relative to current sandbox location.
|
||||
artworkURL = URL(fileURLWithPath: unwrappedArtworkURL.relativePath, relativeTo: DatabaseManager.gamesDirectoryURL)
|
||||
}
|
||||
else if let host = unwrappedArtworkURL.host?.lowercased(), host == "img.gamefaqs.net" || host == "gamefaqs1.cbsistatic.com",
|
||||
var components = URLComponents(url: unwrappedArtworkURL, resolvingAgainstBaseURL: false)
|
||||
{
|
||||
// Quick fix for broken album artwork URLs due to host change.
|
||||
components.host = "gamefaqs.gamespot.com"
|
||||
components.scheme = "https"
|
||||
|
||||
let updatedPath = "/a" + components.path
|
||||
components.path = updatedPath
|
||||
|
||||
if let url = components.url
|
||||
{
|
||||
artworkURL = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return artworkURL
|
||||
}
|
||||
@ -55,6 +82,18 @@ public class Game: _Game, GameProtocol
|
||||
}
|
||||
}
|
||||
|
||||
extension Game
|
||||
{
|
||||
class var recentlyPlayedFetchRequest: NSFetchRequest<Game> {
|
||||
let fetchRequest: NSFetchRequest<Game> = Game.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "%K != nil", #keyPath(Game.playedDate))
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Game.playedDate, ascending: false), NSSortDescriptor(keyPath: \Game.name, ascending: true)]
|
||||
fetchRequest.fetchLimit = 4
|
||||
|
||||
return fetchRequest
|
||||
}
|
||||
}
|
||||
|
||||
extension Game
|
||||
{
|
||||
override public func prepareForDeletion()
|
||||
@ -63,11 +102,17 @@ extension Game
|
||||
|
||||
guard let managedObjectContext = self.managedObjectContext else { return }
|
||||
|
||||
// If filename == empty string (e.g. during merge), ignore this deletion.
|
||||
// Otherwise, we may accidentally delete the entire Games directory!
|
||||
guard !self.filename.isEmpty else { return }
|
||||
|
||||
// If a game with the same identifier is also currently being inserted, Core Data is more than likely resolving a conflict by deleting the previous instance
|
||||
// In this case, we make sure we DON'T delete the game file + misc other Core Data relationships, or else we'll just lose all that data
|
||||
guard !managedObjectContext.insertedObjects.contains(where: { ($0 as? Game)?.identifier == self.identifier }) else { return }
|
||||
|
||||
guard FileManager.default.fileExists(atPath: self.fileURL.path) else { return }
|
||||
// Double-check fileURL is NOT actually a directory, which we should never delete.
|
||||
var isDirectory: ObjCBool = false
|
||||
guard FileManager.default.fileExists(atPath: self.fileURL.path, isDirectory: &isDirectory), !isDirectory.boolValue else { return }
|
||||
|
||||
do
|
||||
{
|
||||
@ -78,7 +123,7 @@ extension Game
|
||||
print(error)
|
||||
}
|
||||
|
||||
for collection in self.gameCollections where collection.games.count == 1
|
||||
if let collection = self.gameCollection, collection.games.count == 1
|
||||
{
|
||||
// Once this game is deleted, collection will have 0 games, so we should delete it
|
||||
managedObjectContext.delete(collection)
|
||||
@ -97,3 +142,71 @@ extension Game
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Game: Syncable
|
||||
{
|
||||
public static var syncablePrimaryKey: AnyKeyPath {
|
||||
return \Game.identifier
|
||||
}
|
||||
|
||||
public var syncableKeys: Set<AnyKeyPath> {
|
||||
return [\Game.artworkURL, \Game.filename, \Game.name, \Game.type]
|
||||
}
|
||||
|
||||
public var syncableFiles: Set<File> {
|
||||
let artworkURL: URL
|
||||
|
||||
if let fileURL = self.artworkURL, fileURL.isFileURL
|
||||
{
|
||||
artworkURL = fileURL
|
||||
}
|
||||
else
|
||||
{
|
||||
artworkURL = DatabaseManager.artworkURL(for: self)
|
||||
}
|
||||
|
||||
let artworkFile = File(identifier: "artwork", fileURL: artworkURL)
|
||||
|
||||
switch self.identifier
|
||||
{
|
||||
case Game.melonDSBIOSIdentifier:
|
||||
let bios7File = File(identifier: "bios7", fileURL: MelonDSEmulatorBridge.shared.bios7URL)
|
||||
let bios9File = File(identifier: "bios9", fileURL: MelonDSEmulatorBridge.shared.bios9URL)
|
||||
let firmwareFile = File(identifier: "firmware", fileURL: MelonDSEmulatorBridge.shared.firmwareURL)
|
||||
|
||||
return [artworkFile, bios7File, bios9File, firmwareFile]
|
||||
|
||||
case Game.melonDSDSiBIOSIdentifier:
|
||||
let bios7File = File(identifier: "bios7", fileURL: MelonDSEmulatorBridge.shared.dsiBIOS7URL)
|
||||
let bios9File = File(identifier: "bios9", fileURL: MelonDSEmulatorBridge.shared.dsiBIOS9URL)
|
||||
let firmwareFile = File(identifier: "firmware", fileURL: MelonDSEmulatorBridge.shared.dsiFirmwareURL)
|
||||
|
||||
// DSi NAND is ~240MB, so don't sync for now until Harmony can selectively download files.
|
||||
// let nandFile = File(identifier: "nand", fileURL: MelonDSEmulatorBridge.shared.dsiNANDURL)
|
||||
|
||||
return [artworkFile, bios7File, bios9File, firmwareFile]
|
||||
|
||||
default:
|
||||
let gameFile = File(identifier: "game", fileURL: self.fileURL)
|
||||
return [artworkFile, gameFile]
|
||||
}
|
||||
}
|
||||
|
||||
public var syncableRelationships: Set<AnyKeyPath> {
|
||||
return [\Game.gameCollection]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.name
|
||||
}
|
||||
|
||||
public func awakeFromSync(_ record: AnyRecord) throws
|
||||
{
|
||||
guard let gameCollection = self.gameCollection else { throw SyncValidationError.incorrectGameCollection(nil) }
|
||||
|
||||
if gameCollection.identifier != self.type.rawValue
|
||||
{
|
||||
throw SyncValidationError.incorrectGameCollection(gameCollection.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,15 +9,16 @@
|
||||
import CoreData
|
||||
|
||||
import DeltaCore
|
||||
import Harmony
|
||||
|
||||
@objc(GameCollection)
|
||||
public class GameCollection: _GameCollection
|
||||
{
|
||||
var name: String {
|
||||
@objc var name: String {
|
||||
return self.system?.localizedName ?? NSLocalizedString("Unknown", comment: "")
|
||||
}
|
||||
|
||||
var shortName: String {
|
||||
@objc var shortName: String {
|
||||
return self.system?.localizedShortName ?? NSLocalizedString("Unknown", comment: "")
|
||||
}
|
||||
|
||||
@ -28,3 +29,23 @@ public class GameCollection: _GameCollection
|
||||
return system
|
||||
}
|
||||
}
|
||||
|
||||
extension GameCollection: Syncable
|
||||
{
|
||||
public static var syncablePrimaryKey: AnyKeyPath {
|
||||
return \GameCollection.identifier
|
||||
}
|
||||
|
||||
public var syncableKeys: Set<AnyKeyPath> {
|
||||
return [\GameCollection.index as AnyKeyPath]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.name
|
||||
}
|
||||
|
||||
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
|
||||
{
|
||||
return .newest
|
||||
}
|
||||
}
|
||||
|
||||
108
Delta/Database/Model/Human/GameControllerInputMapping.swift
Normal file
108
Delta/Database/Model/Human/GameControllerInputMapping.swift
Normal file
@ -0,0 +1,108 @@
|
||||
//
|
||||
// GameControllerInputMapping.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 8/30/16.
|
||||
// Copyright (c) 2016 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import DeltaCore
|
||||
import Harmony
|
||||
|
||||
@objc(GameControllerInputMapping)
|
||||
public class GameControllerInputMapping: _GameControllerInputMapping
|
||||
{
|
||||
private var inputMapping: DeltaCore.GameControllerInputMapping {
|
||||
get { return self.deltaCoreInputMapping as! DeltaCore.GameControllerInputMapping }
|
||||
set { self.deltaCoreInputMapping = newValue }
|
||||
}
|
||||
|
||||
public convenience init(inputMapping: DeltaCore.GameControllerInputMapping, context: NSManagedObjectContext)
|
||||
{
|
||||
self.init(entity: GameControllerInputMapping.entity(), insertInto: context)
|
||||
|
||||
self.inputMapping = inputMapping
|
||||
}
|
||||
|
||||
public override func awakeFromInsert()
|
||||
{
|
||||
super.awakeFromInsert()
|
||||
|
||||
self.identifier = UUID().uuidString
|
||||
}
|
||||
}
|
||||
|
||||
extension GameControllerInputMapping
|
||||
{
|
||||
class func inputMapping(for gameController: GameController, gameType: GameType, in managedObjectContext: NSManagedObjectContext) -> GameControllerInputMapping?
|
||||
{
|
||||
guard let playerIndex = gameController.playerIndex else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let fetchRequest: NSFetchRequest<GameControllerInputMapping> = GameControllerInputMapping.fetchRequest()
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@ AND %K == %d", #keyPath(GameControllerInputMapping.gameControllerInputType), gameController.inputType.rawValue, #keyPath(GameControllerInputMapping.gameType), gameType.rawValue, #keyPath(GameControllerInputMapping.playerIndex), playerIndex)
|
||||
|
||||
do
|
||||
{
|
||||
let inputMappings = try managedObjectContext.fetch(fetchRequest)
|
||||
|
||||
let inputMapping = inputMappings.first(where: { !$0.isDeleted })
|
||||
return inputMapping
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension GameControllerInputMapping: GameControllerInputMappingProtocol
|
||||
{
|
||||
var name: String? {
|
||||
get { return self.inputMapping.name }
|
||||
set { self.inputMapping.name = newValue }
|
||||
}
|
||||
|
||||
var supportedControllerInputs: [Input] {
|
||||
return self.inputMapping.supportedControllerInputs
|
||||
}
|
||||
|
||||
public func input(forControllerInput controllerInput: Input) -> Input?
|
||||
{
|
||||
return self.inputMapping.input(forControllerInput: controllerInput)
|
||||
}
|
||||
|
||||
func set(_ input: Input?, forControllerInput controllerInput: Input)
|
||||
{
|
||||
self.inputMapping.set(input, forControllerInput: controllerInput)
|
||||
}
|
||||
}
|
||||
|
||||
extension GameControllerInputMapping: Syncable
|
||||
{
|
||||
public static var syncablePrimaryKey: AnyKeyPath {
|
||||
return \GameControllerInputMapping.identifier
|
||||
}
|
||||
|
||||
public var syncableKeys: Set<AnyKeyPath> {
|
||||
return [\GameControllerInputMapping.deltaCoreInputMapping,
|
||||
\GameControllerInputMapping.gameControllerInputType,
|
||||
\GameControllerInputMapping.gameType,
|
||||
\GameControllerInputMapping.playerIndex]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.name
|
||||
}
|
||||
|
||||
public func resolveConflict(_ record: AnyRecord) -> ConflictResolution
|
||||
{
|
||||
return .newest
|
||||
}
|
||||
}
|
||||
116
Delta/Database/Model/Human/GameSave.swift
Normal file
116
Delta/Database/Model/Human/GameSave.swift
Normal file
@ -0,0 +1,116 @@
|
||||
//
|
||||
// GameSave.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 8/30/16.
|
||||
// Copyright (c) 2016 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import GBCDeltaCore
|
||||
|
||||
import Harmony
|
||||
|
||||
@objc(GameSave)
|
||||
public class GameSave: _GameSave
|
||||
{
|
||||
public override func awakeFromInsert()
|
||||
{
|
||||
super.awakeFromInsert()
|
||||
|
||||
self.modifiedDate = Date()
|
||||
}
|
||||
}
|
||||
|
||||
extension GameSave: Syncable
|
||||
{
|
||||
public static var syncablePrimaryKey: AnyKeyPath {
|
||||
return \GameSave.identifier
|
||||
}
|
||||
|
||||
public var syncableKeys: Set<AnyKeyPath> {
|
||||
return [\GameSave.modifiedDate, \GameSave.sha1]
|
||||
}
|
||||
|
||||
public var syncableRelationships: Set<AnyKeyPath> {
|
||||
return [\GameSave.game]
|
||||
}
|
||||
|
||||
public var syncableFiles: Set<File> {
|
||||
guard let game = self.game else { return [] }
|
||||
|
||||
var files: Set<File> = [File(identifier: "gameSave", fileURL: game.gameSaveURL)]
|
||||
|
||||
if game.type == .gbc
|
||||
{
|
||||
let gameTimeSaveURL = game.gameSaveURL.deletingPathExtension().appendingPathExtension("rtc")
|
||||
files.insert(File(identifier: "gameTimeSave", fileURL: gameTimeSaveURL))
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
public var syncableMetadata: [HarmonyMetadataKey : String] {
|
||||
guard let game = self.game else { return [:] }
|
||||
|
||||
// Use self.identifier to always link with exact matching game.
|
||||
return [.gameID: self.identifier, .gameName: game.name]
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.game?.name
|
||||
}
|
||||
|
||||
public var isSyncingEnabled: Bool {
|
||||
// self.game may be nil if being downloaded, so don't enforce it.
|
||||
// guard let identifier = self.game?.identifier else { return false }
|
||||
|
||||
return self.game?.identifier != Game.melonDSBIOSIdentifier && self.game?.identifier != Game.melonDSDSiBIOSIdentifier
|
||||
}
|
||||
|
||||
public func awakeFromSync(_ record: AnyRecord) throws
|
||||
{
|
||||
do
|
||||
{
|
||||
guard let game = self.game else { throw SyncValidationError.incorrectGame(nil) }
|
||||
|
||||
if game.identifier != self.identifier
|
||||
{
|
||||
let fetchRequest = GameSave.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(GameSave.identifier), game.identifier)
|
||||
|
||||
if let misplacedGameSave = try self.managedObjectContext?.fetch(fetchRequest).first, misplacedGameSave.game == nil
|
||||
{
|
||||
// Relink game with its correct gameSave, in case we accidentally misplaced it.
|
||||
// Otherwise, corrupted records might displace already-downloaded GameSaves
|
||||
// due to automatic Core Data relationship propagation, despite us throwing error.
|
||||
game.gameSave = misplacedGameSave
|
||||
}
|
||||
else
|
||||
{
|
||||
// Either there is no misplacedGameSave, or there is but it's linked to another game somehow.
|
||||
game.gameSave = nil
|
||||
}
|
||||
|
||||
throw SyncValidationError.incorrectGame(game.name)
|
||||
}
|
||||
}
|
||||
catch let error as SyncValidationError
|
||||
{
|
||||
guard SyncManager.shared.ignoredCorruptedRecordIDs.contains(record.recordID) else { throw error }
|
||||
|
||||
let fetchRequest = Game.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), self.identifier)
|
||||
|
||||
if let correctGame = try self.managedObjectContext?.fetch(fetchRequest).first
|
||||
{
|
||||
self.game = correctGame
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ValidationError.nilRelationshipObjects(keys: [#keyPath(GameSave.game)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,10 +9,14 @@
|
||||
import Foundation
|
||||
|
||||
import DeltaCore
|
||||
import Harmony
|
||||
|
||||
import struct DSDeltaCore.DS
|
||||
|
||||
@objc public enum SaveStateType: Int16
|
||||
{
|
||||
case auto
|
||||
case quick
|
||||
case general
|
||||
case locked
|
||||
}
|
||||
@ -20,6 +24,13 @@ import DeltaCore
|
||||
@objc(SaveState)
|
||||
public class SaveState: _SaveState, SaveStateProtocol
|
||||
{
|
||||
public static let localizedDateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.timeStyle = .short
|
||||
dateFormatter.dateStyle = .short
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
public var fileURL: URL {
|
||||
let fileURL = DatabaseManager.saveStatesDirectoryURL(for: self.game!).appendingPathComponent(self.filename)
|
||||
return fileURL
|
||||
@ -35,6 +46,11 @@ public class SaveState: _SaveState, SaveStateProtocol
|
||||
return self.game!.type
|
||||
}
|
||||
|
||||
public var localizedName: String {
|
||||
let localizedName = self.name ?? SaveState.localizedDateFormatter.string(from: self.modifiedDate)
|
||||
return localizedName
|
||||
}
|
||||
|
||||
@NSManaged private var primitiveFilename: String
|
||||
@NSManaged private var primitiveIdentifier: String
|
||||
@NSManaged private var primitiveCreationDate: Date
|
||||
@ -78,4 +94,101 @@ public class SaveState: _SaveState, SaveStateProtocol
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
class func fetchRequest(for game: Game, type: SaveStateType) -> NSFetchRequest<SaveState>
|
||||
{
|
||||
let predicate = NSPredicate(format: "%K == %@ AND %K == %d", #keyPath(SaveState.game), game, #keyPath(SaveState.type), type.rawValue)
|
||||
|
||||
let fetchRequest: NSFetchRequest<SaveState> = SaveState.fetchRequest()
|
||||
fetchRequest.predicate = predicate
|
||||
|
||||
return fetchRequest
|
||||
}
|
||||
}
|
||||
|
||||
extension SaveState: Syncable
|
||||
{
|
||||
public static var syncablePrimaryKey: AnyKeyPath {
|
||||
return \SaveState.identifier
|
||||
}
|
||||
|
||||
public var syncableKeys: Set<AnyKeyPath> {
|
||||
return [\SaveState.creationDate, \SaveState.filename, \SaveState.modifiedDate, \SaveState.name, \SaveState.type, \SaveState.coreIdentifier]
|
||||
}
|
||||
|
||||
public var syncableFiles: Set<File> {
|
||||
return [File(identifier: "saveState", fileURL: self.fileURL), File(identifier: "thumbnail", fileURL: self.imageFileURL)]
|
||||
}
|
||||
|
||||
public var syncableRelationships: Set<AnyKeyPath> {
|
||||
return [\SaveState.game]
|
||||
}
|
||||
|
||||
public var isSyncingEnabled: Bool {
|
||||
// self.game may be nil if being downloaded, so don't enforce it.
|
||||
// guard let identifier = self.game?.identifier else { return false }
|
||||
|
||||
let isSyncingEnabled = (self.type != .auto && self.type != .quick) && (self.game?.identifier != Game.melonDSBIOSIdentifier && self.game?.identifier != Game.melonDSDSiBIOSIdentifier)
|
||||
return isSyncingEnabled
|
||||
}
|
||||
|
||||
public var syncableMetadata: [HarmonyMetadataKey : String] {
|
||||
guard let game = self.game else { return [:] }
|
||||
return [.gameID: game.identifier, .gameName: game.name, .coreID: self.coreIdentifier, .verifiedGameID: game.identifier].compactMapValues { $0 }
|
||||
}
|
||||
|
||||
public var syncableLocalizedName: String? {
|
||||
return self.localizedName
|
||||
}
|
||||
|
||||
public func awakeFromSync(_ record: AnyRecord) throws
|
||||
{
|
||||
let verifiedGameID = record.remoteMetadata?[.verifiedGameID]
|
||||
|
||||
do
|
||||
{
|
||||
guard let game = self.game else { return }
|
||||
|
||||
if let system = System(gameType: game.type), self.coreIdentifier == nil
|
||||
{
|
||||
if let coreIdentifier = record.remoteMetadata?[.coreID]
|
||||
{
|
||||
// SaveState was synced to older version of Delta and lost its coreIdentifier,
|
||||
// but it remains in the remote metadata so we can reassign it.
|
||||
self.coreIdentifier = coreIdentifier
|
||||
}
|
||||
else
|
||||
{
|
||||
switch system
|
||||
{
|
||||
case .ds: self.coreIdentifier = DS.core.identifier // Assume DS save state with nil coreIdentifier is from DeSmuME core.
|
||||
default: self.coreIdentifier = system.deltaCore.identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let verifiedGameID, verifiedGameID != game.identifier
|
||||
{
|
||||
// Game does not match verified game ID, which most likely means
|
||||
// this SaveState was reviewed + fixed on another device, but not uploaded.
|
||||
throw SyncValidationError.incorrectGame(game.name)
|
||||
}
|
||||
}
|
||||
catch let error as SyncValidationError
|
||||
{
|
||||
guard let verifiedGameID, SyncManager.shared.ignoredCorruptedRecordIDs.contains(record.recordID) else { throw error }
|
||||
|
||||
let fetchRequest = Game.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), verifiedGameID)
|
||||
|
||||
if let correctGame = try self.managedObjectContext?.fetch(fetchRequest).first
|
||||
{
|
||||
self.game = correctGame
|
||||
}
|
||||
else
|
||||
{
|
||||
throw ValidationError.nilRelationshipObjects(keys: [#keyPath(GameSave.game)])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -24,7 +24,7 @@ public class _Cheat: NSManagedObject
|
||||
|
||||
@NSManaged public var modifiedDate: Date
|
||||
|
||||
@NSManaged public var name: String?
|
||||
@NSManaged public var name: String
|
||||
|
||||
@NSManaged public var type: CheatType
|
||||
|
||||
|
||||
@ -28,5 +28,9 @@ public class _ControllerSkin: NSManagedObject
|
||||
|
||||
// MARK: - Relationships
|
||||
|
||||
@NSManaged public var preferredLandscapeSkinByGames: Set<Game>
|
||||
|
||||
@NSManaged public var preferredPortraitSkinByGames: Set<Game>
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -22,13 +22,21 @@ public class _Game: NSManagedObject
|
||||
|
||||
@NSManaged public var name: String
|
||||
|
||||
@NSManaged public var playedDate: Date?
|
||||
|
||||
@NSManaged public var type: GameType
|
||||
|
||||
// MARK: - Relationships
|
||||
|
||||
@NSManaged public var cheats: Set<Cheat>
|
||||
|
||||
@NSManaged public var gameCollections: Set<GameCollection>
|
||||
@NSManaged public var gameCollection: GameCollection?
|
||||
|
||||
@NSManaged public var gameSave: GameSave?
|
||||
|
||||
@NSManaged public var preferredLandscapeSkin: ControllerSkin?
|
||||
|
||||
@NSManaged public var preferredPortraitSkin: ControllerSkin?
|
||||
|
||||
@NSManaged public var previewSaveState: SaveState?
|
||||
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
// DO NOT EDIT. This file is machine-generated and constantly overwritten.
|
||||
// Make changes to GameControllerInputMapping.swift instead.
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import DeltaCore
|
||||
|
||||
public class _GameControllerInputMapping: NSManagedObject
|
||||
{
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<GameControllerInputMapping> {
|
||||
return NSFetchRequest<GameControllerInputMapping>(entityName: "GameControllerInputMapping")
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@NSManaged public var deltaCoreInputMapping: Any
|
||||
|
||||
@NSManaged public var gameControllerInputType: GameControllerInputType
|
||||
|
||||
@NSManaged public var gameType: GameType
|
||||
|
||||
@NSManaged public var identifier: String
|
||||
|
||||
@NSManaged public var playerIndex: Int16
|
||||
|
||||
// MARK: - Relationships
|
||||
|
||||
}
|
||||
|
||||
28
Delta/Database/Model/Machine/_GameSave.swift
Normal file
28
Delta/Database/Model/Machine/_GameSave.swift
Normal file
@ -0,0 +1,28 @@
|
||||
// DO NOT EDIT. This file is machine-generated and constantly overwritten.
|
||||
// Make changes to GameSave.swift instead.
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import DeltaCore
|
||||
|
||||
public class _GameSave: NSManagedObject
|
||||
{
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<GameSave> {
|
||||
return NSFetchRequest<GameSave>(entityName: "GameSave")
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@NSManaged public var identifier: String
|
||||
|
||||
@NSManaged public var modifiedDate: Date
|
||||
|
||||
@NSManaged public var sha1: String?
|
||||
|
||||
// MARK: - Relationships
|
||||
|
||||
@NSManaged public var game: Game?
|
||||
|
||||
}
|
||||
|
||||
@ -14,6 +14,8 @@ public class _SaveState: NSManagedObject
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
@NSManaged public var coreIdentifier: String?
|
||||
|
||||
@NSManaged public var creationDate: Date
|
||||
|
||||
@NSManaged public var filename: String
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,19 @@
|
||||
//
|
||||
// GameControllerInputMappingMigrationPolicy.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 1/30/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
@objc(GameControllerInputMappingMigrationPolicy)
|
||||
class GameControllerInputMappingMigrationPolicy: NSEntityMigrationPolicy
|
||||
{
|
||||
@objc(migrateIdentifier)
|
||||
func migrateIdentifier() -> String
|
||||
{
|
||||
return UUID().uuidString
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
//
|
||||
// SaveStateMigrationPolicy.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 9/28/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import DeltaCore
|
||||
|
||||
import struct DSDeltaCore.DS
|
||||
|
||||
@objc(SaveStateToSaveStateMigrationPolicy)
|
||||
class SaveStateToSaveStateMigrationPolicy: NSEntityMigrationPolicy
|
||||
{
|
||||
@objc(migrateSaveStateType:)
|
||||
func migrateSaveStateType(_ rawValue: NSNumber) -> NSNumber
|
||||
{
|
||||
switch rawValue.intValue
|
||||
{
|
||||
case 0: return NSNumber(value: SaveStateType.auto.rawValue)
|
||||
case 1: return NSNumber(value: SaveStateType.general.rawValue)
|
||||
case 2: return NSNumber(value: SaveStateType.locked.rawValue)
|
||||
default: return rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delta5 to Delta6
|
||||
extension SaveStateToSaveStateMigrationPolicy
|
||||
{
|
||||
@objc(defaultCoreIdentifierForGameType:)
|
||||
func defaultCoreIdentifier(for gameType: GameType) -> String?
|
||||
{
|
||||
guard let system = System(gameType: gameType) else { return nil }
|
||||
|
||||
switch system
|
||||
{
|
||||
case .ds: return DS.core.identifier // Assume any existing save state is from DeSmuME.
|
||||
default: return system.deltaCore.identifier
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -9,13 +9,35 @@
|
||||
#ifndef ControllerSkinConfigurations_h
|
||||
#define ControllerSkinConfigurations_h
|
||||
|
||||
// Every possible (supported) combination of traits.
|
||||
typedef NS_OPTIONS(int16_t, ControllerSkinConfigurations)
|
||||
{
|
||||
ControllerSkinConfigurationFullScreenPortrait = 1 << 0,
|
||||
ControllerSkinConfigurationFullScreenLandscape = 1 << 1,
|
||||
/* iPhone */
|
||||
ControllerSkinConfigurationiPhoneStandardPortrait NS_SWIFT_NAME(iphoneStandardPortrait) = 1 << 0,
|
||||
ControllerSkinConfigurationiPhoneStandardLandscape NS_SWIFT_NAME(iphoneStandardLandscape) = 1 << 1,
|
||||
|
||||
ControllerSkinConfigurationSplitViewPortrait = 1 << 2,
|
||||
ControllerSkinConfigurationSplitViewLandscape = 1 << 3,
|
||||
// iPhone doesn't support Split View
|
||||
// ControllerSkinConfigurationiPhoneSplitViewPortrait = 1 << 2,
|
||||
// ControllerSkinConfigurationiPhoneSplitViewLandscape = 1 << 3,
|
||||
|
||||
ControllerSkinConfigurationiPhoneEdgeToEdgePortrait NS_SWIFT_NAME(iphoneEdgeToEdgePortrait) = 1 << 4,
|
||||
ControllerSkinConfigurationiPhoneEdgeToEdgeLandscape NS_SWIFT_NAME(iphoneEdgeToEdgeLandscape) = 1 << 5,
|
||||
|
||||
|
||||
/* iPad */
|
||||
ControllerSkinConfigurationiPadStandardPortrait NS_SWIFT_NAME(ipadStandardPortrait) = 1 << 6,
|
||||
ControllerSkinConfigurationiPadStandardLandscape NS_SWIFT_NAME(ipadStandardLandscape) = 1 << 7,
|
||||
|
||||
ControllerSkinConfigurationiPadSplitViewPortrait NS_SWIFT_NAME(ipadSplitViewPortrait) = 1 << 2, // Backwards compatible with legacy ControllerSkinConfigurationSplitViewPortrait
|
||||
ControllerSkinConfigurationiPadSplitViewLandscape NS_SWIFT_NAME(ipadSplitViewLandscape) = 1 << 3, // Backwards compatible with legacy ControllerSkinConfigurationSplitViewLandscape
|
||||
|
||||
ControllerSkinConfigurationiPadEdgeToEdgePortrait NS_SWIFT_NAME(ipadEdgeToEdgePortrait) = 1 << 8,
|
||||
ControllerSkinConfigurationiPadEdgeToEdgeLandscape NS_SWIFT_NAME(ipadEdgeToEdgeLandscape) = 1 << 9,
|
||||
|
||||
|
||||
/* TV */
|
||||
ControllerSkinConfigurationTVStandardPortrait = 1 << 10,
|
||||
ControllerSkinConfigurationTVStandardLandscape = 1 << 11,
|
||||
};
|
||||
|
||||
#endif /* ControllerSkinConfigurations_h */
|
||||
|
||||
@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.78" systemVersion="18C54" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="LocalRecord" representedClassName="LocalRecord" syncable="YES">
|
||||
<attribute name="modificationDate" attributeType="Date" syncable="YES"/>
|
||||
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
|
||||
<attribute name="recordedObjectURI" attributeType="URI" syncable="YES"/>
|
||||
<attribute name="status" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="versionDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="versionIdentifier" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="managedRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedRecord" inverseName="localRecord" inverseEntity="ManagedRecord" syncable="YES"/>
|
||||
<relationship name="remoteFiles" toMany="YES" deletionRule="Cascade" destinationEntity="RemoteFile" inverseName="localRecord" inverseEntity="RemoteFile" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="recordedObjectType"/>
|
||||
<constraint value="recordedObjectIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="ManagedRecord" representedClassName="ManagedRecord" syncable="YES">
|
||||
<attribute name="isConflicted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="isSyncingEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
|
||||
<relationship name="localRecord" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="LocalRecord" inverseName="managedRecord" inverseEntity="LocalRecord" syncable="YES"/>
|
||||
<relationship name="remoteRecord" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="RemoteRecord" inverseName="managedRecord" inverseEntity="RemoteRecord" syncable="YES"/>
|
||||
<fetchIndex name="byRecordedObject">
|
||||
<fetchIndexElement property="recordedObjectType" type="Binary" order="ascending"/>
|
||||
<fetchIndexElement property="recordedObjectIdentifier" type="Binary" order="ascending"/>
|
||||
</fetchIndex>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="recordedObjectType"/>
|
||||
<constraint value="recordedObjectIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="RemoteFile" representedClassName="RemoteFile" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="remoteIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="sha1Hash" attributeType="String" syncable="YES"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
|
||||
<attribute name="versionIdentifier" attributeType="String" syncable="YES"/>
|
||||
<relationship name="localRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="LocalRecord" inverseName="remoteFiles" inverseEntity="LocalRecord" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="RemoteRecord" representedClassName="RemoteRecord" syncable="YES">
|
||||
<attribute name="author" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="isLocked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="localizedName" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="metadata" optional="YES" attributeType="Transformable" syncable="YES"/>
|
||||
<attribute name="previousVersionDate" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="previousVersionIdentifier" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="recordedObjectIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="recordedObjectType" attributeType="String" syncable="YES"/>
|
||||
<attribute name="status" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="versionIdentifier" attributeType="String" syncable="YES"/>
|
||||
<relationship name="managedRecord" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ManagedRecord" inverseName="remoteRecord" inverseEntity="ManagedRecord" syncable="YES"/>
|
||||
<fetchIndex name="byIdentifier">
|
||||
<fetchIndexElement property="identifier" type="Binary" order="ascending"/>
|
||||
</fetchIndex>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="recordedObjectType"/>
|
||||
<constraint value="recordedObjectIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<configuration name="External"/>
|
||||
<configuration name="Harmony">
|
||||
<memberEntity name="ManagedRecord"/>
|
||||
<memberEntity name="LocalRecord"/>
|
||||
<memberEntity name="RemoteRecord"/>
|
||||
<memberEntity name="RemoteFile"/>
|
||||
</configuration>
|
||||
<elements>
|
||||
<element name="LocalRecord" positionX="93.01953125" positionY="273.0234375" width="128" height="180"/>
|
||||
<element name="ManagedRecord" positionX="298.4375" positionY="225.49609375" width="128" height="133"/>
|
||||
<element name="RemoteFile" positionX="288" positionY="378" width="128" height="135"/>
|
||||
<element name="RemoteRecord" positionX="488.2578125" positionY="272.56640625" width="128" height="240"/>
|
||||
</elements>
|
||||
</model>
|
||||
@ -0,0 +1,61 @@
|
||||
//
|
||||
// GameControllerInputMappingTransformer.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 9/27/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import DeltaCore
|
||||
|
||||
@objc(GameControllerInputMappingTransformer)
|
||||
class GameControllerInputMappingTransformer: ValueTransformer
|
||||
{
|
||||
override class func transformedValueClass() -> AnyClass {
|
||||
return NSData.self
|
||||
}
|
||||
|
||||
override class func allowsReverseTransformation() -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func transformedValue(_ value: Any?) -> Any?
|
||||
{
|
||||
guard let inputMapping = value as? DeltaCore.GameControllerInputMapping else { return nil }
|
||||
|
||||
let plistEncoder = PropertyListEncoder()
|
||||
|
||||
do
|
||||
{
|
||||
let data = try plistEncoder.encode(inputMapping)
|
||||
return data
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func reverseTransformedValue(_ value: Any?) -> Any?
|
||||
{
|
||||
guard let inputMappingData = value as? Data else { return nil }
|
||||
|
||||
let plistDecoder = PropertyListDecoder()
|
||||
|
||||
do
|
||||
{
|
||||
let inputMapping = try plistDecoder.decode(DeltaCore.GameControllerInputMapping.self, from: inputMappingData)
|
||||
return inputMapping
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,9 +8,34 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
// Must be a class (not struct) so it can be used with Objective-C generics
|
||||
class GameMetadata
|
||||
// Must be an NSObject subclass so it can be used with RSTCellContentDataSource.
|
||||
class GameMetadata: NSObject
|
||||
{
|
||||
var name: String?
|
||||
var artworkURL: URL?
|
||||
let releaseID: Int
|
||||
let romID: Int
|
||||
|
||||
let name: String?
|
||||
let artworkURL: URL?
|
||||
|
||||
init(releaseID: Int, romID: Int, name: String?, artworkURL: URL?)
|
||||
{
|
||||
self.releaseID = releaseID
|
||||
self.romID = romID
|
||||
self.name = name
|
||||
self.artworkURL = artworkURL
|
||||
}
|
||||
}
|
||||
|
||||
extension GameMetadata
|
||||
{
|
||||
override var hash: Int {
|
||||
return self.releaseID.hashValue ^ self.romID.hashValue
|
||||
}
|
||||
|
||||
override func isEqual(_ object: Any?) -> Bool
|
||||
{
|
||||
guard let metadata = object as? GameMetadata else { return false }
|
||||
|
||||
return self.releaseID == metadata.releaseID && self.romID == metadata.romID
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,6 +9,11 @@
|
||||
import Foundation
|
||||
import SQLite
|
||||
|
||||
private extension UserDefaults
|
||||
{
|
||||
@NSManaged var previousGamesDatabaseVersion: Int
|
||||
}
|
||||
|
||||
extension ExpressionType
|
||||
{
|
||||
static var name: SQLite.Expression<String?> {
|
||||
@ -19,13 +24,17 @@ extension ExpressionType
|
||||
return SQLite.Expression<String?>("releaseCoverFront")
|
||||
}
|
||||
|
||||
static var hash: SQLite.Expression<String> {
|
||||
static var sha1Hash: SQLite.Expression<String> {
|
||||
return SQLite.Expression<String>("romHashSHA1")
|
||||
}
|
||||
|
||||
static var romID: SQLite.Expression<Int> {
|
||||
return SQLite.Expression<Int>("romID")
|
||||
}
|
||||
|
||||
static var releaseID: SQLite.Expression<Int> {
|
||||
return SQLite.Expression<Int>("releaseID")
|
||||
}
|
||||
}
|
||||
|
||||
extension Table
|
||||
@ -48,16 +57,28 @@ extension VirtualTable
|
||||
|
||||
extension GamesDatabase
|
||||
{
|
||||
enum Error: Swift.Error
|
||||
enum Error: LocalizedError
|
||||
{
|
||||
case doesNotExist
|
||||
case connection(Swift.Error)
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self
|
||||
{
|
||||
case .doesNotExist:
|
||||
return NSLocalizedString("The SQLite database could not be found.", comment: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class GamesDatabase
|
||||
{
|
||||
fileprivate let connection: Connection
|
||||
static let version = 3
|
||||
static var previousVersion: Int? {
|
||||
return UserDefaults.standard.previousGamesDatabaseVersion
|
||||
}
|
||||
|
||||
private let connection: Connection
|
||||
|
||||
init() throws
|
||||
{
|
||||
@ -69,30 +90,39 @@ class GamesDatabase
|
||||
}
|
||||
catch
|
||||
{
|
||||
throw Error.connection(error)
|
||||
throw error
|
||||
}
|
||||
|
||||
self.invalidateVirtualTableIfNeeded()
|
||||
}
|
||||
|
||||
func metadataResults(forGameName gameName: String) -> [GameMetadata]
|
||||
{
|
||||
let releaseID = Expression<Any>.releaseID
|
||||
let romID = Expression<Any>.romID
|
||||
let name = Expression<Any>.name
|
||||
let artworkAddress = Expression<Any>.artworkAddress
|
||||
|
||||
let query = VirtualTable.search.select(name, artworkAddress).filter(name.match(gameName + "*"))
|
||||
let query = VirtualTable.search.select(releaseID, romID, name, artworkAddress).filter(name.match(gameName + "*"))
|
||||
|
||||
do
|
||||
{
|
||||
let rows = try self.connection.prepare(query)
|
||||
|
||||
let results = rows.map { row -> GameMetadata in
|
||||
let metadata = GameMetadata()
|
||||
metadata.name = row[name]
|
||||
let results = rows.map { (row) -> GameMetadata in
|
||||
|
||||
let artworkURL: URL?
|
||||
if let address = row[artworkAddress]
|
||||
{
|
||||
metadata.artworkURL = URL(string: address)
|
||||
artworkURL = URL(string: address)
|
||||
}
|
||||
else
|
||||
{
|
||||
artworkURL = nil
|
||||
}
|
||||
|
||||
|
||||
let metadata = GameMetadata(releaseID: row[releaseID], romID: row[romID], name: row[name], artworkURL: artworkURL)
|
||||
return metadata
|
||||
}
|
||||
|
||||
@ -118,26 +148,31 @@ class GamesDatabase
|
||||
|
||||
func metadata(for game: Game) -> GameMetadata?
|
||||
{
|
||||
let releaseID = Expression<Any>.releaseID
|
||||
let name = Expression<Any>.name
|
||||
let artworkAddress = Expression<Any>.artworkAddress
|
||||
let hash = Expression<Any>.hash
|
||||
|
||||
let sha1Hash = Expression<Any>.sha1Hash
|
||||
let romID = Expression<Any>.romID
|
||||
|
||||
let gameHash = game.identifier.uppercased()
|
||||
let query = Table.roms.select(name, artworkAddress).filter(hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID])
|
||||
let query = Table.roms.select(releaseID, name, artworkAddress, Table.roms[romID]).filter(sha1Hash == gameHash).join(Table.releases, on: Table.roms[romID] == Table.releases[romID])
|
||||
|
||||
do
|
||||
{
|
||||
if let row = try self.connection.pluck(query)
|
||||
{
|
||||
let metadata = GameMetadata()
|
||||
metadata.name = row[name]
|
||||
|
||||
let artworkURL: URL?
|
||||
if let address = row[artworkAddress]
|
||||
{
|
||||
metadata.artworkURL = URL(string: address)
|
||||
artworkURL = URL(string: address)
|
||||
}
|
||||
else
|
||||
{
|
||||
artworkURL = nil
|
||||
}
|
||||
|
||||
let metadata = GameMetadata(releaseID: row[releaseID], romID: row[Table.roms[romID]], name: row[name], artworkURL: artworkURL)
|
||||
return metadata
|
||||
}
|
||||
}
|
||||
@ -152,16 +187,34 @@ class GamesDatabase
|
||||
|
||||
private extension GamesDatabase
|
||||
{
|
||||
func invalidateVirtualTableIfNeeded()
|
||||
{
|
||||
guard UserDefaults.standard.previousGamesDatabaseVersion != GamesDatabase.version else { return }
|
||||
|
||||
do
|
||||
{
|
||||
try self.connection.run(VirtualTable.search.drop(ifExists: true))
|
||||
|
||||
UserDefaults.standard.previousGamesDatabaseVersion = GamesDatabase.version
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
func prepareFTS() -> Bool
|
||||
{
|
||||
let name = Expression<Any>.name
|
||||
let artworkAddress = Expression<Any>.artworkAddress
|
||||
let releaseID = Expression<Any>.releaseID
|
||||
let romID = Expression<Any>.romID
|
||||
|
||||
do
|
||||
{
|
||||
try self.connection.run(VirtualTable.search.create(.FTS4([name, artworkAddress], tokenize: .Unicode61())))
|
||||
try self.connection.run(VirtualTable.search.create(.FTS4([releaseID, romID, name, artworkAddress], tokenize: .Unicode61())))
|
||||
|
||||
let update = VirtualTable.search.insert(Table.releases.select(name, artworkAddress))
|
||||
let update = VirtualTable.search.insert(Table.releases.select(releaseID, romID, name, artworkAddress))
|
||||
_ = try self.connection.run(update)
|
||||
}
|
||||
catch
|
||||
|
||||
@ -15,13 +15,11 @@ class GamesDatabaseBrowserViewController: UITableViewController
|
||||
{
|
||||
var selectionHandler: ((GameMetadata) -> Void)?
|
||||
|
||||
fileprivate let database: GamesDatabase?
|
||||
fileprivate let dataSource: RSTArrayTableViewDataSource<GameMetadata>
|
||||
private let database: GamesDatabase?
|
||||
|
||||
fileprivate let operationQueue = RSTOperationQueue()
|
||||
fileprivate let imageCache = NSCache<NSURL, UIImage>()
|
||||
private let dataSource: RSTArrayTableViewPrefetchingDataSource<GameMetadata, UIImage>
|
||||
|
||||
override init(style: UITableViewStyle) {
|
||||
override init(style: UITableView.Style) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
@ -37,39 +35,13 @@ class GamesDatabaseBrowserViewController: UITableViewController
|
||||
print(error)
|
||||
}
|
||||
|
||||
self.dataSource = RSTArrayTableViewDataSource<GameMetadata>(items: [])
|
||||
|
||||
let placeholderView = RSTPlaceholderView()
|
||||
placeholderView.textLabel.textColor = UIColor.lightText
|
||||
placeholderView.detailTextLabel.textColor = UIColor.lightText
|
||||
|
||||
self.dataSource.placeholderView = placeholderView
|
||||
self.dataSource = RSTArrayTableViewPrefetchingDataSource<GameMetadata, UIImage>(items: [])
|
||||
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
self.dataSource.cellConfigurationHandler = { (cell, metadata, indexPath) in
|
||||
self.configure(cell: cell as! GameMetadataTableViewCell, with: metadata, for: indexPath)
|
||||
}
|
||||
|
||||
if let database = self.database
|
||||
{
|
||||
self.dataSource.searchController.searchHandler = { [unowned database, unowned dataSource] (searchValue, previousSearchValue) in
|
||||
|
||||
return RSTBlockOperation(executionBlock: { [unowned database, unowned dataSource] (operation) in
|
||||
let results = database.metadataResults(forGameName: searchValue.text)
|
||||
|
||||
guard !operation.isCancelled else { return }
|
||||
|
||||
dataSource.items = results
|
||||
|
||||
rst_dispatch_sync_on_main_thread {
|
||||
self.updatePlaceholderView()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
self.definesPresentationContext = true
|
||||
|
||||
self.prepareDataSource()
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
@ -82,26 +54,105 @@ class GamesDatabaseBrowserViewController: UITableViewController
|
||||
|
||||
self.view.backgroundColor = UIColor.deltaDarkGray
|
||||
|
||||
self.tableView.register(GameTableViewCell.nib!, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
self.tableView.prefetchDataSource = self.dataSource
|
||||
|
||||
self.tableView.indicatorStyle = .white
|
||||
self.tableView.separatorColor = UIColor.gray
|
||||
|
||||
self.dataSource.searchController.delegate = self
|
||||
self.dataSource.searchController.searchBar.barStyle = .blackTranslucent
|
||||
self.tableView.tableHeaderView = self.dataSource.searchController.searchBar
|
||||
self.dataSource.searchController.searchBar.barStyle = .black
|
||||
|
||||
self.navigationItem.searchController = self.dataSource.searchController
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = false
|
||||
|
||||
self.updatePlaceholderView()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.dataSource.searchController.isActive = true
|
||||
}
|
||||
|
||||
override func didReceiveMemoryWarning()
|
||||
{
|
||||
super.didReceiveMemoryWarning()
|
||||
}
|
||||
}
|
||||
|
||||
extension GamesDatabaseBrowserViewController
|
||||
private extension GamesDatabaseBrowserViewController
|
||||
{
|
||||
func configure(cell: GameMetadataTableViewCell, with metadata: GameMetadata, for indexPath: IndexPath)
|
||||
func prepareDataSource()
|
||||
{
|
||||
/* Placeholder View */
|
||||
let placeholderView = RSTPlaceholderView()
|
||||
placeholderView.textLabel.textColor = UIColor.lightText
|
||||
placeholderView.detailTextLabel.textColor = UIColor.lightText
|
||||
self.dataSource.placeholderView = placeholderView
|
||||
|
||||
|
||||
/* Cell Configuration */
|
||||
self.dataSource.cellConfigurationHandler = { [unowned self] (cell, metadata, indexPath) in
|
||||
self.configure(cell: cell as! GameTableViewCell, with: metadata, for: indexPath)
|
||||
}
|
||||
|
||||
|
||||
/* Prefetching */
|
||||
self.dataSource.prefetchHandler = { (metadata, indexPath, completionHandler) in
|
||||
guard let artworkURL = metadata.artworkURL else { return nil }
|
||||
|
||||
let operation = LoadImageURLOperation(url: artworkURL)
|
||||
operation.resultHandler = { (image, error) in
|
||||
completionHandler(image, error)
|
||||
}
|
||||
return operation
|
||||
}
|
||||
|
||||
self.dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
guard let image = image else { return }
|
||||
|
||||
let cell = cell as! GameTableViewCell
|
||||
|
||||
let artworkDisplaySize = AVMakeRect(aspectRatio: image.size, insideRect: cell.artworkImageView.bounds)
|
||||
let offset = (cell.artworkImageView.bounds.width - artworkDisplaySize.width) / 2
|
||||
|
||||
// Offset artworkImageViewLeadingConstraint and artworkImageViewTrailingConstraint to right-align artworkImageView
|
||||
cell.artworkImageViewLeadingConstraint.constant += offset
|
||||
cell.artworkImageViewTrailingConstraint.constant -= offset
|
||||
|
||||
cell.artworkImageView.image = image
|
||||
cell.artworkImageView.superview?.layoutIfNeeded()
|
||||
}
|
||||
|
||||
|
||||
/* Searching */
|
||||
if let database = self.database
|
||||
{
|
||||
self.dataSource.searchController.searchHandler = { [unowned self, unowned database] (searchValue, previousSearchValue) in
|
||||
return RSTBlockOperation() { [unowned self, unowned database] (operation) in
|
||||
let results = database.metadataResults(forGameName: searchValue.text)
|
||||
|
||||
guard !operation.isCancelled else { return }
|
||||
|
||||
self.dataSource.items = results
|
||||
|
||||
rst_dispatch_sync_on_main_thread {
|
||||
self.resetTableViewContentOffset()
|
||||
self.updatePlaceholderView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension GamesDatabaseBrowserViewController
|
||||
{
|
||||
func configure(cell: GameTableViewCell, with metadata: GameMetadata, for indexPath: IndexPath)
|
||||
{
|
||||
cell.backgroundColor = UIColor.deltaDarkGray
|
||||
|
||||
@ -112,30 +163,6 @@ extension GamesDatabaseBrowserViewController
|
||||
cell.artworkImageViewTrailingConstraint.constant = 15
|
||||
|
||||
cell.separatorInset.left = cell.nameLabel.frame.minX
|
||||
|
||||
if let artworkURL = metadata.artworkURL
|
||||
{
|
||||
let operation = LoadImageURLOperation(url: artworkURL)
|
||||
operation.resultsCache = self.imageCache
|
||||
operation.resultHandler = { (image, error) in
|
||||
if let image = image
|
||||
{
|
||||
let artworkDisplaySize = AVMakeRect(aspectRatio: image.size, insideRect: cell.artworkImageView.bounds)
|
||||
let offset = (cell.artworkImageView.bounds.width - artworkDisplaySize.width) / 2
|
||||
|
||||
DispatchQueue.main.async {
|
||||
// Offset artworkImageViewLeadingConstraint and artworkImageViewTrailingConstraint to right-align artworkImageView
|
||||
cell.artworkImageViewLeadingConstraint.constant += offset
|
||||
cell.artworkImageViewTrailingConstraint.constant -= offset
|
||||
|
||||
cell.artworkImageView.image = image
|
||||
cell.artworkImageView.superview?.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.operationQueue.addOperation(operation, forKey: indexPath as NSIndexPath)
|
||||
}
|
||||
}
|
||||
|
||||
func updatePlaceholderView()
|
||||
@ -153,6 +180,12 @@ extension GamesDatabaseBrowserViewController
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("Please make sure the name is correct, or try searching for another game.", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
func resetTableViewContentOffset()
|
||||
{
|
||||
self.tableView.setContentOffset(CGPoint.zero, animated: false)
|
||||
self.tableView.setContentOffset(CGPoint(x: 0, y: -self.view.safeAreaInsets.top), animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
extension GamesDatabaseBrowserViewController
|
||||
@ -167,20 +200,15 @@ extension GamesDatabaseBrowserViewController
|
||||
let metadata = self.dataSource.item(at: indexPath)
|
||||
self.selectionHandler?(metadata)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
||||
{
|
||||
let operation = self.operationQueue[indexPath as NSIndexPath]
|
||||
operation?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
|
||||
{
|
||||
func didPresentSearchController(_ searchController: UISearchController)
|
||||
{
|
||||
// Fix incorrect table view scroll indicator insets
|
||||
self.tableView.scrollIndicatorInsets.top = self.navigationController!.navigationBar.bounds.height + UIApplication.shared.statusBarFrame.height
|
||||
DispatchQueue.main.async {
|
||||
searchController.searchBar.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
func willDismissSearchController(_ searchController: UISearchController)
|
||||
@ -193,7 +221,6 @@ extension GamesDatabaseBrowserViewController: UISearchControllerDelegate
|
||||
func didDismissSearchController(_ searchController: UISearchController)
|
||||
{
|
||||
// Fix potentially incorrect offset if user dismisses searchController while scrolling
|
||||
self.tableView.setContentOffset(CGPoint.zero, animated: false)
|
||||
self.tableView.setContentOffset(CGPoint(x: 0, y: -self.topLayoutGuide.length), animated: false)
|
||||
self.resetTableViewContentOffset()
|
||||
}
|
||||
}
|
||||
|
||||
126
Delta/Database/Repair/GamePickerViewController.swift
Normal file
126
Delta/Database/Repair/GamePickerViewController.swift
Normal file
@ -0,0 +1,126 @@
|
||||
//
|
||||
// GamePickerViewController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 8/4/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import Roxas
|
||||
|
||||
class GamePickerViewController: UITableViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
var gameHandler: ((Game?) -> Void)?
|
||||
|
||||
init()
|
||||
{
|
||||
super.init(style: .insetGrouped)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.navigationController?.delegate = self
|
||||
|
||||
self.dataSource.proxy = self
|
||||
self.tableView.dataSource = self.dataSource
|
||||
self.tableView.prefetchDataSource = self.dataSource
|
||||
|
||||
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
self.navigationItem.title = NSLocalizedString("Choose Game", comment: "")
|
||||
self.navigationItem.searchController = self.dataSource.searchController
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = false
|
||||
}
|
||||
}
|
||||
|
||||
private extension GamePickerViewController
|
||||
{
|
||||
func makeDataSource() -> RSTFetchedResultsTableViewPrefetchingDataSource<Game, UIImage>
|
||||
{
|
||||
let fetchRequest = Game.fetchRequest()
|
||||
fetchRequest.propertiesToFetch = [#keyPath(Game.name), #keyPath(Game.identifier), #keyPath(Game.artworkURL)]
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Game.gameCollection?.index, ascending: true), NSSortDescriptor(keyPath: \Game.name, ascending: true)]
|
||||
|
||||
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(Game.gameCollection.name), cacheName: nil)
|
||||
let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<Game, UIImage>(fetchedResultsController: fetchedResultsController)
|
||||
dataSource.cellConfigurationHandler = { (cell, game, indexPath) in
|
||||
var configuration = UIListContentConfiguration.valueCell()
|
||||
configuration.prefersSideBySideTextAndSecondaryText = false
|
||||
|
||||
configuration.text = game.name
|
||||
|
||||
configuration.secondaryText = game.identifier
|
||||
configuration.secondaryTextProperties.font = .preferredFont(forTextStyle: .caption1)
|
||||
|
||||
configuration.image = UIImage(named: "BoxArt")
|
||||
configuration.imageProperties.maximumSize = CGSize(width: 48, height: 48)
|
||||
configuration.imageProperties.reservedLayoutSize = CGSize(width: 48, height: 48)
|
||||
configuration.imageProperties.cornerRadius = 4
|
||||
|
||||
cell.contentConfiguration = configuration
|
||||
}
|
||||
dataSource.prefetchHandler = { (game, indexPath, completionHandler) in
|
||||
guard let artworkURL = game.artworkURL else {
|
||||
completionHandler(nil, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
let imageOperation = LoadImageURLOperation(url: artworkURL)
|
||||
imageOperation.resultHandler = { (image, error) in
|
||||
completionHandler(image, error)
|
||||
}
|
||||
|
||||
return imageOperation
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
guard let image = image, var config = cell.contentConfiguration as? UIListContentConfiguration else { return }
|
||||
config.image = image
|
||||
cell.contentConfiguration = config
|
||||
}
|
||||
|
||||
dataSource.searchController.searchableKeyPaths = [#keyPath(Game.name), #keyPath(Game.identifier)]
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
extension GamePickerViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
let game = self.dataSource.item(at: indexPath)
|
||||
self.gameHandler?(game)
|
||||
|
||||
self.navigationController?.delegate = nil // Prevent calling navigationController(_:willShow:)
|
||||
self.navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
|
||||
{
|
||||
guard let section = self.dataSource.fetchedResultsController.sections?[section], !section.name.isEmpty else {
|
||||
return NSLocalizedString("Unknown System", comment: "")
|
||||
}
|
||||
|
||||
return section.name
|
||||
}
|
||||
}
|
||||
|
||||
extension GamePickerViewController: UINavigationControllerDelegate
|
||||
{
|
||||
func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool)
|
||||
{
|
||||
guard viewController != self else { return }
|
||||
|
||||
self.gameHandler?(nil)
|
||||
}
|
||||
}
|
||||
479
Delta/Database/Repair/RepairDatabaseViewController.swift
Normal file
479
Delta/Database/Repair/RepairDatabaseViewController.swift
Normal file
@ -0,0 +1,479 @@
|
||||
//
|
||||
// RepairDatabaseViewController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 8/4/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import OSLog
|
||||
|
||||
import DeltaCore
|
||||
|
||||
import Roxas
|
||||
import Harmony
|
||||
|
||||
private extension String
|
||||
{
|
||||
func sanitizedFilePath() -> String
|
||||
{
|
||||
let sanitizedFilePath = self.components(separatedBy: .urlFilenameAllowed.inverted).joined()
|
||||
return sanitizedFilePath
|
||||
}
|
||||
}
|
||||
|
||||
class RepairDatabaseViewController: UIViewController
|
||||
{
|
||||
var completionHandler: (() -> Void)?
|
||||
|
||||
private var _viewDidAppear = false
|
||||
|
||||
private lazy var managedObjectContext = DatabaseManager.shared.newBackgroundSavingViewContext()
|
||||
private lazy var gameSavesContext = DatabaseManager.shared.newBackgroundContext(withParent: self.managedObjectContext)
|
||||
|
||||
private var gamesByID: [String: Game]?
|
||||
|
||||
private lazy var backupsDirectory = FileManager.default.documentsDirectory.appendingPathComponent("Backups")
|
||||
private lazy var gameSavesDirectory = DatabaseManager.gamesDirectoryURL
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.view.backgroundColor = .systemBackground
|
||||
|
||||
self.isModalInPresentation = true
|
||||
|
||||
let placeholderView = RSTPlaceholderView()
|
||||
placeholderView.textLabel.text = NSLocalizedString("Verifying Database…", comment: "")
|
||||
placeholderView.detailTextLabel.text = nil
|
||||
placeholderView.activityIndicatorView.startAnimating()
|
||||
placeholderView.stackView.spacing = 15
|
||||
self.view.addSubview(placeholderView, pinningEdgesWith: .zero)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
if !_viewDidAppear
|
||||
{
|
||||
self.repairDatabase()
|
||||
}
|
||||
|
||||
_viewDidAppear = true
|
||||
}
|
||||
}
|
||||
|
||||
private extension RepairDatabaseViewController
|
||||
{
|
||||
func repairDatabase()
|
||||
{
|
||||
Logger.database.info("Begin repairing database...")
|
||||
|
||||
self.repairGames { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: "Unable to Repair Games", error: error)
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
case .success:
|
||||
self.repairGameSaves { result in
|
||||
DispatchQueue.main.async {
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
let alertController = UIAlertController(title: "Unable to Repair Save Files", error: error)
|
||||
self.present(alertController, animated: true)
|
||||
|
||||
case .success:
|
||||
self.showReviewViewController()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func repairGames(completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.managedObjectContext.perform {
|
||||
do
|
||||
{
|
||||
let fetchRequest = Game.fetchRequest()
|
||||
fetchRequest.propertiesToFetch = [#keyPath(Game.type)]
|
||||
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(Game.gameCollection)]
|
||||
|
||||
let allGames = try self.managedObjectContext.fetch(fetchRequest)
|
||||
let affectedGames = allGames.filter { $0.type.rawValue != $0.gameCollection?.identifier }
|
||||
|
||||
let gameCollections = try self.managedObjectContext.fetch(GameCollection.fetchRequest())
|
||||
let gameCollectionsByID = gameCollections.reduce(into: [:]) { $0[$1.identifier] = $1 }
|
||||
|
||||
for game in affectedGames
|
||||
{
|
||||
let gameCollection = gameCollectionsByID[game.type.rawValue]
|
||||
game.gameCollection = gameCollection
|
||||
|
||||
Logger.database.notice("Re-associating “\(game.name, privacy: .public)” with GameCollection: \(gameCollection?.identifier ?? "nil", privacy: .public)")
|
||||
}
|
||||
|
||||
try self.managedObjectContext.save()
|
||||
|
||||
completion(.success)
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func repairGameSaves(completion: @escaping (Result<Void, Error>) -> Void)
|
||||
{
|
||||
self.managedObjectContext.perform {
|
||||
do
|
||||
{
|
||||
// Fetch GameSaves that don't have same identifier as their Game,
|
||||
// OR GameSaves that have a non-nil SHA1 hash.
|
||||
//
|
||||
// This covers GameSaves connected to wrong games and GameSaves with nil Games,
|
||||
// as well as any GameSaves modified since last beta (which we assume are corrupted).
|
||||
|
||||
let fetchRequest = GameSave.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "(%K == nil) OR (%K != %K) OR (%K != nil)",
|
||||
#keyPath(GameSave.game),
|
||||
#keyPath(GameSave.identifier), #keyPath(GameSave.game.identifier),
|
||||
#keyPath(GameSave.sha1))
|
||||
|
||||
let gameSaves = try self.managedObjectContext.fetch(fetchRequest)
|
||||
let gameSavesByID = gameSaves.reduce(into: [:]) { $0[$1.identifier] = $1 }
|
||||
|
||||
let gamesFetchRequest = Game.fetchRequest()
|
||||
gamesFetchRequest.predicate = NSPredicate(format: "%K IN %@", #keyPath(Game.identifier), Set(gameSavesByID.keys))
|
||||
|
||||
let games = try self.managedObjectContext.fetch(gamesFetchRequest)
|
||||
self.gamesByID = games.reduce(into: [:]) { $0[$1.identifier] = $1 }
|
||||
|
||||
let savesBackupsDirectory = self.backupsDirectory.appendingPathComponent("Saves")
|
||||
try FileManager.default.createDirectory(at: savesBackupsDirectory, withIntermediateDirectories: true)
|
||||
|
||||
var conflictedGames = Set<Game>()
|
||||
|
||||
for gameSave in gameSaves
|
||||
{
|
||||
let expectedGame = self.repair(gameSave, backupsDirectory: savesBackupsDirectory)
|
||||
|
||||
// At this point, gameSave is only updated in gameSavesContext,
|
||||
// so gameSave here still points to previous game,
|
||||
|
||||
if let game = gameSave.game
|
||||
{
|
||||
Logger.database.notice("The save file for “\(game.name, privacy: .public)” is potentially corrupted, writing to conflicts.txt")
|
||||
conflictedGames.insert(game)
|
||||
}
|
||||
|
||||
if let expectedGame
|
||||
{
|
||||
Logger.database.notice("The save file for “\(expectedGame.name, privacy: .public)” is potentially corrupted, writing to conflicts.txt")
|
||||
conflictedGames.insert(expectedGame)
|
||||
}
|
||||
}
|
||||
|
||||
try self.gameSavesContext.performAndWait {
|
||||
try self.gameSavesContext.save()
|
||||
}
|
||||
|
||||
try self.managedObjectContext.save()
|
||||
|
||||
let outputURL = self.backupsDirectory.appendingPathComponent("conflicts.txt")
|
||||
|
||||
let conflictsLog = conflictedGames.map { $0.name + " (" + $0.identifier + ")" }.sorted().joined(separator: "\n")
|
||||
try conflictsLog.write(to: outputURL, atomically: true, encoding: .utf8)
|
||||
|
||||
completion(.success)
|
||||
}
|
||||
catch
|
||||
{
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns expectedGame, but in managedObjectContext (not gameSavesContext)
|
||||
func repair(_ gameSave: GameSave, backupsDirectory: URL) -> Game?
|
||||
{
|
||||
Logger.database.notice("Repairing GameSave \(gameSave.identifier, privacy: .public)...")
|
||||
|
||||
guard let expectedGame = self.gamesByID?[gameSave.identifier] else {
|
||||
// Game doesn't exist, so we'll back up save file and delete record.
|
||||
|
||||
Logger.database.warning("Orphaning GameSave \(gameSave.identifier, privacy: .public) due to no matching game.")
|
||||
|
||||
do
|
||||
{
|
||||
try self.backup(gameSave, for: nil, to: backupsDirectory)
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.database.error("Failed to back up save file for orphaned GameSave \(gameSave.identifier, privacy: .public). \(error, privacy: .public)")
|
||||
}
|
||||
|
||||
self.gameSavesContext.performAndWait {
|
||||
let gameSave = self.gameSavesContext.object(with: gameSave.objectID) as! GameSave
|
||||
gameSave.game = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
let misplacedGameSave: GameSave?
|
||||
if let otherGameSave = expectedGame.gameSave, otherGameSave != gameSave
|
||||
{
|
||||
misplacedGameSave = otherGameSave
|
||||
|
||||
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public) will misplace \(otherGameSave.identifier, privacy: .public)")
|
||||
}
|
||||
else
|
||||
{
|
||||
misplacedGameSave = nil
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
// Back up the save file gameSave (incorrectly) refers to, but name it after the _expected_ game.
|
||||
try self.backup(gameSave, for: expectedGame, to: backupsDirectory)
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.database.error("Failed to back up save file for GameSave \(gameSave.identifier, privacy: .public). Expected Game: \(expectedGame.identifier). \(error, privacy: .public)")
|
||||
}
|
||||
|
||||
// Ignore error if we can't hash file, not that big a deal.
|
||||
let hash = try? RSTHasher.sha1HashOfFile(at: expectedGame.gameSaveURL)
|
||||
|
||||
// Make changes on separate context so we don't change any relationships until we're finished.
|
||||
// This allows us to refer to previous relationships.
|
||||
self.gameSavesContext.performAndWait {
|
||||
let gameSave = self.gameSavesContext.object(with: gameSave.objectID) as! GameSave
|
||||
let expectedGame = self.gameSavesContext.object(with: expectedGame.objectID) as! Game
|
||||
let misplacedGameSave: GameSave? = misplacedGameSave.map { self.gameSavesContext.object(with: $0.objectID) as! GameSave }
|
||||
|
||||
if hash == gameSave.sha1
|
||||
{
|
||||
// .sav has same hash as GameSave SHA1,
|
||||
// so we can relink without changes.
|
||||
|
||||
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public)'s hash matches .sav, relinking without changes.")
|
||||
}
|
||||
else if let misplacedGameSave
|
||||
{
|
||||
// GameSave data differs from actual .sav file,
|
||||
// so copy metadata from misplacedGameSave.
|
||||
|
||||
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public)'s hash does NOT match .sav, ignoring misplaced GameSave \(misplacedGameSave.identifier, privacy: .public).")
|
||||
|
||||
// Not worth potential conflicts.
|
||||
// gameSave.sha1 = misplacedGameSave.sha1
|
||||
// gameSave.modifiedDate = misplacedGameSave.modifiedDate
|
||||
}
|
||||
else
|
||||
{
|
||||
// GameSave data differs from actual .sav file,
|
||||
// so copy metadata from disk.
|
||||
Logger.database.info("GameSave \(gameSave.identifier, privacy: .public)'s hash does NOT match .sav, ignoring.")
|
||||
|
||||
// Not worth potential conflicts.
|
||||
// let modifiedDate = try? FileManager.default.attributesOfItem(atPath: expectedGame.gameSaveURL.path)[.modificationDate] as? Date
|
||||
// gameSave.sha1 = hash
|
||||
// gameSave.modifiedDate = modifiedDate ?? Date()
|
||||
}
|
||||
|
||||
gameSave.game = expectedGame
|
||||
}
|
||||
|
||||
return expectedGame
|
||||
}
|
||||
|
||||
func backup(_ gameSave: GameSave, for expectedGame: Game?, to backupsDirectory: URL) throws
|
||||
{
|
||||
Logger.database.notice("Backing up GameSave \(gameSave.identifier, privacy: .public). Expected Game: \(expectedGame?.name ?? "nil", privacy: .public)")
|
||||
|
||||
if let game = gameSave.game
|
||||
{
|
||||
// GameSave is linked with incorrect game.
|
||||
|
||||
// Prefer using expectedGame's saveFileExtension over game's.
|
||||
let saveFileExtension: String
|
||||
if let deltaCore = Delta.core(for: expectedGame?.type ?? game.type)
|
||||
{
|
||||
saveFileExtension = deltaCore.gameSaveFileExtension
|
||||
}
|
||||
else
|
||||
{
|
||||
saveFileExtension = "sav"
|
||||
}
|
||||
|
||||
// 1. Backup existing file at `game`'s expected save file location
|
||||
if FileManager.default.fileExists(atPath: game.gameSaveURL.path)
|
||||
{
|
||||
// Filename = expectedGame.name? + game.identifier
|
||||
|
||||
let filename: String
|
||||
if let expectedGame
|
||||
{
|
||||
filename = expectedGame.name + "_" + game.identifier
|
||||
}
|
||||
else
|
||||
{
|
||||
filename = game.identifier
|
||||
}
|
||||
|
||||
let sanitizedFilename = filename.sanitizedFilePath()
|
||||
|
||||
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension(saveFileExtension)
|
||||
try FileManager.default.copyItem(at: game.gameSaveURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
Logger.database.notice("Backed up save file \(game.gameSaveURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
|
||||
|
||||
let rtcFileURL = game.gameSaveURL.deletingPathExtension().appendingPathExtension("rtc")
|
||||
if FileManager.default.fileExists(atPath: rtcFileURL.path)
|
||||
{
|
||||
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension("rtc")
|
||||
try FileManager.default.copyItem(at: rtcFileURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
Logger.database.notice("Backed up RTC save file \(rtcFileURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Backup existing file at `expectedGame`'s save file location
|
||||
if let expectedGame, FileManager.default.fileExists(atPath: expectedGame.gameSaveURL.path)
|
||||
{
|
||||
// Filename = expectedGame.name + (misplacedGameSave.identifier ?? expectedGame.identifier)
|
||||
|
||||
let filename = expectedGame.name + "_" + (expectedGame.gameSave?.identifier ?? expectedGame.identifier)
|
||||
let sanitizedFilename = filename.sanitizedFilePath()
|
||||
|
||||
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension(saveFileExtension)
|
||||
try FileManager.default.copyItem(at: expectedGame.gameSaveURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
Logger.database.notice("Backed up expected save file \(expectedGame.gameSaveURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
|
||||
|
||||
let rtcFileURL = expectedGame.gameSaveURL.deletingPathExtension().appendingPathExtension("rtc")
|
||||
if FileManager.default.fileExists(atPath: rtcFileURL.path)
|
||||
{
|
||||
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension("rtc")
|
||||
try FileManager.default.copyItem(at: rtcFileURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
Logger.database.notice("Backed up expected RTC save file \(rtcFileURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@discardableResult
|
||||
func backUp(_ saveFileURL: URL) throws -> Bool
|
||||
{
|
||||
guard FileManager.default.fileExists(atPath: saveFileURL.path) else { return false }
|
||||
|
||||
// Filename = expectedGame.name? + gameSave.identifier
|
||||
|
||||
let filename: String
|
||||
if let expectedGame
|
||||
{
|
||||
filename = expectedGame.name + "_" + gameSave.identifier
|
||||
}
|
||||
else
|
||||
{
|
||||
filename = gameSave.identifier
|
||||
}
|
||||
|
||||
let sanitizedFilename = filename.sanitizedFilePath()
|
||||
|
||||
let destinationURL = backupsDirectory.appendingPathComponent(sanitizedFilename).appendingPathExtension(saveFileURL.pathExtension)
|
||||
try FileManager.default.copyItem(at: saveFileURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
Logger.database.notice("Backed up discovered save file \(saveFileURL.lastPathComponent, privacy: .public) to \(destinationURL.lastPathComponent, privacy: .public)")
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GameSave is _not_ linked to a Game, so instead we iterate through all save files on disk to find match.
|
||||
let savURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("sav")
|
||||
let srmURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("srm")
|
||||
let dsvURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("dsv")
|
||||
|
||||
let saveFileURLs = [savURL, srmURL, dsvURL]
|
||||
for saveFileURL in saveFileURLs
|
||||
{
|
||||
if try backUp(saveFileURL)
|
||||
{
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// ALWAYS attempt to back up RTC file.
|
||||
let rtcURL = self.gameSavesDirectory.appendingPathComponent(gameSave.identifier).appendingPathExtension("rtc")
|
||||
try backUp(rtcURL)
|
||||
}
|
||||
}
|
||||
|
||||
func showReviewViewController()
|
||||
{
|
||||
Logger.database.info("Finished repairing Games and GameSaves, reviewing recent SaveStates...")
|
||||
|
||||
let viewController = ReviewSaveStatesViewController()
|
||||
viewController.filter = .sinceLastBeta
|
||||
viewController.completionHandler = { [weak self] in
|
||||
self?.finish()
|
||||
}
|
||||
self.navigationController?.pushViewController(viewController, animated: true)
|
||||
}
|
||||
|
||||
func finish()
|
||||
{
|
||||
Logger.database.info("Finished repairing database!")
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
do
|
||||
{
|
||||
let store = try OSLogStore(scope: .currentProcessIdentifier)
|
||||
|
||||
// All logs since the app launched.
|
||||
let position = store.position(timeIntervalSinceLatestBoot: 0)
|
||||
|
||||
let entries = try store.getEntries(at: position)
|
||||
.compactMap { $0 as? OSLogEntryLog }
|
||||
.filter { $0.subsystem == Logger.deltaSubsystem || $0.subsystem == Logger.harmonySubsystem }
|
||||
.map { "[\($0.date.formatted())] [\($0.level.localizedName)] \($0.composedMessage)" }
|
||||
|
||||
let outputURL = self.backupsDirectory.appendingPathComponent("repair.log")
|
||||
try FileManager.default.createDirectory(at: self.backupsDirectory, withIntermediateDirectories: true)
|
||||
|
||||
let outputText = entries.joined(separator: "\n")
|
||||
try outputText.write(to: outputURL, atomically: true, encoding: .utf8)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to export Harmony logs.", error)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Database Repaired", comment: ""),
|
||||
message: NSLocalizedString("Some save files may still be corrupted and require you to restore an older version from the Delta Sync settings.\n\nA text file listing all affected games has been saved to “On My Device/Delta/Backups/conflicts.txt” in the Files app, alongside backups of any conflicted save files.", comment: ""),
|
||||
preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: UIAlertAction.ok.title, style: UIAlertAction.ok.style) { _ in
|
||||
self.completionHandler?()
|
||||
})
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
377
Delta/Database/Repair/ReviewSaveStatesViewController.swift
Normal file
377
Delta/Database/Repair/ReviewSaveStatesViewController.swift
Normal file
@ -0,0 +1,377 @@
|
||||
//
|
||||
// ReviewSaveStatesViewController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 8/4/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import OSLog
|
||||
|
||||
import Harmony
|
||||
import Roxas
|
||||
|
||||
extension ReviewSaveStatesViewController
|
||||
{
|
||||
enum Filter
|
||||
{
|
||||
case recent
|
||||
case all
|
||||
case sinceLastBeta
|
||||
}
|
||||
}
|
||||
|
||||
extension RecordFlags
|
||||
{
|
||||
static let isGameRelationshipVerified = RecordFlags(rawValue: 1 << 0)
|
||||
}
|
||||
|
||||
class ReviewSaveStatesViewController: UITableViewController
|
||||
{
|
||||
var filter: Filter = .recent {
|
||||
didSet {
|
||||
self.updateDataSource()
|
||||
}
|
||||
}
|
||||
|
||||
var completionHandler: (() -> Void)?
|
||||
|
||||
private lazy var managedObjectContext = DatabaseManager.shared.newBackgroundSavingViewContext()
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var descriptionDataSource = self.makeDescriptionDataSource()
|
||||
private lazy var saveStatesDataSource = self.makeSaveStatesDataSource()
|
||||
|
||||
private weak var _parentNavigationController: UINavigationController?
|
||||
|
||||
init()
|
||||
{
|
||||
super.init(style: .insetGrouped)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.dataSource.proxy = self
|
||||
self.tableView.dataSource = self.dataSource
|
||||
self.tableView.prefetchDataSource = self.dataSource
|
||||
|
||||
self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(ReviewSaveStatesViewController.finish))
|
||||
self.navigationItem.rightBarButtonItem = doneButton
|
||||
|
||||
self.navigationItem.title = NSLocalizedString("Review Save States", comment: "")
|
||||
|
||||
// Disable going back to RepairDatabaseViewController.
|
||||
self.navigationItem.setHidesBackButton(true, animated: false)
|
||||
self.navigationController?.interactivePopGestureRecognizer?.isEnabled = false
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if let parent = self.parent, parent.navigationItem.title == nil
|
||||
{
|
||||
// Must change parent's navigationItem when we're contained in SwiftUI View.
|
||||
parent.navigationItem.title = NSLocalizedString("Review Save States", comment: "")
|
||||
parent.navigationItem.rightBarButtonItem = self.makeFilterButton()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
_parentNavigationController = self.parent?.navigationController
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
switch self.filter
|
||||
{
|
||||
case .all, .recent:
|
||||
if self.parent == nil || self.parent?.parent == nil
|
||||
{
|
||||
// Only finish if we're popped off navigation controller.
|
||||
self.finish()
|
||||
}
|
||||
|
||||
case .sinceLastBeta: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension ReviewSaveStatesViewController
|
||||
{
|
||||
func makeDataSource() -> RSTCompositeTableViewPrefetchingDataSource<SaveState, UIImage>
|
||||
{
|
||||
let dataSource = RSTCompositeTableViewPrefetchingDataSource<SaveState, UIImage>(dataSources: [self.descriptionDataSource, self.saveStatesDataSource])
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makeDescriptionDataSource() -> RSTDynamicTableViewPrefetchingDataSource<SaveState, UIImage>
|
||||
{
|
||||
let dataSource = RSTDynamicTableViewPrefetchingDataSource<SaveState, UIImage>()
|
||||
dataSource.numberOfSectionsHandler = { 1 }
|
||||
dataSource.numberOfItemsHandler = { _ in 0 }
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makeSaveStatesDataSource() -> RSTFetchedResultsTableViewPrefetchingDataSource<SaveState, UIImage>
|
||||
{
|
||||
let fetchedResultsController = self.makeSaveStatesFetchedResultsController()
|
||||
|
||||
let dataSource = RSTFetchedResultsTableViewPrefetchingDataSource<SaveState, UIImage>(fetchedResultsController: fetchedResultsController)
|
||||
dataSource.cellConfigurationHandler = { (cell, saveState, indexPath) in
|
||||
var configuration = UIListContentConfiguration.valueCell()
|
||||
configuration.prefersSideBySideTextAndSecondaryText = false
|
||||
|
||||
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold) ?? .preferredFontDescriptor(withTextStyle: .body)
|
||||
configuration.text = saveState.name ?? NSLocalizedString("Untitled", comment: "")
|
||||
configuration.textProperties.font = UIFont(descriptor: fontDescriptor, size: 0)
|
||||
|
||||
configuration.secondaryText = SaveState.localizedDateFormatter.string(from: saveState.modifiedDate)
|
||||
configuration.secondaryTextProperties.font = .preferredFont(forTextStyle: .caption1)
|
||||
|
||||
configuration.image = nil
|
||||
configuration.imageProperties.maximumSize = CGSize(width: 80, height: 80)
|
||||
configuration.imageProperties.reservedLayoutSize = CGSize(width: 80, height: 80)
|
||||
configuration.imageProperties.cornerRadius = 6
|
||||
|
||||
cell.contentConfiguration = configuration
|
||||
|
||||
cell.accessoryType = .disclosureIndicator
|
||||
}
|
||||
dataSource.prefetchHandler = { (saveState, indexPath, completionHandler) in
|
||||
guard saveState.game != nil else {
|
||||
completionHandler(nil, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
let imageOperation = LoadImageURLOperation(url: saveState.imageFileURL)
|
||||
imageOperation.resultHandler = { (image, error) in
|
||||
completionHandler(image, error)
|
||||
}
|
||||
|
||||
if self.isAppearing
|
||||
{
|
||||
imageOperation.start()
|
||||
imageOperation.waitUntilFinished()
|
||||
return nil
|
||||
}
|
||||
|
||||
return imageOperation
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
guard let image = image, var config = cell.contentConfiguration as? UIListContentConfiguration else { return }
|
||||
config.image = image
|
||||
cell.contentConfiguration = config
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makeSaveStatesFetchedResultsController() -> NSFetchedResultsController<SaveState>
|
||||
{
|
||||
let fetchRequest = SaveState.fetchRequest()
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \SaveState.game?.name, ascending: true), NSSortDescriptor(keyPath: \SaveState.modifiedDate, ascending: false)]
|
||||
|
||||
let predicate = NSPredicate(format: "%K != %@", #keyPath(SaveState.type), SaveStateType.auto.rawValue as NSNumber)
|
||||
|
||||
switch self.filter
|
||||
{
|
||||
case .recent:
|
||||
let oneMonthAgo = Calendar.current.date(byAdding: .month, value: -1, to: Date()) ?? Date().addingTimeInterval(-1 * 60 * 60 * 24 * 30)
|
||||
let recentPredicate = NSPredicate(format: "%K > %@", #keyPath(SaveState.modifiedDate), oneMonthAgo as NSDate)
|
||||
|
||||
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, recentPredicate])
|
||||
|
||||
case .all:
|
||||
fetchRequest.predicate = predicate
|
||||
|
||||
case .sinceLastBeta:
|
||||
let dateComponents = DateComponents(year: 2023, month: 7, day: 18, hour: 0, minute: 0, second: 0)
|
||||
let lastBetaDate = Calendar.current.date(from: dateComponents) ?? Date().addingTimeInterval(-1 * 60 * 60 * 24 * 45)
|
||||
|
||||
let sinceLastBetaPredicate = NSPredicate(format: "%K > %@", #keyPath(SaveState.modifiedDate), lastBetaDate as NSDate)
|
||||
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [predicate, sinceLastBetaPredicate])
|
||||
}
|
||||
|
||||
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext, sectionNameKeyPath: #keyPath(SaveState.game.name), cacheName: nil)
|
||||
return fetchedResultsController
|
||||
}
|
||||
|
||||
func updateDataSource()
|
||||
{
|
||||
let fetchedResultsController = self.makeSaveStatesFetchedResultsController()
|
||||
self.saveStatesDataSource.fetchedResultsController = fetchedResultsController
|
||||
}
|
||||
|
||||
func makeFilterButton() -> UIBarButtonItem
|
||||
{
|
||||
let recentAction = UIAction(title: NSLocalizedString("Past Month", comment: ""), image: UIImage(systemName: "calendar")) { [weak self] _ in
|
||||
self?.filter = .recent
|
||||
}
|
||||
let allAction = UIAction(title: NSLocalizedString("All Time", comment: ""), image: UIImage(systemName: "clock")) { [weak self] _ in
|
||||
self?.filter = .all
|
||||
}
|
||||
|
||||
var options: UIMenu.Options = []
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
options = .singleSelection
|
||||
|
||||
recentAction.state = self.filter == .recent ? .on : .off
|
||||
allAction.state = self.filter == .all ? .on : .off
|
||||
}
|
||||
|
||||
let filterMenu = UIMenu(options: options, children: [recentAction, allAction])
|
||||
|
||||
let filterButton = UIBarButtonItem(title: NSLocalizedString("Filter", comment: ""), image: UIImage(systemName: "calendar.badge.clock"), menu: filterMenu)
|
||||
return filterButton
|
||||
}
|
||||
}
|
||||
|
||||
private extension ReviewSaveStatesViewController
|
||||
{
|
||||
func pickGame(for saveState: SaveState)
|
||||
{
|
||||
let gamePickerViewController = GamePickerViewController()
|
||||
gamePickerViewController.gameHandler = { game in
|
||||
guard let game else { return }
|
||||
|
||||
let previousGame = saveState.game
|
||||
if previousGame != nil
|
||||
{
|
||||
// Move files to new location.
|
||||
|
||||
let destinationDirectory = DatabaseManager.saveStatesDirectoryURL(for: game)
|
||||
|
||||
for fileURL in [saveState.fileURL, saveState.imageFileURL]
|
||||
{
|
||||
guard FileManager.default.fileExists(atPath: fileURL.path) else { continue }
|
||||
|
||||
let destinationURL = destinationDirectory.appendingPathComponent(fileURL.lastPathComponent)
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true) // Copy, don't move, in case app quits before user confirms.
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.database.error("Failed to copy SaveState “\(saveState.localizedName, privacy: .public)” from \(fileURL, privacy: .public) to \(destinationURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tempGame = self.managedObjectContext.object(with: game.objectID) as! Game
|
||||
saveState.game = tempGame
|
||||
|
||||
Logger.database.notice("Re-associated SaveState “\(saveState.localizedName, privacy: .public)” with game “\(tempGame.name, privacy: .public)”. Previously: \(previousGame?.name ?? "nil", privacy: .public)")
|
||||
}
|
||||
|
||||
self.navigationController?.pushViewController(gamePickerViewController, animated: true)
|
||||
}
|
||||
|
||||
@objc func finish()
|
||||
{
|
||||
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = true
|
||||
|
||||
self.managedObjectContext.perform {
|
||||
do
|
||||
{
|
||||
let saveStates: [SaveState]?
|
||||
|
||||
switch self.filter
|
||||
{
|
||||
case .recent, .all:
|
||||
// Only upload metadata for changed SaveStates.
|
||||
saveStates = self.managedObjectContext.updatedObjects.compactMap { $0 as? SaveState }
|
||||
|
||||
case .sinceLastBeta:
|
||||
// Upload metadata for _all_ SaveStates.
|
||||
saveStates = self.saveStatesDataSource.fetchedResultsController.fetchedObjects
|
||||
}
|
||||
|
||||
try self.managedObjectContext.save()
|
||||
|
||||
if let saveStates = saveStates, let coordinator = SyncManager.shared.coordinator
|
||||
{
|
||||
let records = try coordinator.recordController.fetchRecords(for: saveStates)
|
||||
if let context = records.first?.recordedObject?.managedObjectContext
|
||||
{
|
||||
try context.performAndWait {
|
||||
for record in records
|
||||
{
|
||||
record.perform { managedRecord in
|
||||
managedRecord.flags.insert(.isGameRelationshipVerified)
|
||||
managedRecord.setNeedsMetadataUpdate()
|
||||
|
||||
let saveState = record.recordedObject
|
||||
Logger.database.notice("Flagged SaveState “\(saveState?.localizedName ?? record.recordID.identifier, privacy: .public)” for metadata update.")
|
||||
}
|
||||
}
|
||||
|
||||
try context.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.completionHandler?()
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
self.navigationItem.rightBarButtonItem?.isIndicatingActivity = false
|
||||
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Unable to Save Changes", comment: ""), error: error)
|
||||
(self._parentNavigationController ?? self).present(alertController, animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ReviewSaveStatesViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
|
||||
{
|
||||
let saveState = self.dataSource.item(at: indexPath)
|
||||
self.pickGame(for: saveState)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String?
|
||||
{
|
||||
if section == 0
|
||||
{
|
||||
return nil
|
||||
}
|
||||
else
|
||||
{
|
||||
let section = section - 1
|
||||
|
||||
guard let gameName = self.saveStatesDataSource.fetchedResultsController.sections?[section].name else { return NSLocalizedString("Unknown Game", comment: "") }
|
||||
return gameName
|
||||
}
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String?
|
||||
{
|
||||
guard section == 0 else { return nil }
|
||||
|
||||
return NSLocalizedString("These save states have been modified recently and may be associated with the wrong game.\n\nPlease change any incorrectly associated save states to the correct game by tapping them.", comment: "")
|
||||
}
|
||||
}
|
||||
67
Delta/Deep Linking/CopyDeepLinkActivity.swift
Normal file
67
Delta/Deep Linking/CopyDeepLinkActivity.swift
Normal file
@ -0,0 +1,67 @@
|
||||
//
|
||||
// CopyDeepLinkActivity.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 8/5/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension UIActivity.ActivityType
|
||||
{
|
||||
static let copyDeepLink = UIActivity.ActivityType("com.rileytestut.Delta.CopyDeepLink")
|
||||
}
|
||||
|
||||
class CopyDeepLinkActivity: UIActivity
|
||||
{
|
||||
private var deepLink: URL?
|
||||
|
||||
override class var activityCategory: UIActivity.Category {
|
||||
return .action
|
||||
}
|
||||
|
||||
override var activityType: UIActivity.ActivityType? {
|
||||
return .copyDeepLink
|
||||
}
|
||||
|
||||
override var activityTitle: String? {
|
||||
return NSLocalizedString("Copy Deep Link", comment: "")
|
||||
}
|
||||
|
||||
override var activityImage: UIImage? {
|
||||
return UIImage(symbolNameIfAvailable: "link") ?? UIImage(named: "Link")
|
||||
}
|
||||
|
||||
override func canPerform(withActivityItems activityItems: [Any]) -> Bool
|
||||
{
|
||||
if activityItems.contains(where: { $0 is Game })
|
||||
{
|
||||
return true
|
||||
}
|
||||
else
|
||||
{
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override func prepare(withActivityItems activityItems: [Any])
|
||||
{
|
||||
guard let game = activityItems.first(where: { $0 is Game }) as? Game else { return }
|
||||
|
||||
self.deepLink = URL(action: .launchGame(identifier: game.identifier))
|
||||
}
|
||||
|
||||
override func perform()
|
||||
{
|
||||
if let deepLink = self.deepLink
|
||||
{
|
||||
UIPasteboard.general.url = deepLink
|
||||
self.activityDidFinish(true)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.activityDidFinish(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
99
Delta/Deep Linking/DeepLink.swift
Normal file
99
Delta/Deep Linking/DeepLink.swift
Normal file
@ -0,0 +1,99 @@
|
||||
//
|
||||
// DeepLink.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 12/29/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension URL
|
||||
{
|
||||
init(action: DeepLink.Action)
|
||||
{
|
||||
switch action
|
||||
{
|
||||
case .launchGame(let identifier):
|
||||
let deepLinkURL = URL(string: "delta://\(action.type.rawValue)/\(identifier)")!
|
||||
self = deepLinkURL
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIApplicationShortcutItem
|
||||
{
|
||||
convenience init(localizedTitle: String, action: DeepLink.Action)
|
||||
{
|
||||
var userInfo: [String: NSSecureCoding]?
|
||||
|
||||
switch action
|
||||
{
|
||||
case .launchGame(let identifier): userInfo = [DeepLink.Key.identifier.rawValue: identifier as NSString]
|
||||
}
|
||||
|
||||
self.init(type: action.type.rawValue, localizedTitle: localizedTitle, localizedSubtitle: nil, icon: nil, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
extension DeepLink
|
||||
{
|
||||
enum Action
|
||||
{
|
||||
case launchGame(identifier: String)
|
||||
|
||||
var type: ActionType {
|
||||
switch self
|
||||
{
|
||||
case .launchGame: return .launchGame
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum ActionType: String
|
||||
{
|
||||
case launchGame = "game"
|
||||
}
|
||||
|
||||
enum Key: String
|
||||
{
|
||||
case identifier
|
||||
case game
|
||||
}
|
||||
}
|
||||
|
||||
enum DeepLink
|
||||
{
|
||||
case url(URL)
|
||||
case shortcut(UIApplicationShortcutItem)
|
||||
|
||||
var actionType: ActionType? {
|
||||
switch self
|
||||
{
|
||||
case .url(let url):
|
||||
guard let host = url.host else { return nil }
|
||||
|
||||
let type = ActionType(rawValue: host)
|
||||
return type
|
||||
|
||||
case .shortcut(let shortcut):
|
||||
let type = ActionType(rawValue: shortcut.type)
|
||||
return type
|
||||
}
|
||||
}
|
||||
|
||||
var action: Action? {
|
||||
guard let type = self.actionType else { return nil }
|
||||
|
||||
switch (self, type)
|
||||
{
|
||||
case (.url(let url), .launchGame):
|
||||
let identifier = url.lastPathComponent
|
||||
return .launchGame(identifier: identifier)
|
||||
|
||||
case (.shortcut(let shortcut), .launchGame):
|
||||
guard let identifier = shortcut.userInfo?[Key.identifier.rawValue] as? String else { return nil }
|
||||
return .launchGame(identifier: identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
91
Delta/Deep Linking/DeepLinkController.swift
Normal file
91
Delta/Deep Linking/DeepLinkController.swift
Normal file
@ -0,0 +1,91 @@
|
||||
//
|
||||
// DeepLinkController.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 12/28/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
extension Notification.Name
|
||||
{
|
||||
static let deepLinkControllerLaunchGame = Notification.Name("deepLinkControllerLaunchGame")
|
||||
}
|
||||
|
||||
extension UIViewController
|
||||
{
|
||||
var allowsDeepLinkingDismissal: Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
struct DeepLinkController
|
||||
{
|
||||
private var window: UIWindow? {
|
||||
if #available(iOS 13, *)
|
||||
{
|
||||
guard let delegate = UIApplication.shared.connectedScenes.lazy.compactMap({ $0.delegate as? UIWindowSceneDelegate }).first, let window = delegate.window else { return nil }
|
||||
return window
|
||||
}
|
||||
else
|
||||
{
|
||||
guard let delegate = UIApplication.shared.delegate, let window = delegate.window else { return nil }
|
||||
return window
|
||||
}
|
||||
}
|
||||
|
||||
private var topViewController: UIViewController? {
|
||||
guard let window = self.window else { return nil }
|
||||
|
||||
var topViewController = window.rootViewController
|
||||
while topViewController?.presentedViewController != nil
|
||||
{
|
||||
guard !(topViewController?.presentedViewController is UIAlertController) else { break }
|
||||
|
||||
topViewController = topViewController?.presentedViewController
|
||||
}
|
||||
|
||||
return topViewController
|
||||
}
|
||||
}
|
||||
|
||||
extension DeepLinkController
|
||||
{
|
||||
@discardableResult func handle(_ deepLink: DeepLink) -> Bool
|
||||
{
|
||||
guard let action = deepLink.action else { return false }
|
||||
|
||||
switch action
|
||||
{
|
||||
case .launchGame(let identifier): return self.launchGame(withIdentifier: identifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension DeepLinkController
|
||||
{
|
||||
func launchGame(withIdentifier identifier: String) -> Bool
|
||||
{
|
||||
guard let topViewController = self.topViewController, topViewController.allowsDeepLinkingDismissal else { return false }
|
||||
|
||||
let fetchRequest: NSFetchRequest<Game> = Game.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "%K == %@", #keyPath(Game.identifier), identifier)
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
|
||||
do
|
||||
{
|
||||
guard let game = try DatabaseManager.shared.viewContext.fetch(fetchRequest).first else { return false }
|
||||
|
||||
NotificationCenter.default.post(name: .deepLinkControllerLaunchGame, object: self, userInfo: [DeepLink.Key.game: game])
|
||||
}
|
||||
catch
|
||||
{
|
||||
print(error)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
29
Delta/Emulation/ActionInput.swift
Normal file
29
Delta/Emulation/ActionInput.swift
Normal file
@ -0,0 +1,29 @@
|
||||
//
|
||||
// ActionInput.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 8/28/17.
|
||||
// Copyright © 2017 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import DeltaCore
|
||||
|
||||
public extension GameControllerInputType
|
||||
{
|
||||
static let action = GameControllerInputType("com.rileytestut.Delta.input.action")
|
||||
}
|
||||
|
||||
enum ActionInput: String
|
||||
{
|
||||
case quickSave
|
||||
case quickLoad
|
||||
case fastForward
|
||||
case toggleFastForward
|
||||
}
|
||||
|
||||
extension ActionInput: Input
|
||||
{
|
||||
var type: InputType {
|
||||
return .controller(.action)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -15,19 +15,31 @@ private var kvoContext = 0
|
||||
class PreviewGameViewController: DeltaCore.GameViewController
|
||||
{
|
||||
// If non-nil, will override the default preview action items returned in previewActionItems()
|
||||
// 如果非nil,将覆盖previewActionItems()中返回的默认预览操作项
|
||||
var overridePreviewActionItems: [UIPreviewActionItem]?
|
||||
|
||||
// Save state to be loaded upon preview
|
||||
// 保存预览时加载的状态
|
||||
var previewSaveState: SaveStateProtocol?
|
||||
|
||||
// Initial image to be shown while loading
|
||||
// 加载时显示的初始图像
|
||||
var previewImage: UIImage? {
|
||||
didSet {
|
||||
self.updatePreviewImage()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated)
|
||||
var isLivePreview: Bool = true
|
||||
|
||||
private var emulatorCoreQueue = DispatchQueue(label: "com.rileytestut.Delta.PreviewGameViewController.emulatorCoreQueue", qos: .userInitiated)
|
||||
private var copiedSaveFiles = [(originalURL: URL, copyURL: URL)]()
|
||||
|
||||
private lazy var temporaryDirectoryURL: URL = {
|
||||
let directoryURL = FileManager.default.temporaryDirectory.appendingPathComponent("preview-" + UUID().uuidString)
|
||||
try? FileManager.default.createDirectory(at: directoryURL, withIntermediateDirectories: true, attributes: nil)
|
||||
return directoryURL
|
||||
}()
|
||||
|
||||
override var game: GameProtocol? {
|
||||
willSet {
|
||||
@ -41,7 +53,8 @@ class PreviewGameViewController: DeltaCore.GameViewController
|
||||
|
||||
emulatorCore.addObserver(self, forKeyPath: #keyPath(EmulatorCore.state), options: [.old], context: &kvoContext)
|
||||
|
||||
self.preferredContentSize = emulatorCore.preferredRenderingSize
|
||||
let size = CGSize(width: emulatorCore.preferredRenderingSize.width * 2.0, height: emulatorCore.preferredRenderingSize.height * 2.0)
|
||||
self.preferredContentSize = size
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,8 +63,28 @@ class PreviewGameViewController: DeltaCore.GameViewController
|
||||
return previewActionItems
|
||||
}
|
||||
|
||||
public required init()
|
||||
{
|
||||
super.init()
|
||||
|
||||
self.delegate = self
|
||||
}
|
||||
|
||||
public required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
self.delegate = self
|
||||
}
|
||||
|
||||
deinit
|
||||
{
|
||||
// Explicitly stop emulatorCore _before_ we remove ourselves as observer
|
||||
// so we can wait until stopped before restoring save files (again).
|
||||
// 在我们删除自己作为观察者之前显式停止 emulatorCore
|
||||
// 所以我们可以等到停止后再恢复保存文件(再次)。
|
||||
self.emulatorCore?.stop()
|
||||
|
||||
self.emulatorCore?.removeObserver(self, forKeyPath: #keyPath(EmulatorCore.state), context: &kvoContext)
|
||||
}
|
||||
}
|
||||
@ -65,17 +98,26 @@ extension PreviewGameViewController
|
||||
super.viewDidLoad()
|
||||
|
||||
self.controllerView.isHidden = true
|
||||
self.controllerView.controllerSkin = nil // Skip loading controller skin from disk, which may be slow.跳过从磁盘加载控制器皮肤,这可能会很慢。
|
||||
|
||||
// Temporarily prevent emulatorCore from updating gameView to prevent flicker of black, or other visual glitches
|
||||
// 暂时阻止 emulatorCore 更新 gameView 以防止黑色闪烁或其他视觉故障
|
||||
self.emulatorCore?.remove(self.gameView)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
self.copySaveFiles()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.emulatorCoreQueue.async {
|
||||
self.emulatorCore?.resume()
|
||||
self.startEmulation()
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,14 +126,33 @@ extension PreviewGameViewController
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Pause in viewWillDisappear and not viewDidDisappear like DeltaCore.GameViewController so the audio cuts off earlier if being dismissed
|
||||
// 在 viewWillDisappear 中暂停,而不是像 DeltaCore.GameViewController 那样在 viewDidDisappear 中暂停,因此如果被关闭,音频会更早切断
|
||||
self.emulatorCore?.pause()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
// Already stopped = we've already restored save files and removed directory.
|
||||
// 已经停止 = 我们已经恢复了保存文件并删除了目录。
|
||||
if self.emulatorCore?.state != .stopped
|
||||
{
|
||||
// Pre-emptively restore save files in case something goes wrong while stopping emulation.
|
||||
// This also ensures if the core is never stopped (for some reason), saves are still restored.
|
||||
// 抢先恢复保存文件,以防在停止仿真时出现问题。
|
||||
// 这也确保如果核心从未停止(由于某种原因),保存仍然可以恢复。
|
||||
self.restoreSaveFiles(removeCopyDirectory: false)
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews()
|
||||
{
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
// Need to update in viewDidLayoutSubviews() to ensure bounds of gameView are not CGRect.zero
|
||||
|
||||
// 需要在 viewDidLayoutSubviews() 中进行更新以确保 gameView 的边界不是 CGRect.zero
|
||||
self.updatePreviewImage()
|
||||
}
|
||||
|
||||
@ -99,6 +160,7 @@ extension PreviewGameViewController
|
||||
{
|
||||
super.didReceiveMemoryWarning()
|
||||
// Dispose of any resources that can be recreated.
|
||||
// 处理掉所有可以重新创建的资源。
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?)
|
||||
@ -111,17 +173,24 @@ extension PreviewGameViewController
|
||||
let state = self.emulatorCore?.state
|
||||
else { return }
|
||||
|
||||
if previousState == .stopped, state == .running
|
||||
{
|
||||
self.emulatorCoreQueue.sync {
|
||||
if self.isAppearing
|
||||
switch state
|
||||
{
|
||||
case .running where previousState == .stopped:
|
||||
self.emulatorCoreQueue.async {
|
||||
// Pause to prevent it from starting before visible (in case user peeked slowly)
|
||||
// 暂停以防止它在可见之前启动(以防用户缓慢地偷看)
|
||||
self.emulatorCore?.pause()
|
||||
}
|
||||
|
||||
self.preparePreview()
|
||||
}
|
||||
|
||||
case .stopped:
|
||||
// Emulation has stopped, so we can safely restore save files,
|
||||
// and also remove the directory they were copied to.
|
||||
// 模拟已停止,因此我们可以安全地恢复保存文件,
|
||||
// 并删除它们复制到的目录。
|
||||
self.restoreSaveFiles(removeCopyDirectory: true)
|
||||
|
||||
default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -171,6 +240,69 @@ private extension PreviewGameViewController
|
||||
self.emulatorCore?.updateCheats()
|
||||
|
||||
// Re-enable emulatorCore to update gameView again
|
||||
// 重新启用emulatorCore以再次更新gameView
|
||||
self.emulatorCore?.add(self.gameView)
|
||||
|
||||
self.emulatorCore?.resume()
|
||||
}
|
||||
|
||||
func copySaveFiles()
|
||||
{
|
||||
guard let game = self.game as? Game, let gameSave = game.gameSave else { return }
|
||||
|
||||
self.copiedSaveFiles.removeAll()
|
||||
|
||||
let fileURLs = gameSave.syncableFiles.lazy.map { $0.fileURL }
|
||||
for fileURL in fileURLs
|
||||
{
|
||||
do
|
||||
{
|
||||
let destinationURL = self.temporaryDirectoryURL.appendingPathComponent(fileURL.lastPathComponent)
|
||||
try FileManager.default.copyItem(at: fileURL, to: destinationURL, shouldReplace: true)
|
||||
|
||||
self.copiedSaveFiles.append((fileURL, destinationURL))
|
||||
print("Copied save file:", fileURL.lastPathComponent)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to back up save file \(fileURL.lastPathComponent).", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restoreSaveFiles(removeCopyDirectory: Bool)
|
||||
{
|
||||
for (originalURL, copyURL) in self.copiedSaveFiles
|
||||
{
|
||||
do
|
||||
{
|
||||
try FileManager.default.copyItem(at: copyURL, to: originalURL, shouldReplace: true)
|
||||
print("Restored save file:", originalURL.lastPathComponent)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to restore copied save file \(copyURL.lastPathComponent).", error)
|
||||
}
|
||||
}
|
||||
|
||||
if removeCopyDirectory
|
||||
{
|
||||
do
|
||||
{
|
||||
try FileManager.default.removeItem(at: self.temporaryDirectoryURL)
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to remove preview temporary directory.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PreviewGameViewController: GameViewControllerDelegate
|
||||
{
|
||||
func gameViewControllerShouldResumeEmulation(_ gameViewController: DeltaCore.GameViewController) -> Bool
|
||||
{
|
||||
return self.isLivePreview
|
||||
}
|
||||
}
|
||||
|
||||
53
Delta/Experimental Features/ExperimentalFeatures.swift
Normal file
53
Delta/Experimental Features/ExperimentalFeatures.swift
Normal file
@ -0,0 +1,53 @@
|
||||
//
|
||||
// ExperimentalFeatures.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 4/6/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import DeltaFeatures
|
||||
|
||||
struct ExperimentalFeatures: FeatureContainer
|
||||
{
|
||||
static let shared = ExperimentalFeatures()
|
||||
|
||||
@Feature(name: "AirPlay Skins",
|
||||
description: "Customize the appearance of games when AirPlaying to your TV.",
|
||||
options: AirPlaySkinsOptions())
|
||||
var airPlaySkins
|
||||
|
||||
@Feature(name: "Variable Fast Forward",
|
||||
description: "Change the preferred Fast Foward speed per-system. You can also change it by long-pressing the Fast Forward button from the Pause Menu.",
|
||||
options: VariableFastForwardOptions())
|
||||
var variableFastForward
|
||||
|
||||
@Feature(name: "Show Status Bar",
|
||||
description: "Enable to show the Status Bar during gameplay.")
|
||||
var showStatusBar
|
||||
|
||||
@Feature(name: "Game Screenshots",
|
||||
description: "When enabled, a Screenshot button will appear in the Pause Menu, allowing you to save a screenshot of your game. You can choose to save the screenshot to Photos or Files.",
|
||||
options: GameScreenshotsOptions())
|
||||
var gameScreenshots
|
||||
|
||||
@Feature(name: "Toast Notifications",
|
||||
description: "Show toast notifications as a confirmation for various actions, such as saving your game or loading a save state.",
|
||||
options: ToastNotificationOptions())
|
||||
var toastNotifications
|
||||
|
||||
@Feature(name: "Review Save States",
|
||||
description: "Review recent Save States to make sure they are associated with the correct game.",
|
||||
options: ReviewSaveStatesOptions())
|
||||
var reviewSaveStates
|
||||
|
||||
@Feature(name: "Alternate App Icon",
|
||||
description: "Change the app icon.",
|
||||
options: AlternateAppIconOptions())
|
||||
var alternateAppIcons
|
||||
|
||||
private init()
|
||||
{
|
||||
self.prepareFeatures()
|
||||
}
|
||||
}
|
||||
167
Delta/Experimental Features/Features/AirPlaySkins.swift
Normal file
167
Delta/Experimental Features/Features/AirPlaySkins.swift
Normal file
@ -0,0 +1,167 @@
|
||||
//
|
||||
// AirPlaySkins.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 4/20/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import DeltaFeatures
|
||||
import DeltaCore
|
||||
|
||||
extension Feature where Options == AirPlaySkinsOptions
|
||||
{
|
||||
func preferredAirPlayControllerSkin(for gameType: GameType) -> ControllerSkin?
|
||||
{
|
||||
guard let identifier = self[gameType] else { return nil }
|
||||
|
||||
let predicate = NSPredicate(format: "%K == %@", #keyPath(ControllerSkin.identifier), identifier)
|
||||
let controllerSkin = ControllerSkin.instancesWithPredicate(predicate, inManagedObjectContext: DatabaseManager.shared.viewContext, type: ControllerSkin.self).first
|
||||
return controllerSkin
|
||||
}
|
||||
}
|
||||
|
||||
struct AirPlaySkinsOptions
|
||||
{
|
||||
@Option(name: "Manage Skins", detailView: { _ in SkinManager() })
|
||||
private var skinManager: String = "" // Hack until I figure out how to support Void properties...
|
||||
|
||||
@Option(name: LocalizedStringKey(System.nes.localizedName), description: "The controller skin used when AirPlaying NES games.", detailView: { SkinPicker(gameType: .nes, controllerSkinID: $0) })
|
||||
var nes: String?
|
||||
|
||||
@Option(name: LocalizedStringKey(System.snes.localizedName), description: "The controller skin used when AirPlaying SNES games.", detailView: { SkinPicker(gameType: .snes, controllerSkinID: $0) })
|
||||
var snes: String?
|
||||
|
||||
@Option(name: LocalizedStringKey(System.genesis.localizedName), description: "The controller skin used when AirPlaying Genesis games.", detailView: { SkinPicker(gameType: .genesis, controllerSkinID: $0) })
|
||||
var genesis: String?
|
||||
|
||||
@Option(name: LocalizedStringKey(System.n64.localizedName), description: "The controller skin used when AirPlaying N64 games.", detailView: { SkinPicker(gameType: .n64, controllerSkinID: $0) })
|
||||
var n64: String?
|
||||
|
||||
@Option(name: LocalizedStringKey(System.gbc.localizedName), description: "The controller skin used when AirPlaying GBC games.", detailView: { SkinPicker(gameType: .gbc, controllerSkinID: $0) })
|
||||
var gbc: String?
|
||||
|
||||
@Option(name: LocalizedStringKey(System.gba.localizedName), description: "The controller skin used when AirPlaying GBA games.", detailView: { SkinPicker(gameType: .gba, controllerSkinID: $0) })
|
||||
var gba: String?
|
||||
|
||||
@Option(name: LocalizedStringKey(System.ds.localizedName), description: "The controller skin used when AirPlaying DS games.", detailView: { SkinPicker(gameType: .ds, controllerSkinID: $0) })
|
||||
var ds: String?
|
||||
|
||||
subscript(gameType: GameType) -> String? {
|
||||
guard let system = System(gameType: gameType) else { return nil }
|
||||
switch system
|
||||
{
|
||||
case .nes: return self.nes
|
||||
case .snes: return self.snes
|
||||
case .genesis: return self.genesis
|
||||
case .n64: return self.n64
|
||||
case .gbc: return self.gbc
|
||||
case .gba: return self.gba
|
||||
case .ds: return self.ds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate extension AirPlaySkinsOptions
|
||||
{
|
||||
struct SkinPicker: View
|
||||
{
|
||||
let gameType: GameType
|
||||
|
||||
@Binding
|
||||
var controllerSkinID: String?
|
||||
|
||||
@FetchRequest
|
||||
private var controllerSkins: FetchedResults<ControllerSkin>
|
||||
|
||||
@Environment(\.featureOption)
|
||||
private var option
|
||||
|
||||
var body: some View {
|
||||
Picker(option.name ?? "", selection: $controllerSkinID) {
|
||||
ForEach(controllerSkins, id: \.identifier) { controllerSkin in
|
||||
Text(controllerSkin.name)
|
||||
.tag(Optional<String>(controllerSkin.identifier)) // Must be Optional<String> in order for selection to work.
|
||||
// .tag(controllerSkin.identifier)
|
||||
}
|
||||
|
||||
Text("None")
|
||||
.tag(String?.none)
|
||||
}
|
||||
.pickerStyle(.menu)
|
||||
.displayInline()
|
||||
}
|
||||
|
||||
init(gameType: GameType, controllerSkinID: Binding<String?>)
|
||||
{
|
||||
self.gameType = gameType
|
||||
self._controllerSkinID = controllerSkinID
|
||||
|
||||
let configuration = ControllerSkinConfigurations.tvStandardLandscape
|
||||
|
||||
let predicate = NSPredicate(format: "%K == %@ AND (%K & %d) != 0 AND %K == NO",
|
||||
#keyPath(ControllerSkin.gameType), self.gameType.rawValue,
|
||||
#keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue,
|
||||
#keyPath(ControllerSkin.isStandard))
|
||||
|
||||
self._controllerSkins = FetchRequest(entity: ControllerSkin.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \ControllerSkin.name, ascending: true)], predicate: predicate)
|
||||
}
|
||||
}
|
||||
|
||||
struct SkinManager: View
|
||||
{
|
||||
@FetchRequest(entity: ControllerSkin.entity(),
|
||||
sortDescriptors: [NSSortDescriptor(keyPath: \ControllerSkin.name, ascending: true)],
|
||||
predicate: {
|
||||
let configuration = ControllerSkinConfigurations.tvStandardLandscape
|
||||
return NSPredicate(format: "(%K & %d) != 0 AND %K == NO",
|
||||
#keyPath(ControllerSkin.supportedConfigurations), configuration.rawValue,
|
||||
#keyPath(ControllerSkin.isStandard))
|
||||
}())
|
||||
private var controllerSkins: FetchedResults<ControllerSkin>
|
||||
|
||||
var body: some View {
|
||||
if controllerSkins.isEmpty
|
||||
{
|
||||
Text("No AirPlay Skins")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
else
|
||||
{
|
||||
List {
|
||||
ForEach(controllerSkins, id: \.identifier) { controllerSkin in
|
||||
HStack {
|
||||
Text(controllerSkin.name)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let system = System(gameType: controllerSkin.gameType)
|
||||
{
|
||||
Text(system.localizedShortName)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deleteAirPlaySkins)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteAirPlaySkins(at indexes: IndexSet)
|
||||
{
|
||||
let objectIDs = indexes.map { controllerSkins[$0].objectID }
|
||||
|
||||
DatabaseManager.shared.performBackgroundTask { context in
|
||||
let controllerSkins = objectIDs.compactMap { context.object(with: $0) as? ControllerSkin }
|
||||
for controllerSkin in controllerSkins
|
||||
{
|
||||
context.delete(controllerSkin)
|
||||
}
|
||||
|
||||
context.saveWithErrorLogging()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
124
Delta/Experimental Features/Features/AlternateAppIcons.swift
Normal file
124
Delta/Experimental Features/Features/AlternateAppIcons.swift
Normal file
@ -0,0 +1,124 @@
|
||||
//
|
||||
// AlternateAppIcons.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Chris Rittenhouse on 5/2/23.
|
||||
// Copyright © 2023 LitRitt. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import DeltaFeatures
|
||||
|
||||
enum AppIcon: String, CaseIterable, CustomStringConvertible, Identifiable
|
||||
{
|
||||
case normal = "Default"
|
||||
case gba4ios = "GBA4iOS"
|
||||
case inverted = "Inverted"
|
||||
case pixelated = "Pixelated"
|
||||
case skin = "Controller Skin"
|
||||
|
||||
var description: String {
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
var id: String {
|
||||
return self.rawValue
|
||||
}
|
||||
|
||||
var author: String {
|
||||
switch self
|
||||
{
|
||||
case .normal: return "Caroline Moore"
|
||||
case .gba4ios: return "Paul Thorsen"
|
||||
case .inverted, .skin, .pixelated: return "LitRitt"
|
||||
}
|
||||
}
|
||||
|
||||
var assetName: String {
|
||||
switch self
|
||||
{
|
||||
case .normal: return "AppIcon"
|
||||
case .gba4ios: return "IconGBA4iOS"
|
||||
case .inverted: return "IconInverted"
|
||||
case .pixelated: return "IconPixelated"
|
||||
case .skin: return "IconSkin"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppIcon: Equatable
|
||||
{
|
||||
static func == (lhs: AppIcon, rhs: AppIcon) -> Bool
|
||||
{
|
||||
return lhs.description == rhs.description
|
||||
}
|
||||
}
|
||||
|
||||
extension AppIcon: LocalizedOptionValue
|
||||
{
|
||||
var localizedDescription: Text {
|
||||
Text(self.description)
|
||||
}
|
||||
}
|
||||
|
||||
struct AlternateAppIconOptions
|
||||
{
|
||||
@Option(name: "Alternate App Icon",
|
||||
description: "Choose from alternate app icons created by the community.",
|
||||
detailView: { value in
|
||||
List {
|
||||
ForEach(AppIcon.allCases) { icon in
|
||||
HStack {
|
||||
if icon == value.wrappedValue
|
||||
{
|
||||
Text("✓")
|
||||
}
|
||||
icon.localizedDescription
|
||||
Text("- by \(icon.author)")
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(.gray)
|
||||
Spacer()
|
||||
Image(uiImage: Bundle.appIcon(for: icon) ?? UIImage())
|
||||
.cornerRadius(13)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
value.wrappedValue = icon
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: value.wrappedValue) { _ in
|
||||
updateAppIcon()
|
||||
}
|
||||
.displayInline()
|
||||
})
|
||||
var icon: AppIcon = .normal
|
||||
}
|
||||
|
||||
extension AlternateAppIconOptions
|
||||
{
|
||||
static func updateAppIcon()
|
||||
{
|
||||
// Get current icon
|
||||
let currentIcon = UIApplication.shared.alternateIconName
|
||||
|
||||
// Apply chosen icon if feature is enabled
|
||||
if ExperimentalFeatures.shared.alternateAppIcons.isEnabled
|
||||
{
|
||||
let icon = ExperimentalFeatures.shared.alternateAppIcons.icon
|
||||
|
||||
// Only apply new icon if it's not already the current icon
|
||||
switch icon
|
||||
{
|
||||
case .normal: if currentIcon != nil { UIApplication.shared.setAlternateIconName(nil) } // Default app icon
|
||||
default: if currentIcon != icon.assetName { UIApplication.shared.setAlternateIconName(icon.assetName) } // Alternate app icon
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Remove alternate icons if feature is disabled
|
||||
if currentIcon != nil { UIApplication.shared.setAlternateIconName(nil) }
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Delta/Experimental Features/Features/GameScreenshots.swift
Normal file
54
Delta/Experimental Features/Features/GameScreenshots.swift
Normal file
@ -0,0 +1,54 @@
|
||||
//
|
||||
// GameScreenshots.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Chris Rittenhouse on 4/24/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import DeltaFeatures
|
||||
|
||||
enum ScreenshotSize: Double, CaseIterable, CustomStringConvertible
|
||||
{
|
||||
case x5 = 5
|
||||
case x4 = 4
|
||||
case x3 = 3
|
||||
case x2 = 2
|
||||
|
||||
var description: String {
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
let formattedText = self.rawValue.formatted(.number.decimalSeparator(strategy: .automatic))
|
||||
return "\(formattedText)x Size"
|
||||
}
|
||||
else
|
||||
{
|
||||
return "\(self.rawValue)x Size"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ScreenshotSize: LocalizedOptionValue
|
||||
{
|
||||
var localizedDescription: Text {
|
||||
Text(self.description)
|
||||
}
|
||||
|
||||
static var localizedNilDescription: Text {
|
||||
Text("Original Size")
|
||||
}
|
||||
}
|
||||
|
||||
struct GameScreenshotsOptions
|
||||
{
|
||||
@Option(name: "Save to Files", description: "Save the screenshot to the app's directory in Files.")
|
||||
var saveToFiles: Bool = true
|
||||
|
||||
@Option(name: "Save to Photos", description: "Save the screenshot to the Photo Library.")
|
||||
var saveToPhotos: Bool = false
|
||||
|
||||
@Option(name: "Image Size", description: "Choose the size of screenshots. This only increases the export size, it does not increase the quality.", values: ScreenshotSize.allCases)
|
||||
var size: ScreenshotSize?
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
//
|
||||
// LinkSaveStatesOptions.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Riley Testut on 8/7/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import DeltaFeatures
|
||||
|
||||
struct ReviewSaveStatesView: UIViewControllerRepresentable
|
||||
{
|
||||
func makeUIViewController(context: Context) -> ReviewSaveStatesViewController
|
||||
{
|
||||
let viewController = ReviewSaveStatesViewController()
|
||||
return viewController
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: ReviewSaveStatesViewController, context: Context)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
struct ReviewSaveStatesOptions
|
||||
{
|
||||
@Option(name: "View Save States", detailView: { _ in ReviewSaveStatesView() })
|
||||
private var reviewSaveStates: String = "" // Hack until I figure out how to support Void properties...
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
//
|
||||
// ToastNotificationOptions.swift
|
||||
// Delta
|
||||
//
|
||||
// Created by Chris Rittenhouse on 4/25/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import DeltaFeatures
|
||||
|
||||
struct ToastNotificationOptions
|
||||
{
|
||||
@Option(name: "Duration", description: "Change how long toasts should be shown.", detailView: { duration in
|
||||
HStack {
|
||||
Text("Duration: \(duration.wrappedValue, specifier: "%.1f")s")
|
||||
Slider(value: duration, in: 1...5, step: 0.5).displayInline()
|
||||
}
|
||||
})
|
||||
var duration: Double = 1.5
|
||||
|
||||
@Option(name: "Game Data Saved",
|
||||
description: "Show toasts when performing an in game save.")
|
||||
var gameSaveEnabled: Bool = true
|
||||
|
||||
@Option(name: "Saved Save State",
|
||||
description: "Show toasts when saving a save state.")
|
||||
var stateSaveEnabled: Bool = true
|
||||
|
||||
@Option(name: "Loaded Save State",
|
||||
description: "Show toasts when loading a save state.")
|
||||
var stateLoadEnabled: Bool = true
|
||||
|
||||
@Option(name: "Fast Forward Toggled",
|
||||
description: "Show toasts when toggling fast forward.")
|
||||
var fastForwardEnabled: Bool = true
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user