Compare commits
31 Commits
805e5eddd0
...
c99c397cf6
Author | SHA1 | Date |
---|---|---|
Shadowfacts | c99c397cf6 | |
Shadowfacts | 814f64b3e2 | |
Shadowfacts | 3a3af77907 | |
Shadowfacts | 93e72e1cb6 | |
Shadowfacts | 522e7830e5 | |
Shadowfacts | 263210ac3c | |
Shadowfacts | 506d2ad8a9 | |
Shadowfacts | f9c0506590 | |
Shadowfacts | 3f4917931b | |
Shadowfacts | b7166771cf | |
Shadowfacts | 40230c5478 | |
Shadowfacts | 68bd9e0bed | |
Shadowfacts | 3e28c012d7 | |
Shadowfacts | 57c023c973 | |
Shadowfacts | cc696e58fc | |
Shadowfacts | 59af29ff64 | |
Shadowfacts | 59fb69525b | |
Shadowfacts | 1bd4d144a3 | |
Shadowfacts | b54d34ebfc | |
Shadowfacts | d1ffab3e42 | |
Shadowfacts | d873b157ee | |
Shadowfacts | d7be2048af | |
Shadowfacts | 3d1f506684 | |
Shadowfacts | cd8f0e7926 | |
Shadowfacts | 960ba84683 | |
Shadowfacts | 2eead1f9de | |
Shadowfacts | b663335c6d | |
Shadowfacts | 9ce6bd566f | |
Shadowfacts | 9547bd2913 | |
Shadowfacts | 9b2e6140a3 | |
Shadowfacts | 6de255681c |
|
@ -0,0 +1,157 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
sodipodi:docname="Tusker.svg"
|
||||
inkscape:version="1.0beta2 (2b71d25, 2019-12-03)"
|
||||
inkscape:export-ydpi="11.52"
|
||||
inkscape:export-xdpi="11.52"
|
||||
inkscape:export-filename="/Users/shadowfacts/Desktop/60x60@2x.png"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 264.58333 264.58333"
|
||||
height="1000"
|
||||
width="1000">
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:path-effect
|
||||
bendpath1-nodetypes="cc"
|
||||
bendpath4="M 27.271345,85.808468 V 178.94843"
|
||||
bendpath3="M 27.271345,178.94843 H 242.39013"
|
||||
bendpath2="M 242.39013,85.808468 V 178.94843"
|
||||
bendpath1="M 26.897168,85.995557 242.39013,85.808468"
|
||||
xx="true"
|
||||
yy="true"
|
||||
lpeversion="1"
|
||||
is_visible="true"
|
||||
id="path-effect1345"
|
||||
effect="envelope" />
|
||||
<inkscape:path-effect
|
||||
allow_transforms="true"
|
||||
css_properties=""
|
||||
attributes=""
|
||||
method="d"
|
||||
linkeditem=""
|
||||
lpeversion="1"
|
||||
is_visible="true"
|
||||
id="path-effect38"
|
||||
effect="clone_original" />
|
||||
<inkscape:path-effect
|
||||
scale_y_rel="false"
|
||||
prop_scale="1"
|
||||
strokepath="M0,0 L1,0"
|
||||
endpoint_spacing_variation="0;1"
|
||||
endpoint_edge_variation="0;1"
|
||||
startpoint_spacing_variation="0;1"
|
||||
startpoint_edge_variation="0;1"
|
||||
count="5"
|
||||
lpeversion="1"
|
||||
is_visible="true"
|
||||
id="path-effect32"
|
||||
effect="curvestitching" />
|
||||
<filter
|
||||
height="1.3500000000000001"
|
||||
width="1.2"
|
||||
id="filter1277"
|
||||
inkscape:label="Drop Shadow"
|
||||
style="color-interpolation-filters:sRGB;">
|
||||
<feFlood
|
||||
id="feFlood1267"
|
||||
result="flood"
|
||||
flood-color="rgb(0,0,0)"
|
||||
flood-opacity="0.321569" />
|
||||
<feComposite
|
||||
id="feComposite1269"
|
||||
result="composite1"
|
||||
operator="in"
|
||||
in2="SourceGraphic"
|
||||
in="flood" />
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur1271"
|
||||
result="blur"
|
||||
stdDeviation="5"
|
||||
in="composite1" />
|
||||
<feOffset
|
||||
id="feOffset1273"
|
||||
result="offset"
|
||||
dy="5"
|
||||
dx="-2.5" />
|
||||
<feComposite
|
||||
id="feComposite1275"
|
||||
result="composite2"
|
||||
operator="over"
|
||||
in2="offset"
|
||||
in="SourceGraphic" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-x="1920"
|
||||
inkscape:window-height="1395"
|
||||
inkscape:window-width="1902"
|
||||
units="px"
|
||||
showgrid="false"
|
||||
inkscape:document-rotation="0"
|
||||
inkscape:current-layer="layer2"
|
||||
inkscape:document-units="px"
|
||||
inkscape:cy="496.39379"
|
||||
inkscape:cx="442.66632"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
borderopacity="1.0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
id="base" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 2"
|
||||
id="layer2"
|
||||
inkscape:groupmode="layer">
|
||||
<rect
|
||||
y="-0.14500916"
|
||||
x="-0.14500916"
|
||||
height="264.87335"
|
||||
width="264.87335"
|
||||
id="rect865"
|
||||
style="fill:#75e04e;fill-opacity:1;stroke:#75e04e;stroke-width:0.239149;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="display:none"
|
||||
id="layer1"
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="Layer 1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z"
|
||||
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
|
||||
id="path28" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1 copy"
|
||||
inkscape:groupmode="layer"
|
||||
id="g1343">
|
||||
<path
|
||||
id="path1341"
|
||||
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.56578600000000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.1 KiB |
|
@ -0,0 +1,153 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
sodipodi:docname="Tusker transparent.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||
inkscape:export-ydpi="98.304001"
|
||||
inkscape:export-xdpi="98.304001"
|
||||
inkscape:export-filename="../Desktop/1024x1024-dark@1x.png"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 264.58333 264.58333"
|
||||
height="1000"
|
||||
width="1000"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:path-effect
|
||||
bendpath1-nodetypes="cc"
|
||||
bendpath4="M 27.271345,85.808468 V 178.94843"
|
||||
bendpath3="M 27.271345,178.94843 H 242.39013"
|
||||
bendpath2="M 242.39013,85.808468 V 178.94843"
|
||||
bendpath1="M 26.897168,85.995557 242.39013,85.808468"
|
||||
xx="true"
|
||||
yy="true"
|
||||
lpeversion="1"
|
||||
is_visible="true"
|
||||
id="path-effect1345"
|
||||
effect="envelope" />
|
||||
<inkscape:path-effect
|
||||
allow_transforms="true"
|
||||
css_properties=""
|
||||
attributes=""
|
||||
method="d"
|
||||
linkeditem=""
|
||||
lpeversion="1"
|
||||
is_visible="true"
|
||||
id="path-effect38"
|
||||
effect="clone_original" />
|
||||
<inkscape:path-effect
|
||||
scale_y_rel="false"
|
||||
prop_scale="1"
|
||||
strokepath="M0,0 L1,0"
|
||||
endpoint_spacing_variation="0;1"
|
||||
endpoint_edge_variation="0;1"
|
||||
startpoint_spacing_variation="0;1"
|
||||
startpoint_edge_variation="0;1"
|
||||
count="5"
|
||||
lpeversion="1"
|
||||
is_visible="true"
|
||||
id="path-effect32"
|
||||
effect="curvestitching" />
|
||||
<filter
|
||||
height="1.317445"
|
||||
width="1.1258237"
|
||||
id="filter1277"
|
||||
inkscape:label="Drop Shadow"
|
||||
style="color-interpolation-filters:sRGB;"
|
||||
x="-0.068723437"
|
||||
y="-0.1318855">
|
||||
<feFlood
|
||||
id="feFlood1267"
|
||||
result="flood"
|
||||
flood-color="rgb(0,0,0)"
|
||||
flood-opacity="0.321569" />
|
||||
<feComposite
|
||||
id="feComposite1269"
|
||||
result="composite1"
|
||||
operator="in"
|
||||
in2="SourceGraphic"
|
||||
in="flood" />
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur1271"
|
||||
result="blur"
|
||||
stdDeviation="5"
|
||||
in="composite1" />
|
||||
<feOffset
|
||||
id="feOffset1273"
|
||||
result="offset"
|
||||
dy="5"
|
||||
dx="-2.5" />
|
||||
<feComposite
|
||||
id="feComposite1275"
|
||||
result="composite2"
|
||||
operator="over"
|
||||
in2="offset"
|
||||
in="SourceGraphic" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-height="1387"
|
||||
inkscape:window-width="1280"
|
||||
units="px"
|
||||
showgrid="false"
|
||||
inkscape:document-rotation="0"
|
||||
inkscape:current-layer="layer2"
|
||||
inkscape:document-units="px"
|
||||
inkscape:cy="404.46507"
|
||||
inkscape:cx="442.29528"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
borderopacity="1.0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
id="base"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 2"
|
||||
id="layer2"
|
||||
inkscape:groupmode="layer" />
|
||||
<g
|
||||
style="display:none"
|
||||
id="layer1"
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="Layer 1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z"
|
||||
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
|
||||
id="path28" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1 copy"
|
||||
inkscape:groupmode="layer"
|
||||
id="g1343">
|
||||
<path
|
||||
id="path1341"
|
||||
style="fill:#75e04e;fill-opacity:1;stroke:#74e04d;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 7.9 KiB |
|
@ -0,0 +1,162 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
sodipodi:docname="Tusker.svg"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25)"
|
||||
inkscape:export-ydpi="98.304001"
|
||||
inkscape:export-xdpi="98.304001"
|
||||
inkscape:export-filename="../Desktop/1024x1024@1x.png"
|
||||
id="svg8"
|
||||
version="1.1"
|
||||
viewBox="0 0 264.58333 264.58333"
|
||||
height="1000"
|
||||
width="1000"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<defs
|
||||
id="defs2">
|
||||
<inkscape:path-effect
|
||||
bendpath1-nodetypes="cc"
|
||||
bendpath4="M 27.271345,85.808468 V 178.94843"
|
||||
bendpath3="M 27.271345,178.94843 H 242.39013"
|
||||
bendpath2="M 242.39013,85.808468 V 178.94843"
|
||||
bendpath1="M 26.897168,85.995557 242.39013,85.808468"
|
||||
xx="true"
|
||||
yy="true"
|
||||
lpeversion="1"
|
||||
is_visible="true"
|
||||
id="path-effect1345"
|
||||
effect="envelope" />
|
||||
<inkscape:path-effect
|
||||
allow_transforms="true"
|
||||
css_properties=""
|
||||
attributes=""
|
||||
method="d"
|
||||
linkeditem=""
|
||||
lpeversion="1"
|
||||
is_visible="true"
|
||||
id="path-effect38"
|
||||
effect="clone_original" />
|
||||
<inkscape:path-effect
|
||||
scale_y_rel="false"
|
||||
prop_scale="1"
|
||||
strokepath="M0,0 L1,0"
|
||||
endpoint_spacing_variation="0;1"
|
||||
endpoint_edge_variation="0;1"
|
||||
startpoint_spacing_variation="0;1"
|
||||
startpoint_edge_variation="0;1"
|
||||
count="5"
|
||||
lpeversion="1"
|
||||
is_visible="true"
|
||||
id="path-effect32"
|
||||
effect="curvestitching" />
|
||||
<filter
|
||||
height="1.317445"
|
||||
width="1.1258237"
|
||||
id="filter1277"
|
||||
inkscape:label="Drop Shadow"
|
||||
style="color-interpolation-filters:sRGB;"
|
||||
x="-0.068723437"
|
||||
y="-0.1318855">
|
||||
<feFlood
|
||||
id="feFlood1267"
|
||||
result="flood"
|
||||
flood-color="rgb(0,0,0)"
|
||||
flood-opacity="0.321569" />
|
||||
<feComposite
|
||||
id="feComposite1269"
|
||||
result="composite1"
|
||||
operator="in"
|
||||
in2="SourceGraphic"
|
||||
in="flood" />
|
||||
<feGaussianBlur
|
||||
id="feGaussianBlur1271"
|
||||
result="blur"
|
||||
stdDeviation="5"
|
||||
in="composite1" />
|
||||
<feOffset
|
||||
id="feOffset1273"
|
||||
result="offset"
|
||||
dy="5"
|
||||
dx="-2.5" />
|
||||
<feComposite
|
||||
id="feComposite1275"
|
||||
result="composite2"
|
||||
operator="over"
|
||||
in2="offset"
|
||||
in="SourceGraphic" />
|
||||
</filter>
|
||||
</defs>
|
||||
<sodipodi:namedview
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-x="1280"
|
||||
inkscape:window-height="1387"
|
||||
inkscape:window-width="1280"
|
||||
units="px"
|
||||
showgrid="false"
|
||||
inkscape:document-rotation="0"
|
||||
inkscape:current-layer="layer2"
|
||||
inkscape:document-units="px"
|
||||
inkscape:cy="496.38895"
|
||||
inkscape:cx="442.29528"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
borderopacity="1.0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff"
|
||||
id="base"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1" />
|
||||
<metadata
|
||||
id="metadata5">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title />
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<g
|
||||
inkscape:label="Layer 2"
|
||||
id="layer2"
|
||||
inkscape:groupmode="layer">
|
||||
<rect
|
||||
y="-0.14500916"
|
||||
x="-0.14500916"
|
||||
height="264.87335"
|
||||
width="264.87335"
|
||||
id="rect865"
|
||||
style="fill:#75e04e;fill-opacity:1;stroke:#75e04e;stroke-width:0.239149;stroke-opacity:1" />
|
||||
</g>
|
||||
<g
|
||||
style="display:none"
|
||||
id="layer1"
|
||||
inkscape:groupmode="layer"
|
||||
inkscape:label="Layer 1">
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z"
|
||||
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.565786px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
|
||||
id="path28" />
|
||||
</g>
|
||||
<g
|
||||
inkscape:label="Layer 1 copy"
|
||||
inkscape:groupmode="layer"
|
||||
id="g1343">
|
||||
<path
|
||||
id="path1341"
|
||||
style="fill:#f9f7f3;fill-opacity:1;stroke:#d0c1a2;stroke-width:0.56578600000000001px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;filter:url(#filter1277)"
|
||||
d="m 63.595661,96.781172 c 2.610557,8.549728 11.109144,14.261728 18.221441,19.677268 14.285995,10.87784 30.777538,19.16253 47.836068,24.76819 12.08516,3.97134 24.9714,5.89737 37.69211,5.9756 11.75058,0.0723 23.533,-2.04773 34.88282,-5.09124 8.49997,-2.27931 17.13306,-4.99674 24.52676,-9.76937 5.59427,-3.6111 11.35542,-7.93448 14.37737,-13.86775 0.73693,-1.44688 2.00968,-3.90176 0.67356,-4.82442 -4.90929,-3.39011 -10.18592,6.31619 -15.70026,8.59349 -8.68681,3.58745 -17.81526,6.26681 -27.07782,7.85916 -11.94219,2.05301 -24.25018,3.46797 -36.29097,2.10779 -12.7013,-1.4348 -25.14557,-5.50493 -36.82103,-10.70737 -8.48127,-3.77914 -16.22058,-9.14294 -23.66896,-14.68689 C 96.49438,102.53405 91.950513,96.75601 86.13513,92.560411 82.533585,89.96202 79.028923,86.323649 74.610572,85.8754 c -3.438589,-0.34885 -7.602338,0.653715 -9.831133,3.295317 -1.655568,1.962204 -1.933509,5.155042 -1.183778,7.610455 z m -36.276154,1.911751 c 1.000129,9.935377 9.068818,18.042637 15.683118,25.523487 13.285704,15.02628 29.55205,27.69127 47.022482,37.54435 12.376983,6.98044 26.073563,11.90937 39.996973,14.7475 13.12015,2.67439 26.76072,2.8433 40.12178,1.96426 10.02366,-0.65947 20.39718,-1.4876 29.64741,-5.40445 6.55654,-2.77625 13.10939,-6.72368 17.37506,-12.42454 1.08663,-1.45223 3.06381,-3.85048 1.78759,-5.13927 -4.73249,-4.77911 -12.53753,5.06785 -19.12154,6.4416 -10.27704,2.1443 -20.88256,2.99026 -31.37744,2.71972 -13.53101,-0.3488 -27.32398,-1.47627 -40.22043,-5.58617 -13.6039,-4.33535 -26.35283,-11.50217 -38.013078,-19.74231 -8.470214,-5.98579 -15.782756,-13.54635 -22.737346,-21.24101 -5.371021,-5.94258 -9.092383,-13.26181 -14.551151,-19.123884 -3.38068,-3.630455 -6.428954,-8.379273 -11.172348,-9.831658 -3.691559,-1.130322 -8.47165,-0.937751 -11.488322,1.47159 -2.240814,1.789682 -3.239987,5.227417 -2.952758,8.080785 z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 8.2 KiB |
|
@ -1,3 +1,18 @@
|
|||
## 2024.4
|
||||
This release introduces support for iOS 18, including a new sidebar/tab bar on iPad, as well as bugfixes and improvements.
|
||||
|
||||
Features/Improvements:
|
||||
- Import image description when adding attachments from Photos if possible
|
||||
- iPadOS 18: New floating sidebar/tab bar
|
||||
|
||||
Bugfixes:
|
||||
- Fix crash when viewing profiles in certain circumstances
|
||||
- Fix video controls in attachment gallery not auto-hiding
|
||||
- Fix crash if hashtag search results includes duplicates
|
||||
- Fix "no content" text not being removed from list timeline after refreshing
|
||||
- macOS: Fix video controls overlay being positioned incorrectly when Reduce Motion is on
|
||||
- macOS: Fix reselecting current item not navigating back
|
||||
|
||||
## 2024.3
|
||||
This update includes a number of bugfixes and performance improvements. See below for a list of fixes.
|
||||
|
||||
|
|
15
CHANGELOG.md
|
@ -1,5 +1,20 @@
|
|||
# Changelog
|
||||
|
||||
## 2024.4 (136)
|
||||
Features/Improvements:
|
||||
- Import image description when adding attachments from Photos if possible
|
||||
- Reorganize toolbar buttons when adding saved hashtag
|
||||
- Show errors when loading video in attachment gallery fails
|
||||
|
||||
Bugfixes:
|
||||
- Fix crash when viewing profiles in certain circumstances
|
||||
- Fix profile tab switching animation getting stuck
|
||||
- Fix video controls in attachment gallery not auto-hiding
|
||||
- Pleroma: Fix error when loading polls in some circumstances
|
||||
- iPadOS 18: Fix incorrect two-column layout when closing sidebar
|
||||
- macOS: Fix video controls overlay being positioned incorrectly when Reduce Motion is on
|
||||
- macOS: Fix reselecting current item not navigating back
|
||||
|
||||
## 2024.4 (135)
|
||||
Features/Improvements:
|
||||
- iOS 18: New floating sidebar/tab bar
|
||||
|
|
|
@ -15,9 +15,13 @@ import Pachyderm
|
|||
import Intents
|
||||
import HTMLStreamer
|
||||
import WebURL
|
||||
import UIKit
|
||||
import TuskerPreferences
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "NotificationService")
|
||||
|
||||
private let emojiRegex = try! NSRegularExpression(pattern: ":(\\w+):", options: [])
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
private static let textConverter = TextConverter(configuration: .init(insertNewlines: false), callbacks: HTMLCallbacks.self)
|
||||
|
@ -225,8 +229,33 @@ class NotificationService: UNNotificationServiceExtension {
|
|||
}
|
||||
|
||||
let updatedContent: UNMutableNotificationContent
|
||||
|
||||
let contentProviding: any UNNotificationContentProviding
|
||||
if #available(iOS 18.0, visionOS 2.0, *),
|
||||
await Preferences.shared.hasFeatureFlag(.pushNotifCustomEmoji) {
|
||||
let attributedString = NSMutableAttributedString(string: content.body)
|
||||
|
||||
for match in emojiRegex.matches(in: content.body, range: NSRange(location: 0, length: content.body.utf16.count)).reversed() {
|
||||
let emojiName = (content.body as NSString).substring(with: match.range(at: 1))
|
||||
guard let emoji = notification.status?.emojis.first(where: { $0.shortcode == emojiName }),
|
||||
let url = URL(emoji.url),
|
||||
let (data, _) = try? await URLSession.shared.data(from: url),
|
||||
let image = UIImage(data: data) else {
|
||||
continue
|
||||
}
|
||||
let attachment = NSTextAttachment(image: image)
|
||||
let attachmentStr = NSAttributedString(attachment: attachment)
|
||||
attributedString.replaceCharacters(in: match.range, with: attachmentStr)
|
||||
}
|
||||
|
||||
let attributedCtx = UNNotificationAttributedMessageContext(sendMessageIntent: intent, attributedContent: attributedString)
|
||||
contentProviding = attributedCtx
|
||||
} else {
|
||||
contentProviding = intent
|
||||
}
|
||||
|
||||
do {
|
||||
let newContent = try content.updating(from: intent)
|
||||
let newContent = try content.updating(from: contentProviding)
|
||||
if let newMutableContent = newContent.mutableCopy() as? UNMutableNotificationContent {
|
||||
pendingRequest?.0 = newMutableContent
|
||||
updatedContent = newMutableContent
|
||||
|
|
|
@ -167,11 +167,23 @@ extension DraftAttachment: NSItemProviderReading {
|
|||
type = .png
|
||||
}
|
||||
|
||||
// Read the caption from the image itself, if there is one.
|
||||
let caption: String
|
||||
if let source = CGImageSourceCreateWithData(data as CFData, [kCGImageSourceTypeIdentifierHint: typeIdentifier as CFString] as CFDictionary),
|
||||
let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any],
|
||||
// This is the dictionary for TIFF properties, but it's present for other image types too
|
||||
let tiffProperties = properties[kCGImagePropertyTIFFDictionary as String] as? [String: Any],
|
||||
let imageDescription = tiffProperties[kCGImagePropertyTIFFImageDescription as String] as? String {
|
||||
caption = imageDescription
|
||||
} else {
|
||||
caption = ""
|
||||
}
|
||||
|
||||
let attachment = DraftAttachment(entity: DraftsPersistentContainer.shared.persistentStoreCoordinator.managedObjectModel.entitiesByName["DraftAttachment"]!, insertInto: nil)
|
||||
attachment.id = UUID()
|
||||
attachment.fileURL = try writeDataToFile(data, id: attachment.id, type: type)
|
||||
attachment.fileType = type.identifier
|
||||
attachment.attachmentDescription = ""
|
||||
attachment.attachmentDescription = caption
|
||||
return attachment
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ public protocol GalleryContentViewController: UIViewController {
|
|||
var bottomControlsAccessoryViewController: UIViewController? { get }
|
||||
var canAnimateFromSourceView: Bool { get }
|
||||
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool)
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool)
|
||||
func galleryContentDidAppear()
|
||||
func galleryContentWillDisappear()
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ public extension GalleryContentViewController {
|
|||
true
|
||||
}
|
||||
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||
}
|
||||
|
||||
func galleryContentDidAppear() {
|
||||
|
|
|
@ -106,7 +106,7 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
|
|||
content.view.frame = sourceFrameInContainer
|
||||
content.view.layer.opacity = 0
|
||||
|
||||
itemViewController.setControlsVisible(false, animated: false)
|
||||
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
||||
}
|
||||
|
||||
animator.addCompletion { _ in
|
||||
|
|
|
@ -42,7 +42,7 @@ class GalleryDismissInteraction: NSObject {
|
|||
|
||||
origControlsVisible = viewController.currentItemViewController.controlsVisible
|
||||
if origControlsVisible! {
|
||||
viewController.currentItemViewController.setControlsVisible(false, animated: true)
|
||||
viewController.currentItemViewController.setControlsVisible(false, animated: true, dueToUserInteraction: false)
|
||||
}
|
||||
|
||||
case .changed:
|
||||
|
|
|
@ -81,10 +81,10 @@ class GalleryItemViewController: UIViewController {
|
|||
overlayVC.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(overlayVC.view)
|
||||
NSLayoutConstraint.activate([
|
||||
overlayVC.view.leadingAnchor.constraint(equalTo: content.view.leadingAnchor),
|
||||
overlayVC.view.trailingAnchor.constraint(equalTo: content.view.trailingAnchor),
|
||||
overlayVC.view.topAnchor.constraint(equalTo: content.view.topAnchor),
|
||||
overlayVC.view.bottomAnchor.constraint(equalTo: content.view.bottomAnchor),
|
||||
overlayVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
overlayVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
overlayVC.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||
overlayVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
|
@ -213,7 +213,7 @@ class GalleryItemViewController: UIViewController {
|
|||
|
||||
updateZoomScale(resetZoom: false)
|
||||
// Ensure the transform is correct if the controls are hidden
|
||||
setControlsVisible(controlsVisible, animated: false)
|
||||
setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false)
|
||||
|
||||
updateTopControlsInsets()
|
||||
}
|
||||
|
@ -229,7 +229,7 @@ class GalleryItemViewController: UIViewController {
|
|||
}
|
||||
centerContent()
|
||||
// Ensure the transform is correct if the controls are hidden and their size changed.
|
||||
setControlsVisible(controlsVisible, animated: false)
|
||||
setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -250,7 +250,7 @@ class GalleryItemViewController: UIViewController {
|
|||
func addContent() {
|
||||
content.loadViewIfNeeded()
|
||||
|
||||
content.setControlsVisible(controlsVisible, animated: false)
|
||||
content.setControlsVisible(controlsVisible, animated: false, dueToUserInteraction: false)
|
||||
|
||||
content.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
if content.parent != self {
|
||||
|
@ -290,7 +290,7 @@ class GalleryItemViewController: UIViewController {
|
|||
content.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||
controlsVisible = visible
|
||||
|
||||
guard let topControlsView,
|
||||
|
@ -301,7 +301,7 @@ class GalleryItemViewController: UIViewController {
|
|||
func updateControlsViews() {
|
||||
topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height)
|
||||
bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height)
|
||||
content.setControlsVisible(visible, animated: animated)
|
||||
content.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
|
||||
}
|
||||
if animated {
|
||||
let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters())
|
||||
|
@ -378,9 +378,6 @@ class GalleryItemViewController: UIViewController {
|
|||
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
|
||||
50, // iPhone 12 mini, 13 mini
|
||||
]
|
||||
let islandDeviceTopInsets: [CGFloat] = [
|
||||
59, // iPhone 14 Pro, 14 Pro Max, 15 Pro, 15 Pro Max
|
||||
]
|
||||
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
||||
// the notch width is not the same for the iPhones 13,
|
||||
// but what we actually want is the same offset from the edges
|
||||
|
@ -390,16 +387,18 @@ class GalleryItemViewController: UIViewController {
|
|||
let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2
|
||||
shareButtonLeadingConstraint.constant = offset
|
||||
closeButtonTrailingConstraint.constant = offset
|
||||
} else if islandDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
||||
shareButtonLeadingConstraint.constant = 24
|
||||
shareButtonTopConstraint.constant = 24
|
||||
closeButtonTrailingConstraint.constant = 24
|
||||
closeButtonTopConstraint.constant = 24
|
||||
} else {
|
||||
} else if view.safeAreaInsets.top == 0 {
|
||||
// square corner devices
|
||||
shareButtonLeadingConstraint.constant = 8
|
||||
shareButtonTopConstraint.constant = 8
|
||||
closeButtonTrailingConstraint.constant = 8
|
||||
closeButtonTopConstraint.constant = 8
|
||||
} else {
|
||||
// dynamic island devices
|
||||
shareButtonLeadingConstraint.constant = 24
|
||||
shareButtonTopConstraint.constant = 24
|
||||
closeButtonTrailingConstraint.constant = 24
|
||||
closeButtonTopConstraint.constant = 24
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -429,7 +428,7 @@ class GalleryItemViewController: UIViewController {
|
|||
scrollView.zoomScale > scrollView.minimumZoomScale {
|
||||
animateZoomOut()
|
||||
} else {
|
||||
setControlsVisible(!controlsVisible, animated: true)
|
||||
setControlsVisible(!controlsVisible, animated: true, dueToUserInteraction: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -531,7 +530,7 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer {
|
|||
}
|
||||
|
||||
func setGalleryControlsVisible(_ visible: Bool, animated: Bool) {
|
||||
setControlsVisible(visible, animated: animated)
|
||||
setControlsVisible(visible, animated: animated, dueToUserInteraction: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -546,9 +545,9 @@ extension GalleryItemViewController: UIScrollViewDelegate {
|
|||
|
||||
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
||||
setControlsVisible(true, animated: true)
|
||||
setControlsVisible(true, animated: true, dueToUserInteraction: true)
|
||||
} else {
|
||||
setControlsVisible(false, animated: true)
|
||||
setControlsVisible(false, animated: true, dueToUserInteraction: true)
|
||||
}
|
||||
|
||||
centerContent()
|
||||
|
|
|
@ -75,7 +75,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
|||
container.layoutIfNeeded()
|
||||
|
||||
// This needs to take place after the layout, so that the transform is correct.
|
||||
itemViewController.setControlsVisible(false, animated: false)
|
||||
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
||||
|
||||
let duration = self.transitionDuration(using: transitionContext)
|
||||
// rougly equivalent to duration: 0.35, bounce: 0.3
|
||||
|
@ -90,7 +90,7 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
|||
content.view.frame = destFrameInContainer
|
||||
content.view.layer.opacity = 1
|
||||
|
||||
itemViewController.setControlsVisible(true, animated: false)
|
||||
itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false)
|
||||
|
||||
if let sourceToDestTransform {
|
||||
self.sourceView.transform = sourceToDestTransform
|
||||
|
|
|
@ -126,7 +126,7 @@ extension GalleryViewController: UIPageViewControllerDelegate {
|
|||
public func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
||||
currentItemViewController.content.galleryContentWillDisappear()
|
||||
let new = pendingViewControllers[0] as! GalleryItemViewController
|
||||
new.setControlsVisible(currentItemViewController.controlsVisible, animated: false)
|
||||
new.setControlsVisible(currentItemViewController.controlsVisible, animated: false, dueToUserInteraction: false)
|
||||
}
|
||||
|
||||
public func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) {
|
||||
|
|
|
@ -16,7 +16,7 @@ public struct Poll: Codable, Sendable {
|
|||
public let votesCount: Int
|
||||
public let votersCount: Int?
|
||||
public let voted: Bool?
|
||||
public let ownVotes: [Int]?
|
||||
public let ownVotes: [Int?]?
|
||||
public let options: [Option]
|
||||
public let emojis: [Emoji]
|
||||
|
||||
|
@ -33,7 +33,7 @@ public struct Poll: Codable, Sendable {
|
|||
self.votesCount = try container.decode(Int.self, forKey: .votesCount)
|
||||
self.votersCount = try container.decodeIfPresent(Int.self, forKey: .votersCount)
|
||||
self.voted = try container.decodeIfPresent(Bool.self, forKey: .voted)
|
||||
self.ownVotes = try container.decodeIfPresent([Int].self, forKey: .ownVotes)
|
||||
self.ownVotes = try container.decodeIfPresent([Int?].self, forKey: .ownVotes)
|
||||
self.options = try container.decode([Poll.Option].self, forKey: .options)
|
||||
self.emojis = try container.decodeIfPresent([Emoji].self, forKey: .emojis) ?? []
|
||||
}
|
||||
|
|
|
@ -9,4 +9,5 @@ import Foundation
|
|||
|
||||
public enum FeatureFlag: String, Codable {
|
||||
case iPadBrowserNavigation = "ipad-browser-navigation"
|
||||
case pushNotifCustomEmoji = "push-notif-custom-emoji"
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@
|
|||
D620483823D38190008A63EF /* StatusContentTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D620483723D38190008A63EF /* StatusContentTextView.swift */; };
|
||||
D6210D762C0B924F009BB569 /* RemoveProfileSuggestionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6210D752C0B924F009BB569 /* RemoveProfileSuggestionService.swift */; };
|
||||
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D621733228F1D5ED004C7DB1 /* ReblogService.swift */; };
|
||||
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */ = {isa = PBXBuildFile; productRef = D62220462C7EA8DF003E43B7 /* TuskerPreferences */; };
|
||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62275A724F1CA2800B82A16 /* ComposeReplyContentView.swift */; };
|
||||
D623A53D2635F5590095BD04 /* StatusPollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A53C2635F5590095BD04 /* StatusPollView.swift */; };
|
||||
D623A5412635FB3C0095BD04 /* PollOptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D623A5402635FB3C0095BD04 /* PollOptionView.swift */; };
|
||||
|
@ -165,7 +166,6 @@
|
|||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
|
||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
|
||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; };
|
||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
|
||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
||||
D674A50927F9128D00BA03AC /* Pachyderm in Frameworks */ = {isa = PBXBuildFile; productRef = D674A50827F9128D00BA03AC /* Pachyderm */; };
|
||||
|
@ -599,7 +599,6 @@
|
|||
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
|
||||
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
|
||||
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
|
||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
||||
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
||||
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
|
@ -830,6 +829,7 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D630C4252BC7845800208903 /* WebURL in Frameworks */,
|
||||
D62220472C7EA8DF003E43B7 /* TuskerPreferences in Frameworks */,
|
||||
D630C4232BC7842C00208903 /* HTMLStreamer in Frameworks */,
|
||||
D630C3E52BC6313400208903 /* Pachyderm in Frameworks */,
|
||||
D630C3DF2BC61C4900208903 /* PushNotifications in Frameworks */,
|
||||
|
@ -1324,7 +1324,6 @@
|
|||
D667E5F62135C2ED0057A976 /* Extensions */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */,
|
||||
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */,
|
||||
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */,
|
||||
D6333B362137838300CE884A /* AttributedString+Helpers.swift */,
|
||||
|
@ -1798,6 +1797,7 @@
|
|||
D630C3E42BC6313400208903 /* Pachyderm */,
|
||||
D630C4222BC7842C00208903 /* HTMLStreamer */,
|
||||
D630C4242BC7845800208903 /* WebURL */,
|
||||
D62220462C7EA8DF003E43B7 /* TuskerPreferences */,
|
||||
);
|
||||
productName = NotificationExtension;
|
||||
productReference = D630C3D12BC61B6000208903 /* NotificationExtension.appex */;
|
||||
|
@ -2216,7 +2216,6 @@
|
|||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
||||
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
|
||||
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
|
||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
||||
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
|
||||
|
@ -3303,6 +3302,10 @@
|
|||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Pachyderm;
|
||||
};
|
||||
D62220462C7EA8DF003E43B7 /* TuskerPreferences */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = TuskerPreferences;
|
||||
};
|
||||
D630C3C72BC43AFD00208903 /* PushNotifications */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = PushNotifications;
|
||||
|
|
|
@ -292,12 +292,15 @@ extension AppDelegate: UNUserNotificationCenterDelegate {
|
|||
let rootViewController = delegate.rootViewController {
|
||||
let mastodonController = MastodonController.getForAccount(account)
|
||||
|
||||
// if the scene is already active, then we animate the account switching if necessary
|
||||
delegate.activateAccount(account, animated: scene.activationState == .foregroundActive)
|
||||
// if the scene is already active, then we animate things
|
||||
let animated = scene.activationState == .foregroundActive
|
||||
|
||||
rootViewController.select(route: .notifications, animated: false) {
|
||||
delegate.activateAccount(account, animated: animated)
|
||||
|
||||
rootViewController.runNavigation(animated: animated) { navigation in
|
||||
navigation.select(route: .notifications)
|
||||
let vc = NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController)
|
||||
rootViewController.getNavigationController().pushViewController(vc, animated: false)
|
||||
navigation.push(viewController: vc)
|
||||
}
|
||||
} else {
|
||||
let activity = UserActivityManager.showNotificationActivity(id: notificationID, accountID: accountID)
|
||||
|
|
After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 51 KiB |
After Width: | Height: | Size: 723 B |
After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 580 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 968 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 1022 B |
After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 917 B |
Before Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 1.3 KiB |
After Width: | Height: | Size: 1.9 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.0 KiB |
After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 6.1 KiB |
After Width: | Height: | Size: 2.2 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 6.4 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 2.9 KiB |
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 5.5 KiB |
|
@ -2,109 +2,511 @@
|
|||
"images" : [
|
||||
{
|
||||
"filename" : "20x20@2x.png",
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "20x20@3x.png",
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "29x29@2x.png",
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "29x29@3x.png",
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "38x38@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "38x38"
|
||||
},
|
||||
{
|
||||
"filename" : "38x38@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "38x38"
|
||||
},
|
||||
{
|
||||
"filename" : "40x40@2x.png",
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "40x40@3x.png",
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "60x60@2x.png",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "40x40@3x 1.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "60x60@3x.png",
|
||||
"idiom" : "iphone",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"filename" : "20x20@1x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"filename" : "20x20@2x-1.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "64x64@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
"size" : "64x64"
|
||||
},
|
||||
{
|
||||
"filename" : "29x29@1x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
"filename" : "64x64@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "64x64"
|
||||
},
|
||||
{
|
||||
"filename" : "29x29@2x-1.png",
|
||||
"idiom" : "ipad",
|
||||
"filename" : "68x68@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"filename" : "40x40@1x.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"filename" : "40x40@2x-1.png",
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
"size" : "68x68"
|
||||
},
|
||||
{
|
||||
"filename" : "76x76@2x.png",
|
||||
"idiom" : "ipad",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"filename" : "83.5x83.5@2x.png",
|
||||
"idiom" : "ipad",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"filename" : "1024x1024@1x.png",
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "20x20-dark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "20x20-dark@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "29x29-dark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "29x29-dark@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "38x38-dark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "38x38"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "38x38-dark@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "38x38"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "40x40-dark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "40x40-dark@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "60x60-dark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "60x60-dark@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "64x64-dark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "64x64"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "64x64-dark@3x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "64x64"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "68x68-dark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "68x68"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "76x76-dark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "83.5x83.5-dark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "1024x1024-dark@1x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "38x38"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "38x38"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "64x64"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "3x",
|
||||
"size" : "64x64"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "68x68"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "tinted"
|
||||
}
|
||||
],
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
|
|
|
@ -19,7 +19,7 @@ import UserAccounts
|
|||
|
||||
fileprivate let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "PersistentStore")
|
||||
|
||||
class MastodonCachePersistentStore: NSPersistentCloudKitContainer {
|
||||
class MastodonCachePersistentStore: NSPersistentCloudKitContainer, @unchecked Sendable {
|
||||
|
||||
private let accountInfo: UserAccountInfo?
|
||||
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
//
|
||||
// Status+Equatable.swift
|
||||
// Tusker
|
||||
//
|
||||
// Created by Shadowfacts on 8/28/18.
|
||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
||||
//
|
||||
|
||||
import Pachyderm
|
||||
|
||||
extension Status: Equatable {
|
||||
public static func ==(lhs: Status, rhs: Status) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
}
|
||||
|
||||
extension Account: Equatable {
|
||||
public static func ==(lhs: Account, rhs: Account) -> Bool {
|
||||
return lhs.id == rhs.id
|
||||
}
|
||||
}
|
|
@ -83,9 +83,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
} else {
|
||||
context = ActiveAccountUserActivityHandlingContext(isHandoff: true, root: rootViewController!)
|
||||
}
|
||||
Task(priority: .userInitiated) {
|
||||
_ = await userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
||||
}
|
||||
_ = userActivity.handleResume(manager: UserActivityManager(scene: scene as! UIWindowScene, context: context))
|
||||
}
|
||||
|
||||
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||
|
@ -193,10 +191,8 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate, TuskerSceneDelegate
|
|||
|
||||
if let activity = launchActivity {
|
||||
func doRestoreActivity(context: UserActivityHandlingContext) {
|
||||
Task(priority: .userInitiated) {
|
||||
_ = await activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
|
||||
context.finalize(activity: activity)
|
||||
}
|
||||
_ = activity.handleResume(manager: UserActivityManager(scene: window!.windowScene!, context: context))
|
||||
context.finalize(activity: activity)
|
||||
}
|
||||
if activity.isStateRestorationActivity {
|
||||
doRestoreActivity(context: StateRestorationUserActivityHandlingContext(root: rootViewController!))
|
||||
|
|
|
@ -88,7 +88,7 @@ struct AnnouncementListRow: View {
|
|||
Button(role: .destructive) {
|
||||
Task {
|
||||
await dismissAnnouncement()
|
||||
await removeAnnouncement()
|
||||
removeAnnouncement()
|
||||
}
|
||||
} label: {
|
||||
Label("Dismiss", systemImage: "xmark")
|
||||
|
|
|
@ -9,14 +9,14 @@
|
|||
import UIKit
|
||||
import Pachyderm
|
||||
|
||||
class AddSavedHashtagViewController: UIViewController {
|
||||
class AddSavedHashtagViewController: UIViewController, CollectionViewController {
|
||||
|
||||
weak var mastodonController: MastodonController!
|
||||
|
||||
var resultsController: SearchResultsViewController!
|
||||
var searchController: UISearchController!
|
||||
|
||||
private var collectionView: UICollectionView!
|
||||
private(set) var collectionView: UICollectionView!
|
||||
private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
|
||||
|
||||
init(mastodonController: MastodonController) {
|
||||
|
@ -91,6 +91,12 @@ class AddSavedHashtagViewController: UIViewController {
|
|||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
if searchController.isActive {
|
||||
resultsController.clearSelectionOnAppear(animated: animated)
|
||||
}
|
||||
|
||||
clearSelectionOnAppear(animated: animated)
|
||||
|
||||
let request = Client.getTrendingHashtags(limit: 10)
|
||||
mastodonController.run(request) { (response) in
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
|
||||
|
@ -108,7 +114,63 @@ class AddSavedHashtagViewController: UIViewController {
|
|||
}
|
||||
|
||||
private func selectHashtag(_ hashtag: Hashtag) {
|
||||
show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
|
||||
let vc = HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController)
|
||||
vc.loadViewIfNeeded()
|
||||
|
||||
let mastodonController = mastodonController!
|
||||
let context = mastodonController.persistentContainer.viewContext
|
||||
let existingSaved = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)).first
|
||||
let saveItem = UIBarButtonItem()
|
||||
func updateSaveItem(saved: Bool) {
|
||||
saveItem.title = saved ? "Unsave Hashag" : "Save Hashtag"
|
||||
saveItem.image = UIImage(systemName: saved ? "minus" : "plus")
|
||||
}
|
||||
saveItem.primaryAction = UIAction(handler: { [unowned self] _ in
|
||||
// re-fetch this in case the button's been tapped before and the captured var would be out of date
|
||||
let existingSaved = try? context.fetch(SavedHashtag.fetchRequest(name: hashtag.name, account: mastodonController.accountInfo!)).first
|
||||
if let existingSaved {
|
||||
context.delete(existingSaved)
|
||||
} else {
|
||||
_ = SavedHashtag(hashtag: hashtag, account: mastodonController.accountInfo!, context: context)
|
||||
}
|
||||
mastodonController.persistentContainer.save(context: context)
|
||||
updateSaveItem(saved: existingSaved == nil)
|
||||
if existingSaved == nil {
|
||||
self.presentingViewController?.dismiss(animated: true)
|
||||
}
|
||||
})
|
||||
// setting primaryAction replace's the bar button's title/image with the action, so do this after
|
||||
updateSaveItem(saved: existingSaved != nil)
|
||||
|
||||
vc.navigationItem.rightBarButtonItems = [
|
||||
saveItem,
|
||||
]
|
||||
|
||||
if mastodonController.instanceFeatures.canFollowHashtags {
|
||||
let existingFollowed = mastodonController.followedHashtags.first(where: { $0.name.lowercased() == hashtag.name })
|
||||
let followItem = UIBarButtonItem()
|
||||
func updateFollowItem(followed: Bool) {
|
||||
followItem.title = followed ? "Unfollow Hashtag" : "Follow Hashtag"
|
||||
followItem.image = UIImage(systemName: "person.badge.\(followed ? "minus" : "plus")")
|
||||
}
|
||||
followItem.primaryAction = UIAction(handler: { [unowned self] _ in
|
||||
Task {
|
||||
let success = await ToggleFollowHashtagService(hashtagName: hashtag.name, presenter: self).toggleFollow()
|
||||
if success {
|
||||
let followed = mastodonController.followedHashtags.contains(where: { $0.name.lowercased() == hashtag.name })
|
||||
updateFollowItem(followed: followed)
|
||||
if followed {
|
||||
self.presentingViewController?.dismiss(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
updateFollowItem(followed: existingFollowed != nil)
|
||||
|
||||
vc.navigationItem.rightBarButtonItems!.append(followItem)
|
||||
}
|
||||
|
||||
show(vc, sender: self)
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
@ -144,3 +206,7 @@ extension AddSavedHashtagViewController: SearchResultsViewControllerDelegate {
|
|||
selectHashtag(hashtag)
|
||||
}
|
||||
}
|
||||
|
||||
extension AddSavedHashtagViewController: TuskerNavigationDelegate {
|
||||
var apiController: MastodonController! { mastodonController }
|
||||
}
|
||||
|
|
|
@ -96,7 +96,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
|||
// so we manually propagate this down to the results controller
|
||||
// so that it can deselect on appear
|
||||
if searchController.isActive {
|
||||
resultsController.viewWillAppear(animated)
|
||||
resultsController.clearSelectionOnAppear(animated: animated)
|
||||
}
|
||||
|
||||
clearSelectionOnAppear(animated: animated)
|
||||
|
@ -308,6 +308,7 @@ class ExploreViewController: UIViewController, UICollectionViewDelegate, Collect
|
|||
actions.append(UIContextualAction(style: .destructive, title: "Unsave", handler: { _, _, completion in
|
||||
context.delete(existing)
|
||||
try! context.save()
|
||||
completion(true)
|
||||
}))
|
||||
}
|
||||
if mastodonController.instanceFeatures.canFollowHashtags,
|
||||
|
|
|
@ -128,7 +128,7 @@ class ImageGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
}
|
||||
}
|
||||
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||
if #available(iOS 16.0, macCatalyst 17.0, *),
|
||||
let analysisInteraction {
|
||||
analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated)
|
||||
|
|
|
@ -52,7 +52,7 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC
|
|||
if let wrapped = await provider() {
|
||||
self.wrapped = wrapped
|
||||
wrapped.container = container
|
||||
wrapped.setControlsVisible(container?.galleryControlsVisible ?? false, animated: false)
|
||||
wrapped.setControlsVisible(container?.galleryControlsVisible ?? false, animated: false, dueToUserInteraction: false)
|
||||
|
||||
addChild(wrapped)
|
||||
wrapped.view.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -102,8 +102,8 @@ class LoadingGalleryContentViewController: UIViewController, GalleryContentViewC
|
|||
])
|
||||
}
|
||||
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||
wrapped?.setControlsVisible(visible, animated: animated)
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||
wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
|
||||
}
|
||||
|
||||
func galleryContentDidAppear() {
|
||||
|
|
|
@ -408,7 +408,7 @@ private class MuteButton: UIControl {
|
|||
let image = UIImage(systemName: muted ? "speaker.slash.fill" : "speaker.wave.3.fill")!
|
||||
if animated,
|
||||
#available(iOS 17.0, *) {
|
||||
imageView.setSymbolImage(image, contentTransition: .replace.wholeSymbol, options: .speed(5))
|
||||
imageView.setSymbolImage(image, contentTransition: .replace.byLayer)
|
||||
} else {
|
||||
imageView.image = image
|
||||
}
|
||||
|
|
|
@ -86,17 +86,10 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
|
||||
updateItemObservations()
|
||||
rateObservation = player.observe(\.rate, options: .old, changeHandler: { [unowned self] player, info in
|
||||
hideControlsWorkItem?.cancel()
|
||||
if player.rate > 0 && info.oldValue == 0 {
|
||||
hideControlsWorkItem = DispatchWorkItem { [weak self] in
|
||||
guard let self,
|
||||
let container = self.container,
|
||||
container.galleryControlsVisible else {
|
||||
return
|
||||
}
|
||||
container.setGalleryControlsVisible(false, animated: true)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!)
|
||||
if player.rate == 0 {
|
||||
hideControlsWorkItem?.cancel()
|
||||
} else if player.rate > 0 && info.oldValue == 0 {
|
||||
scheduleControlsHide()
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -114,12 +107,52 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
MainActor.runUnsafely {
|
||||
if item.status == .readyToPlay {
|
||||
self.container?.setGalleryContentLoading(false)
|
||||
statusObservation = nil
|
||||
self.statusObservation = nil
|
||||
} else if item.status == .failed,
|
||||
let error = item.error {
|
||||
self.container?.setGalleryContentLoading(false)
|
||||
self.showErrorView(error)
|
||||
self.statusObservation = nil
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func showErrorView(_ error: any Error) {
|
||||
let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
|
||||
image.tintColor = .secondaryLabel
|
||||
image.contentMode = .scaleAspectFit
|
||||
|
||||
let label = UILabel()
|
||||
label.text = "Error Loading"
|
||||
label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||
label.textColor = .secondaryLabel
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
|
||||
let reason = UILabel()
|
||||
reason.text = error.localizedDescription
|
||||
reason.font = .preferredFont(forTextStyle: .subheadline)
|
||||
reason.textColor = .secondaryLabel
|
||||
reason.adjustsFontForContentSizeCategory = true
|
||||
|
||||
let stackView = UIStackView(arrangedSubviews: [
|
||||
image,
|
||||
label,
|
||||
reason,
|
||||
])
|
||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .center
|
||||
stackView.spacing = 8
|
||||
view.addSubview(stackView)
|
||||
NSLayoutConstraint.activate([
|
||||
image.widthAnchor.constraint(equalToConstant: 64),
|
||||
image.heightAnchor.constraint(equalToConstant: 64),
|
||||
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||
])
|
||||
}
|
||||
|
||||
@objc private func preferencesChanged() {
|
||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||
let isPlaying = player.rate > 0
|
||||
|
@ -137,6 +170,20 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
}
|
||||
}
|
||||
|
||||
private func scheduleControlsHide() {
|
||||
hideControlsWorkItem = DispatchWorkItem { [weak self] in
|
||||
MainActor.runUnsafely {
|
||||
guard let self,
|
||||
let container = self.container,
|
||||
container.galleryControlsVisible else {
|
||||
return
|
||||
}
|
||||
container.setGalleryControlsVisible(false, animated: true)
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!)
|
||||
}
|
||||
|
||||
// MARK: GalleryContentViewController
|
||||
|
||||
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||
|
@ -164,9 +211,15 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
|||
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||
#endif
|
||||
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||
overlayVC.setVisible(visible)
|
||||
hideControlsWorkItem?.cancel()
|
||||
|
||||
if !visible {
|
||||
hideControlsWorkItem?.cancel()
|
||||
} else if dueToUserInteraction,
|
||||
player.rate > 0 {
|
||||
scheduleControlsHide()
|
||||
}
|
||||
}
|
||||
|
||||
func galleryContentDidAppear() {
|
||||
|
|
|
@ -79,12 +79,10 @@ class AccountSwitchingContainerViewController: UIViewController {
|
|||
stateRestorationLogger.debug("AccountSwitchingContainer: reusing existing VC for \(account.id, privacy: .public)")
|
||||
} else {
|
||||
newRoot = newRootProvider()
|
||||
Task(priority: .userInitiated) {
|
||||
stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)")
|
||||
let context = StateRestorationUserActivityHandlingContext(root: newRoot)
|
||||
_ = await activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context))
|
||||
context.finalize(activity: activity)
|
||||
}
|
||||
stateRestorationLogger.debug("AccountSwitchingContainer: restoring \(activity.activityType, privacy: .public) for \(account.id, privacy: .public)")
|
||||
let context = StateRestorationUserActivityHandlingContext(root: newRoot)
|
||||
_ = activity.handleResume(manager: UserActivityManager(scene: view.window!.windowScene!, context: context))
|
||||
context.finalize(activity: activity)
|
||||
}
|
||||
} else {
|
||||
newRoot = newRootProvider()
|
||||
|
|
|
@ -184,10 +184,8 @@ extension BaseMainTabBarViewController: BackgroundableViewController {
|
|||
|
||||
extension BaseMainTabBarViewController: StatusBarTappableViewController {
|
||||
func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
|
||||
guard presentedViewController == nil else {
|
||||
return .stop
|
||||
}
|
||||
guard let vc = selectedViewController as? StatusBarTappableViewController else {
|
||||
guard presentedViewController == nil,
|
||||
let vc = selectedViewController as? StatusBarTappableViewController else {
|
||||
return .continue
|
||||
}
|
||||
return vc.handleStatusBarTapped(xPosition: xPosition)
|
||||
|
|
|
@ -15,7 +15,7 @@ protocol MainSidebarViewControllerDelegate: AnyObject {
|
|||
func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item, previousItem: MainSidebarViewController.Item?)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController, previousItem: MainSidebarViewController.Item?)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item)
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didReselectItem item: MainSidebarViewController.Item)
|
||||
}
|
||||
|
||||
class MainSidebarViewController: UIViewController {
|
||||
|
@ -452,7 +452,7 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
|||
}
|
||||
itemLastSelectedTimestamps[item] = Date()
|
||||
if previouslySelectedItem == item {
|
||||
sidebarDelegate?.sidebar(self, scrollToTopFor: item)
|
||||
sidebarDelegate?.sidebar(self, didReselectItem: item)
|
||||
} else if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) {
|
||||
if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) {
|
||||
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
|
||||
|
|
|
@ -333,14 +333,14 @@ extension MainSplitViewController: UISplitViewControllerDelegate {
|
|||
// Transfer the navigation stack, dropping the search VC, to keep anything the user has opened
|
||||
transferNavigationStack(from: .tab(.explore), to: exploreNav, dropFirst: true, append: true)
|
||||
|
||||
tabBarViewController.select(tab: .explore, dismissPresented: false)
|
||||
tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
|
||||
|
||||
case let .tab(tab):
|
||||
// sidebar items that map 1 <-> 1 can be transferred directly
|
||||
tabBarViewController.select(tab: tab, dismissPresented: false)
|
||||
tabBarViewController.select(tab: tab, dismissPresented: false, animated: false)
|
||||
|
||||
case .bookmarks, .favorites, .list(_), .savedHashtag(_), .savedInstance(_):
|
||||
tabBarViewController.select(tab: .explore, dismissPresented: false)
|
||||
tabBarViewController.select(tab: .explore, dismissPresented: false, animated: false)
|
||||
// Make sure the Explore VC doesn't show its search bar when it appears, in case the user was previously
|
||||
// in compact mode and performing a search.
|
||||
let exploreNav = tabBarViewController.viewController(for: .explore) as! UINavigationController
|
||||
|
@ -493,8 +493,12 @@ extension MainSplitViewController: MainSidebarViewControllerDelegate {
|
|||
secondaryNavController.viewControllers = [viewController]
|
||||
}
|
||||
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, scrollToTopFor item: MainSidebarViewController.Item) {
|
||||
(secondaryNavController as? TabBarScrollableViewController)?.tabBarScrollToTop()
|
||||
func sidebar(_ sidebarViewController: MainSidebarViewController, didReselectItem item: MainSidebarViewController.Item) {
|
||||
if secondaryNavController.viewControllers.count == 1 {
|
||||
(secondaryNavController.topViewController as? TabBarScrollableViewController)?.tabBarScrollToTop()
|
||||
} else {
|
||||
secondaryNavController.popToRootViewController(animated: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,19 +54,22 @@ class MainTabBarViewController: BaseMainTabBarViewController {
|
|||
view.backgroundColor = .appBackground
|
||||
}
|
||||
|
||||
func select(tab: Tab, dismissPresented: Bool) {
|
||||
func select(tab: Tab, dismissPresented: Bool, animated: Bool, completion: (() -> Void)? = nil) {
|
||||
if tab == .compose {
|
||||
compose(editing: nil)
|
||||
compose(editing: nil, completion: completion)
|
||||
} else {
|
||||
// when switching tabs, dismiss the currently presented VC
|
||||
// otherwise the selected tab changes behind the presented VC
|
||||
if presentedViewController != nil && dismissPresented {
|
||||
dismiss(animated: true) {
|
||||
dismiss(animated: animated) {
|
||||
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
|
||||
self.selectedIndex = tab.rawValue
|
||||
completion?()
|
||||
}
|
||||
} else {
|
||||
stateRestorationLogger.info("MainTabBarViewController: selecting \(String(describing: tab), privacy: .public)")
|
||||
selectedIndex = tab.rawValue
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -151,25 +154,24 @@ extension MainTabBarViewController: TuskerRootViewController {
|
|||
func select(route: TuskerRoute, animated: Bool, completion: (() -> Void)?) {
|
||||
switch route {
|
||||
case .timelines:
|
||||
select(tab: .timelines, dismissPresented: true)
|
||||
select(tab: .timelines, dismissPresented: true, animated: animated, completion: completion)
|
||||
case .notifications:
|
||||
select(tab: .notifications, dismissPresented: true)
|
||||
select(tab: .notifications, dismissPresented: true, animated: animated, completion: completion)
|
||||
case .myProfile:
|
||||
select(tab: .myProfile, dismissPresented: true)
|
||||
select(tab: .myProfile, dismissPresented: true, animated: animated, completion: completion)
|
||||
case .explore:
|
||||
select(tab: .explore, dismissPresented: true)
|
||||
select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
|
||||
case .bookmarks:
|
||||
select(tab: .explore, dismissPresented: true)
|
||||
select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
|
||||
getNavigationController().pushViewController(BookmarksViewController(mastodonController: mastodonController), animated: animated)
|
||||
case .list(id: let id):
|
||||
select(tab: .explore, dismissPresented: true)
|
||||
select(tab: .explore, dismissPresented: true, animated: animated, completion: completion)
|
||||
if let list = mastodonController.getCachedList(id: id) {
|
||||
let nav = getNavigationController()
|
||||
_ = nav.popToRootViewController(animated: animated)
|
||||
nav.pushViewController(ListTimelineViewController(for: list, mastodonController: mastodonController), animated: animated)
|
||||
}
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
|
||||
func getNavigationDelegate() -> TuskerNavigationDelegate? {
|
||||
|
@ -186,7 +188,7 @@ extension MainTabBarViewController: TuskerRootViewController {
|
|||
return
|
||||
}
|
||||
|
||||
select(tab: .explore, dismissPresented: true)
|
||||
select(tab: .explore, dismissPresented: true, animated: false)
|
||||
exploreNavController.popToRootViewController(animated: false)
|
||||
|
||||
// setting searchController.isActive directly doesn't work until the view has loaded/appeared for the first time
|
||||
|
|
|
@ -247,7 +247,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
|
|||
|
||||
private func embedInNavigationController(_ vc: UIViewController) -> UIViewController {
|
||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||
return UINavigationController(rootViewController: vc)
|
||||
return EnhancedNavigationViewController(rootViewController: vc)
|
||||
} else {
|
||||
let nav = AdaptableNavigationController()
|
||||
nav.viewControllers = [vc]
|
||||
|
@ -285,7 +285,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
|
|||
var tabs: [UITab] = []
|
||||
let savedReq = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
|
||||
let saved = (try? mastodonController.persistentContainer.viewContext.fetch(savedReq)) ?? []
|
||||
for hashtag in saved {
|
||||
for hashtag in saved where !seenTags.contains(hashtag.name) {
|
||||
seenTags.insert(hashtag.name)
|
||||
tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider))
|
||||
}
|
||||
|
@ -293,6 +293,7 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
|
|||
let followedReq = FollowedHashtag.fetchRequest()
|
||||
let followed = (try? mastodonController.persistentContainer.viewContext.fetch(followedReq)) ?? []
|
||||
for hashtag in followed where !seenTags.contains(hashtag.name) {
|
||||
seenTags.insert(hashtag.name)
|
||||
tabs.append(HashtagTab(hashtagName: hashtag.name, viewControllerProvider: viewControllerProvider))
|
||||
}
|
||||
|
||||
|
@ -313,10 +314,6 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
|
|||
}
|
||||
}
|
||||
|
||||
@objc func handleComposeKeyCommand() {
|
||||
compose(editing: nil)
|
||||
}
|
||||
|
||||
@objc private func sidebarTapped() {
|
||||
#if !os(visionOS)
|
||||
fastAccountSwitcher?.hide()
|
||||
|
@ -407,6 +404,33 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
|
|||
return true
|
||||
}
|
||||
#endif
|
||||
|
||||
// MARK: Keyboard shortcuts
|
||||
|
||||
@objc func handleSidebarCommandTimelines() {
|
||||
selectedTab = homeTab
|
||||
}
|
||||
|
||||
@objc func handleSidebarCommandNotifications() {
|
||||
selectedTab = notificationsTab
|
||||
}
|
||||
|
||||
@objc func handleSidebarCommandExplore() {
|
||||
selectedTab = exploreTab
|
||||
}
|
||||
|
||||
@objc func handleSidebarCommandBookmarks() {
|
||||
selectedTab = bookmarksTab
|
||||
}
|
||||
|
||||
@objc func handleSidebarCommandMyProfile() {
|
||||
selectedTab = myProfileTab
|
||||
}
|
||||
|
||||
@objc func handleComposeKeyCommand() {
|
||||
compose(editing: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@available(iOS 18.0, *)
|
||||
|
@ -441,10 +465,12 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate {
|
|||
return true
|
||||
} else if let selectedTab,
|
||||
selectedTab == tab,
|
||||
let nav = selectedViewController as? any NavigationControllerProtocol,
|
||||
nav.viewControllers.count == 1,
|
||||
let scrollableVC = nav.viewControllers[0] as? TabBarScrollableViewController {
|
||||
scrollableVC.tabBarScrollToTop()
|
||||
let nav = selectedViewController as? any NavigationControllerProtocol {
|
||||
if nav.viewControllers.count == 1 {
|
||||
(nav.viewControllers[0] as? TabBarScrollableViewController)?.tabBarScrollToTop()
|
||||
} else {
|
||||
nav.popToRootViewController(animated: true)
|
||||
}
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
|
|
|
@ -20,6 +20,14 @@ protocol TuskerRootViewController: UIViewController, StateRestorableViewControll
|
|||
func presentPreferences(completion: (() -> Void)?) -> PreferencesNavigationController?
|
||||
}
|
||||
|
||||
extension TuskerRootViewController {
|
||||
func runNavigation(animated: Bool, _ builder: (_ navigation: TuskerNavigationSequence) -> Void) {
|
||||
let sequence = TuskerNavigationSequence(root: self, animated: animated)
|
||||
builder(sequence)
|
||||
sequence.run()
|
||||
}
|
||||
}
|
||||
|
||||
enum TuskerRoute {
|
||||
case timelines
|
||||
case notifications
|
||||
|
@ -29,6 +37,67 @@ enum TuskerRoute {
|
|||
case list(id: String)
|
||||
}
|
||||
|
||||
/// A class that manages running a sequence of navigation operations on a ``TuskerRootViewController``.
|
||||
///
|
||||
/// Use this type, rather than calling multiple methods on the root VC in a row, because it manages waiting until each previous step finishes.
|
||||
@MainActor
|
||||
final class TuskerNavigationSequence {
|
||||
private let root: any TuskerRootViewController
|
||||
private let animated: Bool
|
||||
private var operations = [() -> Void]()
|
||||
|
||||
init(root: any TuskerRootViewController, animated: Bool) {
|
||||
self.root = root
|
||||
self.animated = animated
|
||||
}
|
||||
|
||||
func select(route: TuskerRoute) {
|
||||
operations.append {
|
||||
self.root.select(route: route, animated: self.animated, completion: self.run)
|
||||
}
|
||||
}
|
||||
|
||||
func push(viewController: UIViewController) {
|
||||
operations.append {
|
||||
let nav = self.root.getNavigationController()
|
||||
nav.pushViewController(viewController, animated: self.animated)
|
||||
self.run()
|
||||
}
|
||||
}
|
||||
|
||||
func popToRoot() {
|
||||
operations.append {
|
||||
let nav = self.root.getNavigationController()
|
||||
nav.popToRootViewController(animated: self.animated)
|
||||
self.run()
|
||||
}
|
||||
}
|
||||
|
||||
func present(viewController: UIViewController) {
|
||||
operations.append {
|
||||
self.root.present(viewController, animated: self.animated, completion: self.run)
|
||||
}
|
||||
}
|
||||
|
||||
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
|
||||
operations.append {
|
||||
block(self.root.getNavigationController().topViewController, self.run)
|
||||
}
|
||||
}
|
||||
|
||||
func addOperation(_ operation: @escaping (_ completion: @escaping () -> Void) -> Void) {
|
||||
operations.append {
|
||||
operation(self.run)
|
||||
}
|
||||
}
|
||||
|
||||
func run() {
|
||||
if !operations.isEmpty {
|
||||
operations.removeFirst()()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
protocol NavigationControllerProtocol: UIViewController {
|
||||
var viewControllers: [UIViewController] { get set }
|
||||
|
|
|
@ -199,6 +199,7 @@ struct AdvancedPrefsView : View {
|
|||
} header: {
|
||||
Text("Feature Flags")
|
||||
}
|
||||
.appGroupedListRowBackground()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ struct PushInstanceSettingsView: View {
|
|||
}
|
||||
|
||||
let subscription = try await PushManager.shared.createSubscription(account: account)
|
||||
let mastodonController = await MastodonController.getForAccount(account)
|
||||
let mastodonController = MastodonController.getForAccount(account)
|
||||
do {
|
||||
let result = try await mastodonController.createPushSubscription(subscription: subscription)
|
||||
PushManager.logger.debug("Push subscription \(result.id, privacy: .public) created on \(account.instanceURL) with endpoint \(result.endpoint, privacy: .public)")
|
||||
|
@ -95,25 +95,25 @@ struct PushInstanceSettingsView: View {
|
|||
return true
|
||||
} catch {
|
||||
// if creation failed, remove the subscription locally as well
|
||||
await PushManager.shared.removeSubscription(account: account)
|
||||
PushManager.shared.removeSubscription(account: account)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private func disableNotifications() async throws {
|
||||
let mastodonController = await MastodonController.getForAccount(account)
|
||||
let mastodonController = MastodonController.getForAccount(account)
|
||||
try await mastodonController.deletePushSubscription()
|
||||
await PushManager.shared.removeSubscription(account: account)
|
||||
PushManager.shared.removeSubscription(account: account)
|
||||
subscription = nil
|
||||
PushManager.logger.debug("Push subscription removed on \(account.instanceURL)")
|
||||
}
|
||||
|
||||
private func updateSubscription(alerts: PushNotifications.PushSubscription.Alerts, policy: PushNotifications.PushSubscription.Policy) async -> Bool {
|
||||
let mastodonController = await MastodonController.getForAccount(account)
|
||||
let mastodonController = MastodonController.getForAccount(account)
|
||||
do {
|
||||
let result = try await mastodonController.updatePushSubscription(alerts: alerts, policy: policy)
|
||||
PushManager.logger.debug("Push subscription \(result.id, privacy: .public) updated on \(account.instanceURL)")
|
||||
await PushManager.shared.updateSubscription(account: account, alerts: alerts, policy: policy)
|
||||
PushManager.shared.updateSubscription(account: account, alerts: alerts, policy: policy)
|
||||
subscription?.alerts = alerts
|
||||
subscription?.policy = policy
|
||||
return true
|
||||
|
|
|
@ -63,11 +63,18 @@ class ProfileHeaderCollectionViewCell: UICollectionViewCell {
|
|||
}
|
||||
}
|
||||
|
||||
// overrides an internal method
|
||||
// when the super impl is used, preferredLayoutAttributesFitting(_:) isn't called while the view is offscreen (i.e., window == nil)
|
||||
// and so the collection view imposes a height of 44pts which breaks the layout
|
||||
@objc func _preferredLayoutAttributesFittingAttributes(_ attributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
|
||||
return preferredLayoutAttributesFitting(attributes)
|
||||
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
|
||||
switch state {
|
||||
case .unloaded:
|
||||
return super.preferredLayoutAttributesFitting(layoutAttributes)
|
||||
case .placeholder(let heightConstraint):
|
||||
layoutAttributes.size.height = heightConstraint.constant
|
||||
return layoutAttributes
|
||||
case .view(let profileHeaderView):
|
||||
let size = profileHeaderView.systemLayoutSizeFitting(layoutAttributes.size, withHorizontalFittingPriority: .required, verticalFittingPriority: .fittingSizeLevel)
|
||||
layoutAttributes.size = size
|
||||
return layoutAttributes
|
||||
}
|
||||
}
|
||||
|
||||
enum State {
|
||||
|
|
|
@ -213,7 +213,7 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
|
|||
|
||||
// old header cell must have the header view
|
||||
let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)!
|
||||
// Set the outgoing VC's header view mode to placeholder, so that it does steal the header view back
|
||||
// Set the outgoing VC's header view mode to placeholder, so that it does not steal the header view back
|
||||
// in case it updates the cell in the background.
|
||||
old.headerViewMode = .placeholder(height: oldHeaderCell.bounds.height)
|
||||
|
||||
|
@ -224,12 +224,13 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
|
|||
}
|
||||
|
||||
// disable user interaction during animation, to avoid any potential weird race conditions
|
||||
headerView.isUserInteractionEnabled = false
|
||||
view.isUserInteractionEnabled = false
|
||||
|
||||
headerView.layer.zPosition = 100
|
||||
view.addSubview(headerView)
|
||||
let oldHeaderCellTop = oldHeaderCell.convert(CGPoint.zero, to: view).y
|
||||
let headerTopOffset = oldHeaderCellTop - view.safeAreaInsets.top
|
||||
let headerBottomOffset = oldHeaderCell.convert(CGPoint(x: 0, y: oldHeaderCell.bounds.maxY), to: view).y// - view.safeAreaInsets.top
|
||||
let headerBottomOffset = oldHeaderCell.convert(CGPoint(x: 0, y: oldHeaderCell.bounds.maxY), to: view).y
|
||||
NSLayoutConstraint.activate([
|
||||
headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: headerTopOffset),
|
||||
headerView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: headerBottomOffset),
|
||||
|
@ -272,16 +273,17 @@ class ProfileViewController: UIViewController, StateRestorableViewController {
|
|||
}
|
||||
animator.addCompletion { _ in
|
||||
old.removeViewAndController()
|
||||
old.collectionView.transform = .identity
|
||||
old.view.transform = .identity
|
||||
|
||||
new.collectionView.transform = .identity
|
||||
new.view.transform = .identity
|
||||
new.collectionView.contentOffset = origOldContentOffset
|
||||
|
||||
// reenable scroll indicators after the switching animation is done
|
||||
old.collectionView.showsVerticalScrollIndicator = true
|
||||
new.collectionView.showsVerticalScrollIndicator = true
|
||||
|
||||
headerView.isUserInteractionEnabled = true
|
||||
self.view.isUserInteractionEnabled = true
|
||||
|
||||
headerView.transform = .identity
|
||||
headerView.layer.zPosition = 0
|
||||
// move the header view into the new page controller's cell
|
||||
|
|
|
@ -1295,7 +1295,7 @@ extension TimelineViewController {
|
|||
|
||||
// if we didn't add any items, that implies the gap was removed, and we want to to make clear what's happening
|
||||
if !addedItems {
|
||||
var config = ToastConfiguration(title: "There's nothing in between!")
|
||||
var config = ToastConfiguration(title: "That's all, folks!")
|
||||
config.dismissAutomaticallyAfter = 2
|
||||
showToast(configuration: config, animated: true)
|
||||
}
|
||||
|
|
|
@ -152,10 +152,10 @@ class MultiColumnNavigationController: UIViewController {
|
|||
let column = stackView.arrangedSubviews[columnIndex]
|
||||
let columnFrame = column.convert(column.bounds, to: scrollView)
|
||||
let offset: CGFloat
|
||||
if columnFrame.maxX < scrollView.bounds.width - scrollView.adjustedTrailingContentInset {
|
||||
if columnFrame.maxX <= view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right - scrollView.adjustedTrailingContentInset {
|
||||
offset = -scrollView.adjustedLeadingContentInset
|
||||
} else {
|
||||
offset = scrollView.contentSize.width - scrollView.bounds.width + scrollView.adjustedTrailingContentInset
|
||||
offset = columnFrame.maxX - scrollView.bounds.width + scrollView.adjustedTrailingContentInset
|
||||
}
|
||||
scrollView.setContentOffset(CGPoint(x: offset, y: -scrollView.adjustedContentInset.top), animated: animated)
|
||||
}
|
||||
|
|
|
@ -241,13 +241,19 @@ class SplitNavigationController: UIViewController {
|
|||
// otherwise the secondary nav's contents disappear immediately, rather than sliding off-screen
|
||||
let animator = UIViewPropertyAnimator(duration: 0.35, curve: .easeInOut) {
|
||||
self.isLayingOutForAnimation = true
|
||||
self.setSecondaryVisible(false)
|
||||
NSLayoutConstraint.deactivate(self.constraints)
|
||||
self.constraints = [
|
||||
self.rootNav.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: self.rootNav.view.bounds.minX),
|
||||
self.rootNav.view.trailingAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.trailingAnchor),
|
||||
self.secondaryNav.view.widthAnchor.constraint(equalToConstant: self.secondaryNav.view.bounds.width),
|
||||
]
|
||||
NSLayoutConstraint.activate(self.constraints)
|
||||
self.view.layoutIfNeeded()
|
||||
}
|
||||
animator.addCompletion { _ in
|
||||
self.secondaryNav.viewControllers = []
|
||||
self.isLayingOutForAnimation = false
|
||||
// self.updateSecondaryNavVisibility()
|
||||
self.secondaryNav.viewControllers = []
|
||||
self.updateSecondaryNavVisibility()
|
||||
}
|
||||
animator.startAnimation()
|
||||
} else {
|
||||
|
|
|
@ -43,9 +43,9 @@ extension NSUserActivity {
|
|||
}
|
||||
|
||||
@MainActor
|
||||
func handleResume(manager: UserActivityManager) async -> Bool {
|
||||
func handleResume(manager: UserActivityManager) -> Bool {
|
||||
guard let type = UserActivityType(rawValue: activityType) else { return false }
|
||||
await type.handle(manager)(self)
|
||||
type.handle(manager)(self)
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -16,93 +16,94 @@ import ComposeUI
|
|||
protocol UserActivityHandlingContext {
|
||||
var isHandoff: Bool { get }
|
||||
|
||||
func select(route: TuskerRoute) async
|
||||
func select(route: TuskerRoute, completion: (() -> Void)?)
|
||||
func present(_ vc: UIViewController)
|
||||
|
||||
var topViewController: UIViewController? { get }
|
||||
func select(route: TuskerRoute)
|
||||
func popToRoot()
|
||||
func push(_ vc: UIViewController)
|
||||
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void)
|
||||
|
||||
func present(_ vc: UIViewController)
|
||||
func compose(editing draft: Draft)
|
||||
|
||||
func finalize(activity: NSUserActivity)
|
||||
}
|
||||
|
||||
extension UserActivityHandlingContext {
|
||||
func select(route: TuskerRoute) async {
|
||||
await withCheckedContinuation { continuation in
|
||||
select(route: route) {
|
||||
continuation.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ActiveAccountUserActivityHandlingContext: UserActivityHandlingContext {
|
||||
let isHandoff: Bool
|
||||
let root: TuskerRootViewController
|
||||
var navigationDelegate: TuskerNavigationDelegate {
|
||||
root.getNavigationDelegate()!
|
||||
private let root: TuskerRootViewController
|
||||
private let navigation: TuskerNavigationSequence
|
||||
|
||||
init(isHandoff: Bool, root: TuskerRootViewController) {
|
||||
self.isHandoff = isHandoff
|
||||
self.root = root
|
||||
self.navigation = TuskerNavigationSequence(root: root, animated: true)
|
||||
}
|
||||
|
||||
func select(route: TuskerRoute, completion: (() -> Void)?) {
|
||||
root.select(route: route, animated: true, completion: completion)
|
||||
func select(route: TuskerRoute) {
|
||||
navigation.select(route: route)
|
||||
}
|
||||
|
||||
func present(_ vc: UIViewController) {
|
||||
navigationDelegate.present(vc, animated: true)
|
||||
navigation.present(viewController: vc)
|
||||
}
|
||||
|
||||
var topViewController: UIViewController? { root.getNavigationController().topViewController }
|
||||
|
||||
func popToRoot() {
|
||||
_ = root.getNavigationController().popToRootViewController(animated: true)
|
||||
navigation.popToRoot()
|
||||
}
|
||||
|
||||
func push(_ vc: UIViewController) {
|
||||
navigationDelegate.show(vc, sender: nil)
|
||||
navigation.push(viewController: vc)
|
||||
}
|
||||
|
||||
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
|
||||
navigation.withTopViewController(block)
|
||||
}
|
||||
|
||||
func compose(editing draft: Draft) {
|
||||
navigationDelegate.compose(editing: draft, animated: true, isDucked: true)
|
||||
navigation.addOperation { completion in
|
||||
root.compose(editing: draft, animated: true, isDucked: true, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func finalize(activity: NSUserActivity) {
|
||||
navigation.run()
|
||||
}
|
||||
}
|
||||
|
||||
class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
|
||||
private var state = State.initial
|
||||
let root: TuskerRootViewController
|
||||
private let root: TuskerRootViewController
|
||||
private let navigation: TuskerNavigationSequence
|
||||
|
||||
init(root: TuskerRootViewController) {
|
||||
self.root = root
|
||||
self.navigation = TuskerNavigationSequence(root: root, animated: false)
|
||||
}
|
||||
|
||||
var isHandoff: Bool { false }
|
||||
|
||||
func select(route: TuskerRoute, completion: (() -> Void)?) {
|
||||
root.select(route: route, animated: false) {
|
||||
self.state = .selectedRoute
|
||||
completion?()
|
||||
}
|
||||
var isHandoff: Bool {
|
||||
false
|
||||
}
|
||||
|
||||
var topViewController: UIViewController? { root.getNavigationController().topViewController }
|
||||
func select(route: TuskerRoute) {
|
||||
navigation.select(route: route)
|
||||
state = .selectedRoute
|
||||
}
|
||||
|
||||
func popToRoot() {
|
||||
// unnecessary during state restoration
|
||||
navigation.popToRoot()
|
||||
}
|
||||
|
||||
func push(_ vc: UIViewController) {
|
||||
precondition(state >= .selectedRoute)
|
||||
root.getNavigationController().pushViewController(vc, animated: false)
|
||||
navigation.push(viewController: vc)
|
||||
state = .pushed
|
||||
}
|
||||
|
||||
func withTopViewController(_ block: @escaping (_ topViewController: UIViewController?, _ completion: @escaping @MainActor () -> Void) -> Void) {
|
||||
navigation.withTopViewController(block)
|
||||
}
|
||||
|
||||
func present(_ vc: UIViewController) {
|
||||
root.present(vc, animated: false)
|
||||
navigation.present(viewController: vc)
|
||||
state = .presented
|
||||
}
|
||||
|
||||
|
@ -120,6 +121,7 @@ class StateRestorationUserActivityHandlingContext: UserActivityHandlingContext {
|
|||
|
||||
func finalize(activity: NSUserActivity) {
|
||||
precondition(state > .initial)
|
||||
navigation.run()
|
||||
#if !os(visionOS)
|
||||
if #available(iOS 16.0, *),
|
||||
let duckedDraft = UserActivityManager.getDuckedDraft(from: activity) {
|
||||
|
|
|
@ -133,12 +133,15 @@ class UserActivityManager {
|
|||
return activity
|
||||
}
|
||||
|
||||
func handleCheckNotifications(activity: NSUserActivity) async {
|
||||
await context.select(route: .notifications)
|
||||
func handleCheckNotifications(activity: NSUserActivity) {
|
||||
context.select(route: .notifications)
|
||||
context.popToRoot()
|
||||
if let notificationsPageController = context.topViewController as? NotificationsPageViewController {
|
||||
notificationsPageController.loadViewIfNeeded()
|
||||
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
|
||||
context.withTopViewController { topViewController, completion in
|
||||
if let notificationsPageController = topViewController as? NotificationsPageViewController {
|
||||
notificationsPageController.loadViewIfNeeded()
|
||||
notificationsPageController.selectMode(Self.getNotificationsMode(from: activity) ?? Preferences.shared.defaultNotificationsMode)
|
||||
}
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -204,32 +207,41 @@ class UserActivityManager {
|
|||
return (timeline, positionInfo)
|
||||
}
|
||||
|
||||
func handleShowTimeline(activity: NSUserActivity) async {
|
||||
func handleShowTimeline(activity: NSUserActivity) {
|
||||
guard let (timeline, positionInfo) = Self.getTimeline(from: activity) else { return }
|
||||
|
||||
var timelineVC: TimelineViewController?
|
||||
if let pinned = PinnedTimeline(timeline: timeline),
|
||||
mastodonController.accountPreferences.pinnedTimelines.contains(pinned) {
|
||||
await context.select(route: .timelines)
|
||||
context.select(route: .timelines)
|
||||
context.popToRoot()
|
||||
let pageController = context.topViewController as! TimelinesPageViewController
|
||||
pageController.selectTimeline(pinned, animated: false)
|
||||
timelineVC = pageController.currentViewController as? TimelineViewController
|
||||
context.withTopViewController { topViewController, completion in
|
||||
let pageController = topViewController as! TimelinesPageViewController
|
||||
pageController.selectTimeline(pinned, animated: false)
|
||||
}
|
||||
} else if case .list(let id) = timeline {
|
||||
await context.select(route: .list(id: id))
|
||||
timelineVC = context.topViewController as? TimelineViewController
|
||||
context.select(route: .list(id: id))
|
||||
} else {
|
||||
await context.select(route: .explore)
|
||||
context.select(route: .explore)
|
||||
context.popToRoot()
|
||||
timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
||||
context.push(timelineVC!)
|
||||
let timelineVC = TimelineViewController(for: timeline, mastodonController: mastodonController)
|
||||
context.push(timelineVC)
|
||||
}
|
||||
|
||||
if let timelineVC,
|
||||
let positionInfo,
|
||||
if let positionInfo,
|
||||
context.isHandoff {
|
||||
Task {
|
||||
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID)
|
||||
context.withTopViewController { topViewController, completion in
|
||||
let timelineVC: TimelineViewController
|
||||
if let topViewController = topViewController as? TimelineViewController {
|
||||
timelineVC = topViewController
|
||||
} else if let topViewController = topViewController as? TimelinesPageViewController {
|
||||
timelineVC = topViewController.currentViewController as! TimelineViewController
|
||||
} else {
|
||||
return
|
||||
}
|
||||
Task {
|
||||
await timelineVC.restoreStateFromHandoff(statusIDs: positionInfo.statusIDs, centerStatusID: positionInfo.centerStatusID)
|
||||
completion()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -249,11 +261,11 @@ class UserActivityManager {
|
|||
return activity.userInfo?["mainStatusID"] as? String
|
||||
}
|
||||
|
||||
func handleShowConversation(activity: NSUserActivity) async {
|
||||
func handleShowConversation(activity: NSUserActivity) {
|
||||
guard let mainStatusID = Self.getConversationStatus(from: activity) else {
|
||||
return
|
||||
}
|
||||
await context.select(route: .timelines)
|
||||
context.select(route: .timelines)
|
||||
context.push(ConversationViewController(for: mainStatusID, state: .unknown, mastodonController: mastodonController))
|
||||
}
|
||||
|
||||
|
@ -274,32 +286,34 @@ class UserActivityManager {
|
|||
return activity.userInfo?["query"] as? String
|
||||
}
|
||||
|
||||
func handleSearch(activity: NSUserActivity) async {
|
||||
await context.select(route: .explore)
|
||||
func handleSearch(activity: NSUserActivity) {
|
||||
context.select(route: .explore)
|
||||
context.popToRoot()
|
||||
|
||||
let searchController: UISearchController
|
||||
let resultsController: SearchResultsViewController
|
||||
if let explore = context.topViewController as? ExploreViewController {
|
||||
explore.loadViewIfNeeded()
|
||||
explore.searchControllerStatusOnAppearance = true
|
||||
searchController = explore.searchController
|
||||
resultsController = explore.resultsController
|
||||
} else if let inlineTrends = context.topViewController as? InlineTrendsViewController {
|
||||
inlineTrends.loadViewIfNeeded()
|
||||
inlineTrends.searchControllerStatusOnAppearance = true
|
||||
searchController = inlineTrends.searchController
|
||||
resultsController = inlineTrends.resultsController
|
||||
} else {
|
||||
return
|
||||
}
|
||||
context.withTopViewController { topViewController, completion in
|
||||
let searchController: UISearchController
|
||||
let resultsController: SearchResultsViewController
|
||||
if let explore = topViewController as? ExploreViewController {
|
||||
explore.loadViewIfNeeded()
|
||||
explore.searchControllerStatusOnAppearance = true
|
||||
searchController = explore.searchController
|
||||
resultsController = explore.resultsController
|
||||
} else if let inlineTrends = topViewController as? InlineTrendsViewController {
|
||||
inlineTrends.loadViewIfNeeded()
|
||||
inlineTrends.searchControllerStatusOnAppearance = true
|
||||
searchController = inlineTrends.searchController
|
||||
resultsController = inlineTrends.resultsController
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
if let query = Self.getSearchQuery(from: activity),
|
||||
!query.isEmpty {
|
||||
searchController.searchBar.text = query
|
||||
resultsController.performSearch(query: query)
|
||||
} else {
|
||||
searchController.searchBar.becomeFirstResponder()
|
||||
if let query = Self.getSearchQuery(from: activity),
|
||||
!query.isEmpty {
|
||||
searchController.searchBar.text = query
|
||||
resultsController.performSearch(query: query)
|
||||
} else {
|
||||
searchController.searchBar.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -311,8 +325,8 @@ class UserActivityManager {
|
|||
return activity
|
||||
}
|
||||
|
||||
func handleBookmarks(activity: NSUserActivity) async {
|
||||
await context.select(route: .bookmarks)
|
||||
func handleBookmarks(activity: NSUserActivity) {
|
||||
context.select(route: .bookmarks)
|
||||
}
|
||||
|
||||
// MARK: - My Profile
|
||||
|
@ -325,8 +339,8 @@ class UserActivityManager {
|
|||
return activity
|
||||
}
|
||||
|
||||
func handleMyProfile(activity: NSUserActivity) async {
|
||||
await context.select(route: .myProfile)
|
||||
func handleMyProfile(activity: NSUserActivity) {
|
||||
context.select(route: .myProfile)
|
||||
}
|
||||
|
||||
// MARK: - Show Profile
|
||||
|
@ -344,11 +358,11 @@ class UserActivityManager {
|
|||
return activity.userInfo?["profileID"] as? String
|
||||
}
|
||||
|
||||
func handleShowProfile(activity: NSUserActivity) async {
|
||||
func handleShowProfile(activity: NSUserActivity) {
|
||||
guard let accountID = Self.getProfile(from: activity) else {
|
||||
return
|
||||
}
|
||||
await context.select(route: .timelines)
|
||||
context.select(route: .timelines)
|
||||
context.push(ProfileViewController(accountID: accountID, mastodonController: mastodonController))
|
||||
}
|
||||
|
||||
|
@ -361,11 +375,11 @@ class UserActivityManager {
|
|||
return activity
|
||||
}
|
||||
|
||||
func handleShowNotification(activity: NSUserActivity) async {
|
||||
func handleShowNotification(activity: NSUserActivity) {
|
||||
guard let notificationID = activity.userInfo?["notificationID"] as? String else {
|
||||
return
|
||||
}
|
||||
await context.select(route: .notifications)
|
||||
context.select(route: .notifications)
|
||||
context.push(NotificationLoadingViewController(notificationID: notificationID, mastodonController: mastodonController))
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ enum UserActivityType: String {
|
|||
|
||||
extension UserActivityType {
|
||||
@MainActor
|
||||
var handle: (UserActivityManager) -> @MainActor (NSUserActivity) async -> Void {
|
||||
var handle: (UserActivityManager) -> @MainActor (NSUserActivity) -> Void {
|
||||
switch self {
|
||||
case .mainScene:
|
||||
fatalError("cannot handle main scene activity")
|
||||
|
|
|
@ -61,7 +61,9 @@ class GifvController {
|
|||
|
||||
private func updatePresentationSizeObservation() {
|
||||
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
|
||||
self.presentationSizeSubject.send(item.presentationSize)
|
||||
DispatchQueue.main.async {
|
||||
self.presentationSizeSubject.send(item.presentationSize)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
// https://help.apple.com/xcode/#/dev745c5c974
|
||||
|
||||
MARKETING_VERSION = 2024.4
|
||||
CURRENT_PROJECT_VERSION = 135
|
||||
CURRENT_PROJECT_VERSION = 136
|
||||
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
||||
|
||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
||||
|
|